feat(conversation): improve ConversationDropDownMenu and ConversationViewer with responsive actions and improved GlobalState handling

This commit is contained in:
Ivan committed 2026-04-12 17:41:00 -05:00
1 parent 5e66dc05e2
commit 3a3b7de10c
3 files changed
+94 -18

No files matched your search

@@ -1,5 +1,5 @@
<template>
<DropDownMenu>
<DropDownMenu v-if="compact">
<template #button>
<IconButton>
<MaterialDesignIcon icon-name="dots-vertical" class="size-7" />
@@ -64,7 +64,7 @@
</div>
<!-- telemetry trust toggle -->
<div v-if="GlobalState.config?.telemetry_enabled" class="border-t">
<div v-if="GlobalState?.config?.telemetry_enabled" class="border-t">
<DropDownMenuItem @click="onToggleTelemetryTrust">
<MaterialDesignIcon
:icon-name="contact?.is_telemetry_trusted ? 'shield-check' : 'shield-outline'"
@@ -80,6 +80,55 @@
</div>
</template>
</DropDownMenu>
<div v-else class="flex items-center gap-0.5 sm:gap-1 flex-wrap justify-end max-w-[min(100%,52vw)] sm:max-w-none">
<IconButton
v-if="hasFailedMessages"
:title="$t('messages.retry_failed')"
class="shrink-0"
@click="$emit('retry-failed')"
>
<MaterialDesignIcon icon-name="refresh" class="size-5 text-red-500" />
</IconButton>
<IconButton :title="$t('messages.telemetry_history')" class="shrink-0" @click="$emit('open-telemetry-history')">
<MaterialDesignIcon icon-name="satellite-variant" class="size-5" />
</IconButton>
<IconButton :title="$t('messages.start_call')" class="shrink-0" @click="$emit('start-call')">
<MaterialDesignIcon icon-name="phone" class="size-5" />
</IconButton>
<IconButton :title="$t('messages.share_contact')" class="shrink-0" @click="$emit('share-contact')">
<MaterialDesignIcon icon-name="notebook-outline" class="size-5" />
</IconButton>
<IconButton :title="$t('messages.pop_out_chat')" class="shrink-0" @click="$emit('popout')">
<MaterialDesignIcon icon-name="open-in-new" class="size-5" />
</IconButton>
<IconButton title="Ping Destination" class="shrink-0" @click="onPingDestination">
<MaterialDesignIcon icon-name="flash" class="size-5" />
</IconButton>
<IconButton :title="$t('messages.custom_display_name')" class="shrink-0" @click="onSetCustomDisplayName">
<MaterialDesignIcon icon-name="account-edit" class="size-5" />
</IconButton>
<IconButton v-if="!isBlocked" title="Banish User" class="shrink-0" @click="onBlockDestination">
<MaterialDesignIcon icon-name="gavel" class="size-5 text-red-500" />
</IconButton>
<IconButton v-else title="Lift Banishment" class="shrink-0" @click="onUnblockDestination">
<MaterialDesignIcon icon-name="check-circle" class="size-5 text-green-500" />
</IconButton>
<IconButton title="Delete Message History" class="shrink-0" @click="onDeleteMessageHistory">
<MaterialDesignIcon icon-name="delete" class="size-5 text-red-500" />
</IconButton>
<IconButton
v-if="GlobalState?.config?.telemetry_enabled"
:title="contact?.is_telemetry_trusted ? $t('app.telemetry_trust_revoke') : $t('app.telemetry_trust_grant')"
class="shrink-0"
@click="onToggleTelemetryTrust"
>
<MaterialDesignIcon
:icon-name="contact?.is_telemetry_trusted ? 'shield-check' : 'shield-outline'"
:class="contact?.is_telemetry_trusted ? 'text-blue-500' : 'text-gray-500'"
class="size-5"
/>
</IconButton>
</div>
</template>
<script>
@@ -108,6 +157,10 @@ export default {
type: Boolean,
default: false,
},
compact: {
type: Boolean,
default: true,
},
},
emits: [
"conversation-deleted",
@@ -123,6 +176,7 @@ export default {
data() {
return {
contact: null,
GlobalState,
};
},
computed: {
@@ -6,23 +6,23 @@
>
<!-- banished overlay -->
<div
v-if="GlobalState.config.banished_effect_enabled && isSelectedPeerBlocked"
v-if="GlobalState?.config?.banished_effect_enabled && isSelectedPeerBlocked"
class="banished-overlay"
:style="{ background: GlobalState.config.banished_color + '33' }"
:style="{ background: (GlobalState?.config?.banished_color || '#dc2626') + '33' }"
>
<span
class="banished-text !opacity-100 !text-white !shadow-lg !bg-red-600 !px-4 !py-2 !rounded-xl !border-2 !tracking-widest"
:style="{
'background-color': GlobalState.config.banished_color,
'border-color': GlobalState.config.banished_color,
'background-color': GlobalState?.config?.banished_color || '#dc2626',
'border-color': GlobalState?.config?.banished_color || '#dc2626',
}"
>{{ GlobalState.config.banished_text }}</span
>{{ GlobalState?.config?.banished_text || "BANISHED" }}</span
>
</div>
<!-- header -->
<div
class="relative z-20 flex items-center px-4 py-3 border-b border-gray-200/60 dark:border-zinc-800/60 bg-white/80 dark:bg-zinc-900/50 backdrop-blur-sm"
class="relative z-20 flex flex-wrap items-center gap-y-2 px-3 sm:px-4 py-3 border-b border-gray-200/60 dark:border-zinc-800/60 bg-white/80 dark:bg-zinc-900/50 backdrop-blur-sm"
>
<!-- peer icon -->
<div class="flex-shrink-0 mr-3">
@@ -127,6 +127,7 @@
<ConversationDropDownMenu
v-if="selectedPeer"
:peer="selectedPeer"
:compact="compactPeerActions"
:has-failed-messages="hasFailedOrCancelledMessages"
@conversation-deleted="onConversationDeleted"
@set-custom-display-name="updateCustomDisplayName"
@@ -1563,7 +1564,7 @@
:is-sending-message="false"
:can-send-message="canSendMessage"
:delivery-method="newMessageDeliveryMethod"
:compact="isMobile"
:compact="compactSendLayout"
:sending-tooltip="sendMessagePathfindingTooltip"
@send="sendMessage"
@delivery-method-changed="newMessageDeliveryMethod = $event"
@@ -2268,7 +2269,8 @@ export default {
},
myLxmfAddressHash: {
type: String,
required: true,
required: false,
default: "",
},
selectedPeer: {
type: Object,
@@ -2363,14 +2365,22 @@ export default {
updateTimer: null,
sendStatusUiMs: Date.now(),
sendStatusTickInterval: null,
windowWidth: typeof window !== "undefined" ? window.innerWidth : 1024,
};
},
computed: {
compactPeerActions() {
return this.windowWidth < 640;
},
compactSendLayout() {
return this.windowWidth < 640;
},
bubbleStyles() {
void GlobalState.detailedOutboundSendStatus;
void this.sendStatusUiMs;
return (chatItem) => {
const styles = {};
const cfg = GlobalState?.config;
const m = chatItem.lxmf_message;
const isFailed = ["cancelled", "failed"].includes(m.state);
@@ -2380,24 +2390,24 @@ export default {
styles["color"] = "#ffffff";
return styles;
}
const color = GlobalState.config.message_failed_bubble_color || "#ef4444";
const color = cfg?.message_failed_bubble_color || "#ef4444";
styles["background-color"] = color;
styles["color"] = "#ffffff";
} else if (chatItem.is_outbound) {
if (chatItem.lxmf_message?._pendingPathfinding) {
if (!this.showRichOutboundPendingUi(chatItem)) {
const color = GlobalState.config.message_outbound_bubble_color || "#4f46e5";
const color = cfg?.message_outbound_bubble_color || "#4f46e5";
styles["background-color"] = color;
styles["color"] = "#ffffff";
return styles;
}
return {};
}
const color = GlobalState.config.message_outbound_bubble_color || "#4f46e5";
const color = cfg?.message_outbound_bubble_color || "#4f46e5";
styles["background-color"] = color;
styles["color"] = "#ffffff";
} else if (GlobalState.config.message_inbound_bubble_color) {
styles["background-color"] = GlobalState.config.message_inbound_bubble_color;
} else if (cfg?.message_inbound_bubble_color) {
styles["background-color"] = cfg.message_inbound_bubble_color;
}
return styles;
@@ -2701,8 +2711,16 @@ export default {
}, 2000);
this._scheduleOutboundSendStatusTick();
this._onWindowResize = () => {
this.windowWidth = window.innerWidth;
};
window.addEventListener("resize", this._onWindowResize);
},
beforeUnmount() {
if (this._onWindowResize) {
window.removeEventListener("resize", this._onWindowResize);
}
if (this.updateTimer) {
clearInterval(this.updateTimer);
}
@@ -48,7 +48,7 @@
<ConversationViewer
ref="conversation-viewer"
:config="config"
:my-lxmf-address-hash="config?.lxmf_address_hash"
:my-lxmf-address-hash="config?.lxmf_address_hash || ''"
:selected-peer="selectedPeer"
:conversations="conversations"
@update:selected-peer="onPeerClick"
@@ -126,6 +126,10 @@ import WebSocketConnection from "../../js/WebSocketConnection";
import MessagesSidebar from "./MessagesSidebar.vue";
import ConversationViewer from "./ConversationViewer.vue";
import GlobalState, { mergeGlobalConfig } from "../../js/GlobalState";
function snapshotGlobalConfig() {
return GlobalState.config && typeof GlobalState.config === "object" ? { ...GlobalState.config } : {};
}
import DialogUtils from "../../js/DialogUtils";
import GlobalEmitter from "../../js/GlobalEmitter";
import ToastUtils from "../../js/ToastUtils";
@@ -150,7 +154,7 @@ export default {
reloadInterval: null,
conversationRefreshTimeout: null,
config: null,
config: snapshotGlobalConfig(),
hasLoadedConversations: false,
peers: {},
selectedPeer: null,
@@ -283,8 +287,8 @@ export default {
this.config = next;
}
} catch (e) {
// do nothing if failed to load config
console.log(e);
this.config = snapshotGlobalConfig();
}
},
async onWebsocketMessage(message) {