mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-10-23 19:12:53 +00:00
feat(App, Contacts, Messages, Settings, Tools): improve UI components with loading overlays, contact editing, and new license links; improve display name handling and auto-select node descriptions
This commit is contained in:
7 files changed
+141
-33
No files matched your search
@@ -47,13 +47,7 @@
|
||||
{{ $t("app.name") }}
|
||||
</div>
|
||||
<div class="hidden sm:block text-sm text-gray-600 dark:text-zinc-300">
|
||||
{{ $t("app.custom_fork_by") }}
|
||||
<a
|
||||
target="_blank"
|
||||
:href="`${giteaBaseUrl}/Sudo-Ivan`"
|
||||
class="text-blue-500 dark:text-blue-300 hover:underline"
|
||||
>Sudo-Ivan</a
|
||||
>
|
||||
{{ $t("app.tagline") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex my-auto ml-auto mr-0 sm:mr-2 space-x-1 sm:space-x-2">
|
||||
@@ -535,21 +529,36 @@
|
||||
<transition name="fade-blur">
|
||||
<div
|
||||
v-if="isSwitchingIdentity"
|
||||
class="fixed inset-0 z-[200] flex items-center justify-center bg-white/10 dark:bg-black/10 backdrop-blur-md"
|
||||
class="fixed inset-0 z-[200] flex items-center justify-center bg-slate-900/45 dark:bg-black/55 backdrop-blur-sm px-4"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="w-full max-w-sm overflow-hidden rounded-2xl border border-gray-200/90 bg-white/95 shadow-xl dark:border-zinc-700/90 dark:bg-zinc-900/95"
|
||||
>
|
||||
<div class="px-6 pt-7 pb-1 text-center">
|
||||
<div
|
||||
class="w-20 h-20 border-4 border-blue-500/20 border-t-blue-500 rounded-full animate-spin"
|
||||
></div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<MaterialDesignIcon icon-name="account-sync" class="w-8 h-8 text-blue-500 animate-pulse" />
|
||||
class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-slate-50 ring-1 ring-gray-200/80 dark:bg-zinc-950 dark:ring-zinc-700"
|
||||
>
|
||||
<img :src="logoUrl" alt="" class="h-9 w-9 object-contain p-1" />
|
||||
</div>
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-zinc-500">
|
||||
{{ $t("app.loading_overlay_kicker") }}
|
||||
</p>
|
||||
<h2 class="mt-1.5 text-lg font-semibold tracking-tight text-gray-900 dark:text-white">
|
||||
{{ $t("app.loading_overlay_title") }}
|
||||
</h2>
|
||||
<p class="mt-3 text-sm leading-relaxed text-gray-600 dark:text-zinc-400">
|
||||
{{ $t("app.loading_overlay_subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="px-6 pb-7 pt-2">
|
||||
<div class="h-1 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-zinc-800">
|
||||
<div
|
||||
class="identity-switch-indeterminate h-full w-1/3 rounded-full bg-blue-600 dark:bg-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 text-xl font-bold text-gray-900 dark:text-white tracking-tight">
|
||||
{{ $t("app.switching_identity") }}
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ $t("app.loading_identity") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -649,9 +658,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
giteaBaseUrl() {
|
||||
return this.config?.gitea_base_url || "https://git.quad4.io";
|
||||
},
|
||||
currentPopoutType() {
|
||||
if (this.$route?.meta?.popoutType) {
|
||||
return this.$route.meta.popoutType;
|
||||
@@ -810,7 +816,7 @@ export default {
|
||||
if (this.isSwitchingIdentity) {
|
||||
this.isSwitchingIdentity = false;
|
||||
}
|
||||
}, 10000);
|
||||
}, 45000);
|
||||
},
|
||||
onSyncPropagationNodeShell() {
|
||||
this.syncPropagationNode();
|
||||
@@ -1528,4 +1534,17 @@ export default {
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(0);
|
||||
}
|
||||
|
||||
@keyframes identity-switch-indeterminate {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(350%);
|
||||
}
|
||||
}
|
||||
|
||||
.identity-switch-indeterminate {
|
||||
animation: identity-switch-indeterminate 1.4s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -44,6 +44,13 @@
|
||||
<v-icon icon="mdi-history" size="20" class="mr-2"></v-icon>
|
||||
{{ $t("app.changelog_title") }}
|
||||
</button>
|
||||
<router-link
|
||||
:to="{ name: 'licenses' }"
|
||||
class="secondary-chip flex-1 min-[480px]:flex-none min-h-[44px] sm:min-h-0 justify-center inline-flex items-center no-underline"
|
||||
>
|
||||
<v-icon icon="mdi-license" size="20" class="mr-2"></v-icon>
|
||||
{{ $t("about.third_party_licenses") }}
|
||||
</router-link>
|
||||
<button
|
||||
v-if="isElectron"
|
||||
type="button"
|
||||
|
||||
@@ -154,6 +154,10 @@
|
||||
{{ $t("contacts.call_contact") }}
|
||||
</button>
|
||||
<div class="border-t border-gray-100 dark:border-zinc-800 my-1"></div>
|
||||
<button type="button" class="context-item" @click="editContactName(contextMenu.contact)">
|
||||
<MaterialDesignIcon icon-name="pencil-outline" class="size-4" />
|
||||
{{ $t("contacts.edit_contact") }}
|
||||
</button>
|
||||
<button type="button" class="context-item" @click="shareContact(contextMenu.contact)">
|
||||
<MaterialDesignIcon icon-name="share-variant" class="size-4" />
|
||||
{{ $t("contacts.share_contact") }}
|
||||
@@ -377,6 +381,7 @@ import QRCode from "qrcode";
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import WebSocketConnection from "../../js/WebSocketConnection";
|
||||
import ToastUtils from "../../js/ToastUtils";
|
||||
import DialogUtils from "../../js/DialogUtils";
|
||||
|
||||
import LxmfUserIcon from "../LxmfUserIcon.vue";
|
||||
|
||||
@@ -680,6 +685,26 @@ export default {
|
||||
ToastUtils.error(this.$t("contacts.failed_remove_contact"));
|
||||
}
|
||||
},
|
||||
async editContactName(contact) {
|
||||
this.closeContextMenu();
|
||||
if (!contact?.id) return;
|
||||
const name = await DialogUtils.prompt(this.$t("contacts.enter_contact_name"), contact.name);
|
||||
if (name == null || name === contact.name) return;
|
||||
try {
|
||||
await window.api.patch(`/api/v1/telephone/contacts/${contact.id}`, { name });
|
||||
const destHash =
|
||||
contact.remote_destination_hash || contact.lxmf_address || contact.remote_identity_hash;
|
||||
if (destHash && name.length > 0) {
|
||||
await window.api.post(`/api/v1/destination/${destHash}/custom-display-name/update`, {
|
||||
display_name: name,
|
||||
});
|
||||
}
|
||||
ToastUtils.success(this.$t("contacts.contact_updated"));
|
||||
await this.getContacts();
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("contacts.failed_update_contact"));
|
||||
}
|
||||
},
|
||||
openConversation(contact) {
|
||||
this.closeContextMenu();
|
||||
const hash = contact.remote_destination_hash || contact.lxmf_address || contact.remote_identity_hash;
|
||||
|
||||
@@ -3458,19 +3458,16 @@ export default {
|
||||
}
|
||||
},
|
||||
async updateCustomDisplayName() {
|
||||
// do nothing if no peer selected
|
||||
if (!this.selectedPeer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ask user for new display name
|
||||
const displayName = await DialogUtils.prompt(this.$t("messages.enter_display_name"));
|
||||
if (displayName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// update display name on server
|
||||
await window.api.post(
|
||||
`/api/v1/destination/${this.selectedPeer.destination_hash}/custom-display-name/update`,
|
||||
{
|
||||
@@ -3478,10 +3475,21 @@ export default {
|
||||
}
|
||||
);
|
||||
|
||||
// update display name in ui
|
||||
await this.getCustomDisplayName();
|
||||
if (displayName.length > 0) {
|
||||
try {
|
||||
const checkResp = await window.api.get(
|
||||
`/api/v1/telephone/contacts/check/${this.selectedPeer.destination_hash}`
|
||||
);
|
||||
const contactId = checkResp.data?.contact?.id;
|
||||
if (contactId) {
|
||||
await window.api.patch(`/api/v1/telephone/contacts/${contactId}`, { name: displayName });
|
||||
}
|
||||
} catch {
|
||||
// non-critical
|
||||
}
|
||||
}
|
||||
|
||||
// reload conversations (so conversations list updates name)
|
||||
await this.getCustomDisplayName();
|
||||
this.$emit("reload-conversations");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -427,10 +427,18 @@ export default {
|
||||
for (const conversation of newConversations) {
|
||||
if (!conversation?.destination_hash) continue;
|
||||
const existingPeer = this.peers[conversation.destination_hash] || {};
|
||||
let displayName = conversation.display_name ?? existingPeer.display_name;
|
||||
if (
|
||||
displayName === "Anonymous Peer" &&
|
||||
existingPeer.display_name &&
|
||||
existingPeer.display_name !== "Anonymous Peer"
|
||||
) {
|
||||
displayName = existingPeer.display_name;
|
||||
}
|
||||
this.peers[conversation.destination_hash] = {
|
||||
...existingPeer,
|
||||
destination_hash: conversation.destination_hash,
|
||||
display_name: conversation.display_name ?? existingPeer.display_name,
|
||||
display_name: displayName,
|
||||
custom_display_name: conversation.custom_display_name ?? existingPeer.custom_display_name,
|
||||
contact_image: conversation.contact_image ?? existingPeer.contact_image,
|
||||
lxmf_user_icon: conversation.lxmf_user_icon ?? existingPeer.lxmf_user_icon,
|
||||
@@ -528,7 +536,9 @@ export default {
|
||||
|
||||
const conv = this.conversations.find((c) => c.destination_hash === peerHash);
|
||||
if (conv) {
|
||||
if (fresh.display_name) conv.display_name = fresh.display_name;
|
||||
if (fresh.display_name && fresh.display_name !== "Anonymous Peer") {
|
||||
conv.display_name = fresh.display_name;
|
||||
}
|
||||
if (fresh.custom_display_name) conv.custom_display_name = fresh.custom_display_name;
|
||||
if (fresh.contact_image) conv.contact_image = fresh.contact_image;
|
||||
if (fresh.lxmf_user_icon) conv.lxmf_user_icon = fresh.lxmf_user_icon;
|
||||
@@ -536,10 +546,15 @@ export default {
|
||||
}
|
||||
|
||||
if (this.selectedPeer && this.selectedPeer.destination_hash === peerHash) {
|
||||
if (fresh.display_name && fresh.display_name !== this.selectedPeer.display_name) {
|
||||
const incomingName = fresh.display_name;
|
||||
const shouldUpdate =
|
||||
incomingName &&
|
||||
incomingName !== "Anonymous Peer" &&
|
||||
incomingName !== this.selectedPeer.display_name;
|
||||
if (shouldUpdate) {
|
||||
this.selectedPeer = {
|
||||
...this.selectedPeer,
|
||||
display_name: fresh.display_name,
|
||||
display_name: incomingName,
|
||||
custom_display_name: fresh.custom_display_name ?? this.selectedPeer.custom_display_name,
|
||||
};
|
||||
}
|
||||
@@ -697,7 +712,15 @@ export default {
|
||||
},
|
||||
updatePeerFromAnnounce: function (announce) {
|
||||
const existing = this.peers[announce.destination_hash] || {};
|
||||
this.peers[announce.destination_hash] = { ...existing, ...announce };
|
||||
const merged = { ...existing, ...announce };
|
||||
if (
|
||||
announce.display_name === "Anonymous Peer" &&
|
||||
existing.display_name &&
|
||||
existing.display_name !== "Anonymous Peer"
|
||||
) {
|
||||
merged.display_name = existing.display_name;
|
||||
}
|
||||
this.peers[announce.destination_hash] = merged;
|
||||
},
|
||||
onUpdatePeerTracking({ destination_hash, is_tracking }) {
|
||||
if (this.peers[destination_hash]) {
|
||||
|
||||
@@ -1682,6 +1682,20 @@
|
||||
<span class="setting-toggle__description">{{
|
||||
$t("app.auto_select_node_description")
|
||||
}}</span>
|
||||
<span
|
||||
v-if="config.lxmf_preferred_propagation_node_auto_select"
|
||||
class="setting-toggle__hint block mt-1 text-xs text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<template v-if="config.lxmf_preferred_propagation_node_destination_hash">
|
||||
<span class="block">{{ $t("app.auto_select_using_label") }}</span>
|
||||
<span class="monospace-field break-all block mt-0.5">{{
|
||||
config.lxmf_preferred_propagation_node_destination_hash
|
||||
}}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t("app.auto_select_pending") }}
|
||||
</template>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="space-y-2">
|
||||
@@ -2037,6 +2051,10 @@ export default {
|
||||
"app.browse_nodes",
|
||||
"app.run_local_node",
|
||||
"app.run_local_node_description",
|
||||
"app.auto_select_node",
|
||||
"app.auto_select_node_description",
|
||||
"app.auto_select_using_label",
|
||||
"app.auto_select_pending",
|
||||
"app.preferred_propagation_node",
|
||||
"app.auto_sync_interval",
|
||||
"app.propagation_stamp_cost",
|
||||
|
||||
@@ -207,6 +207,14 @@ export default {
|
||||
titleKey: "docs.title",
|
||||
descriptionKey: "docs.subtitle",
|
||||
},
|
||||
{
|
||||
name: "licenses",
|
||||
route: { name: "licenses" },
|
||||
icon: "license",
|
||||
iconBg: "tool-card__icon bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-300",
|
||||
titleKey: "licenses.title",
|
||||
descriptionKey: "licenses.short_description",
|
||||
},
|
||||
{
|
||||
name: "micron-editor",
|
||||
route: { name: "micron-editor" },
|
||||
|
||||
Reference in new issue
Block a user