mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-25 15:22:10 +00:00
feat(conversation): add detailed outbound send status toggle and improve configuration handling
This commit is contained in:
@@ -562,7 +562,7 @@ import { useTheme } from "vuetify";
|
||||
import SidebarLink from "./SidebarLink.vue";
|
||||
import DialogUtils from "../js/DialogUtils";
|
||||
import WebSocketConnection from "../js/WebSocketConnection";
|
||||
import GlobalState from "../js/GlobalState";
|
||||
import GlobalState, { mergeGlobalConfig } from "../js/GlobalState";
|
||||
import Utils from "../js/Utils";
|
||||
import GlobalEmitter from "../js/GlobalEmitter";
|
||||
import NotificationUtils from "../js/NotificationUtils";
|
||||
@@ -712,6 +712,14 @@ export default {
|
||||
this.toneGenerator.stop();
|
||||
},
|
||||
mounted() {
|
||||
try {
|
||||
const v = localStorage.getItem("meshchatx_detailed_outbound_send_status");
|
||||
if (v === "true" || v === "false") {
|
||||
GlobalState.detailedOutboundSendStatus = v === "true";
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
this.startShellAuthWatch();
|
||||
if (ElectronUtils.isElectron()) {
|
||||
window.electron.onProtocolLink((url) => {
|
||||
@@ -820,9 +828,11 @@ export default {
|
||||
this.$refs.tutorialModal?.show();
|
||||
},
|
||||
onConfigUpdatedExternally(newConfig) {
|
||||
if (!newConfig) return;
|
||||
if (!newConfig || typeof newConfig !== "object") {
|
||||
return;
|
||||
}
|
||||
mergeGlobalConfig(newConfig);
|
||||
this.config = newConfig;
|
||||
GlobalState.config = newConfig;
|
||||
this.displayName = newConfig.display_name;
|
||||
},
|
||||
applyThemePreference(theme) {
|
||||
@@ -830,8 +840,9 @@ export default {
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.classList.toggle("dark", mode === "dark");
|
||||
}
|
||||
if (this.vuetifyTheme?.global?.name) {
|
||||
this.vuetifyTheme.global.name.value = mode;
|
||||
const themeName = this.vuetifyTheme?.global?.name;
|
||||
if (themeName && typeof themeName === "object" && "value" in themeName) {
|
||||
themeName.value = mode;
|
||||
}
|
||||
},
|
||||
getHashPopoutValue() {
|
||||
@@ -843,9 +854,12 @@ export default {
|
||||
const json = JSON.parse(message.data);
|
||||
switch (json.type) {
|
||||
case "config": {
|
||||
this.config = json.config;
|
||||
GlobalState.config = json.config;
|
||||
this.displayName = json.config.display_name;
|
||||
const next = json?.config;
|
||||
if (next && typeof next === "object") {
|
||||
mergeGlobalConfig(next);
|
||||
this.config = next;
|
||||
this.displayName = next.display_name;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "keyboard_shortcuts": {
|
||||
@@ -1020,9 +1034,12 @@ export default {
|
||||
async getConfig() {
|
||||
try {
|
||||
const response = await window.api.get(`/api/v1/config`);
|
||||
this.config = response.data.config;
|
||||
GlobalState.config = response.data.config;
|
||||
this.displayName = response.data.config.display_name;
|
||||
const next = response.data?.config;
|
||||
if (next && typeof next === "object") {
|
||||
mergeGlobalConfig(next);
|
||||
this.config = next;
|
||||
this.displayName = next.display_name;
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing if failed to load config
|
||||
console.log(e);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
class="cursor-default relative inline-block text-left"
|
||||
>
|
||||
<!-- menu button -->
|
||||
<div ref="dropdown-button" @click.stop="toggleMenu">
|
||||
<div ref="dropdown-button" class="touch-manipulation" @click.stop="toggleMenu">
|
||||
<slot name="button" />
|
||||
</div>
|
||||
|
||||
@@ -58,9 +58,8 @@ export default {
|
||||
this.isShowingMenu = false;
|
||||
this.dropdownPosition = null;
|
||||
},
|
||||
onClickOutsideMenu(event) {
|
||||
onClickOutsideMenu() {
|
||||
if (this.isShowingMenu) {
|
||||
event.preventDefault();
|
||||
this.hideMenu();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,10 +6,29 @@
|
||||
</IconButton>
|
||||
</template>
|
||||
<template #items>
|
||||
<DropDownMenuItem v-if="hasFailedMessages" @click="$emit('retry-failed')">
|
||||
<MaterialDesignIcon icon-name="refresh" class="size-5 text-red-500" />
|
||||
<span>{{ $t("messages.retry_failed") }}</span>
|
||||
</DropDownMenuItem>
|
||||
<DropDownMenuItem @click="$emit('open-telemetry-history')">
|
||||
<MaterialDesignIcon icon-name="satellite-variant" class="size-5" />
|
||||
<span>{{ $t("messages.telemetry_history") }}</span>
|
||||
</DropDownMenuItem>
|
||||
<DropDownMenuItem @click="$emit('start-call')">
|
||||
<MaterialDesignIcon icon-name="phone" class="size-5" />
|
||||
<span>{{ $t("messages.start_call") }}</span>
|
||||
</DropDownMenuItem>
|
||||
<DropDownMenuItem @click="$emit('share-contact')">
|
||||
<MaterialDesignIcon icon-name="notebook-outline" class="size-5" />
|
||||
<span>{{ $t("messages.share_contact") }}</span>
|
||||
</DropDownMenuItem>
|
||||
|
||||
<div class="border-t border-gray-100 dark:border-zinc-800" />
|
||||
|
||||
<!-- popout button -->
|
||||
<DropDownMenuItem @click="$emit('popout')">
|
||||
<MaterialDesignIcon icon-name="open-in-new" class="size-5" />
|
||||
<span>Popout Chat</span>
|
||||
<span>{{ $t("messages.pop_out_chat") }}</span>
|
||||
</DropDownMenuItem>
|
||||
|
||||
<!-- ping button -->
|
||||
@@ -45,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'"
|
||||
@@ -85,6 +104,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasFailedMessages: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
"conversation-deleted",
|
||||
@@ -92,6 +115,10 @@ export default {
|
||||
"block-status-changed",
|
||||
"popout",
|
||||
"view-telemetry-history",
|
||||
"retry-failed",
|
||||
"open-telemetry-history",
|
||||
"start-call",
|
||||
"share-contact",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
@@ -208,16 +235,13 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// delete all lxmf messages from "us to destination" and from "destination to us"
|
||||
try {
|
||||
await window.api.delete(`/api/v1/lxmf-messages/conversation/${this.peer.destination_hash}`);
|
||||
this.$emit("conversation-deleted");
|
||||
} catch (e) {
|
||||
DialogUtils.alert(this.$t("messages.failed_delete_history"));
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
// fire callback
|
||||
this.$emit("conversation-deleted");
|
||||
},
|
||||
async onSetCustomDisplayName() {
|
||||
this.$emit("set-custom-display-name");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@
|
||||
:has-more-announces="hasMoreAnnounces"
|
||||
:peers-search-term="peersSearchTerm"
|
||||
:total-peers-count="totalPeersCount"
|
||||
:pinned-peer-hashes="pinnedPeerHashes"
|
||||
@conversation-click="onConversationClick"
|
||||
@peer-click="onPeerClick"
|
||||
@conversation-search-changed="onConversationSearchChanged"
|
||||
@@ -36,6 +37,7 @@
|
||||
@bulk-delete="onBulkDelete"
|
||||
@export-folders="onExportFolders"
|
||||
@import-folders="onImportFolders"
|
||||
@toggle-conversation-pin="onToggleConversationPin"
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -123,7 +125,7 @@
|
||||
import WebSocketConnection from "../../js/WebSocketConnection";
|
||||
import MessagesSidebar from "./MessagesSidebar.vue";
|
||||
import ConversationViewer from "./ConversationViewer.vue";
|
||||
import GlobalState from "../../js/GlobalState";
|
||||
import GlobalState, { mergeGlobalConfig } from "../../js/GlobalState";
|
||||
import DialogUtils from "../../js/DialogUtils";
|
||||
import GlobalEmitter from "../../js/GlobalEmitter";
|
||||
import ToastUtils from "../../js/ToastUtils";
|
||||
@@ -172,6 +174,8 @@ export default {
|
||||
filterHasAttachmentsOnly: false,
|
||||
isLoadingConversations: false,
|
||||
|
||||
pinnedPeerHashes: [],
|
||||
|
||||
isIngestModalOpen: false,
|
||||
ingestUri: "",
|
||||
};
|
||||
@@ -216,6 +220,7 @@ export default {
|
||||
|
||||
this.getConfig();
|
||||
this.getConversations();
|
||||
this.loadConversationPins();
|
||||
this.getFolders();
|
||||
this.getLxmfDeliveryAnnounces();
|
||||
|
||||
@@ -272,7 +277,11 @@ export default {
|
||||
async getConfig() {
|
||||
try {
|
||||
const response = await window.api.get(`/api/v1/config`);
|
||||
this.config = response.data.config;
|
||||
const next = response.data?.config;
|
||||
if (next && typeof next === "object") {
|
||||
mergeGlobalConfig(next);
|
||||
this.config = next;
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing if failed to load config
|
||||
console.log(e);
|
||||
@@ -282,7 +291,11 @@ export default {
|
||||
const json = JSON.parse(message.data);
|
||||
switch (json.type) {
|
||||
case "config": {
|
||||
this.config = json.config;
|
||||
const next = json?.config;
|
||||
if (next && typeof next === "object") {
|
||||
mergeGlobalConfig(next);
|
||||
this.config = next;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "announce": {
|
||||
@@ -431,6 +444,25 @@ export default {
|
||||
this.isLoadingMore = false;
|
||||
}
|
||||
},
|
||||
async loadConversationPins() {
|
||||
try {
|
||||
const response = await window.api.get("/api/v1/lxmf/conversation-pins");
|
||||
this.pinnedPeerHashes = response.data.peer_hashes || [];
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
async onToggleConversationPin(destinationHash) {
|
||||
try {
|
||||
const response = await window.api.post("/api/v1/lxmf/conversation-pins/toggle", {
|
||||
destination_hash: destinationHash,
|
||||
});
|
||||
this.pinnedPeerHashes = response.data.peer_hashes || [];
|
||||
} catch (e) {
|
||||
ToastUtils.error(this.$t("messages.failed_toggle_pin"));
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
peerHashFromMessage(msg) {
|
||||
return msg.is_incoming ? msg.source_hash : msg.destination_hash;
|
||||
},
|
||||
|
||||
@@ -303,6 +303,7 @@
|
||||
GlobalState.config.banished_effect_enabled && isBlocked(conversation.destination_hash),
|
||||
selectionMode,
|
||||
selectedHashes.has(conversation.destination_hash),
|
||||
pinnedSet.has(conversation.destination_hash),
|
||||
timeAgoTick,
|
||||
]"
|
||||
class="flex cursor-pointer p-2 border-l-2 relative group conversation-item"
|
||||
@@ -391,6 +392,13 @@
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-between ml-1 py-1 shrink-0">
|
||||
<div class="flex items-center space-x-1">
|
||||
<div
|
||||
v-if="pinnedSet.has(conversation.destination_hash)"
|
||||
class="text-blue-500 dark:text-blue-400"
|
||||
title="Pinned"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="pin" class="w-4 h-4" />
|
||||
</div>
|
||||
<div v-if="conversation.has_attachments" class="text-gray-500 dark:text-gray-300">
|
||||
<MaterialDesignIcon icon-name="paperclip" class="w-4 h-4" />
|
||||
</div>
|
||||
@@ -432,6 +440,22 @@
|
||||
<MaterialDesignIcon icon-name="email-open-outline" class="size-4 text-gray-400" />
|
||||
<span class="font-medium">Mark as Read</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="contextMenu.targetHash"
|
||||
type="button"
|
||||
class="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 dark:text-zinc-300 hover:bg-gray-100 dark:hover:bg-zinc-700 transition-all active:scale-95"
|
||||
@click="togglePinFromContextMenu"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
:icon-name="isContextTargetPinned ? 'pin-off' : 'pin'"
|
||||
class="size-4 text-gray-400"
|
||||
/>
|
||||
<span class="font-medium">{{
|
||||
isContextTargetPinned
|
||||
? $t("messages.unpin_conversation")
|
||||
: $t("messages.pin_conversation")
|
||||
}}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 dark:text-zinc-300 hover:bg-gray-100 dark:hover:bg-zinc-700 transition-all active:scale-95"
|
||||
@@ -751,6 +775,10 @@ export default {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
pinnedPeerHashes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
"conversation-click",
|
||||
@@ -770,6 +798,7 @@ export default {
|
||||
"bulk-delete",
|
||||
"export-folders",
|
||||
"import-folders",
|
||||
"toggle-conversation-pin",
|
||||
],
|
||||
data() {
|
||||
let foldersExpanded = true;
|
||||
@@ -816,8 +845,25 @@ export default {
|
||||
blockedDestinations() {
|
||||
return GlobalState.blockedDestinations;
|
||||
},
|
||||
pinnedSet() {
|
||||
return new Set(this.pinnedPeerHashes || []);
|
||||
},
|
||||
isContextTargetPinned() {
|
||||
return Boolean(this.contextMenu.targetHash && this.pinnedSet.has(this.contextMenu.targetHash));
|
||||
},
|
||||
displayedConversations() {
|
||||
return this.conversations;
|
||||
const list = [...this.conversations];
|
||||
const pinned = this.pinnedSet;
|
||||
const idx = new Map(list.map((c, i) => [c.destination_hash, i]));
|
||||
list.sort((a, b) => {
|
||||
const ap = pinned.has(a.destination_hash);
|
||||
const bp = pinned.has(b.destination_hash);
|
||||
if (ap !== bp) {
|
||||
return ap ? -1 : 1;
|
||||
}
|
||||
return idx.get(a.destination_hash) - idx.get(b.destination_hash);
|
||||
});
|
||||
return list;
|
||||
},
|
||||
peersCount() {
|
||||
return Object.keys(this.peers).length;
|
||||
@@ -845,7 +891,10 @@ export default {
|
||||
return this.conversations.some((c) => c.is_unread);
|
||||
},
|
||||
allSelected() {
|
||||
return this.conversations.length > 0 && this.selectedHashes.size === this.conversations.length;
|
||||
return (
|
||||
this.displayedConversations.length > 0 &&
|
||||
this.selectedHashes.size === this.displayedConversations.length
|
||||
);
|
||||
},
|
||||
messageIconStyle() {
|
||||
const size = GlobalState.config?.message_icon_size || 28;
|
||||
@@ -906,7 +955,7 @@ export default {
|
||||
if (this.allSelected) {
|
||||
this.selectedHashes.clear();
|
||||
} else {
|
||||
this.conversations.forEach((c) => this.selectedHashes.add(c.destination_hash));
|
||||
this.displayedConversations.forEach((c) => this.selectedHashes.add(c.destination_hash));
|
||||
}
|
||||
},
|
||||
toggleSelectConversation(hash) {
|
||||
@@ -930,6 +979,14 @@ export default {
|
||||
// fetch contact info for trust status
|
||||
await this.fetchContactForContextMenu(hash);
|
||||
},
|
||||
togglePinFromContextMenu() {
|
||||
const h = this.contextMenu.targetHash;
|
||||
if (!h) {
|
||||
return;
|
||||
}
|
||||
this.$emit("toggle-conversation-pin", h);
|
||||
this.contextMenu.show = false;
|
||||
},
|
||||
onFolderContextMenu(event) {
|
||||
event.preventDefault();
|
||||
// Show folder management menu
|
||||
|
||||
@@ -1,118 +1,142 @@
|
||||
<template>
|
||||
<div class="relative inline-flex items-stretch rounded-xl shadow-sm">
|
||||
<!-- send button -->
|
||||
<button
|
||||
:disabled="!canSendMessage"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-l-xl px-4 py-2.5 text-sm font-semibold text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||
:class="[
|
||||
canSendMessage
|
||||
? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500'
|
||||
: 'bg-gray-400 dark:bg-zinc-500 focus-visible:outline-gray-500 cursor-not-allowed',
|
||||
]"
|
||||
@click="send"
|
||||
>
|
||||
<svg
|
||||
v-if="!isSendingMessage"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
|
||||
/>
|
||||
</svg>
|
||||
<span v-if="isSendingMessage" class="flex items-center gap-2">
|
||||
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Sending...
|
||||
</span>
|
||||
<span v-else>
|
||||
<span v-if="deliveryMethod === 'direct'">Send (Direct)</span>
|
||||
<span v-else-if="deliveryMethod === 'opportunistic'">Send (Opportunistic)</span>
|
||||
<span v-else-if="deliveryMethod === 'propagated'">Send (Propagated)</span>
|
||||
<span v-else>Send</span>
|
||||
</span>
|
||||
</button>
|
||||
<div class="relative self-stretch">
|
||||
<!-- dropdown button -->
|
||||
<template v-if="compact">
|
||||
<button
|
||||
:disabled="!canSendMessage"
|
||||
type="button"
|
||||
class="border-l relative inline-flex items-center justify-center rounded-r-xl px-2.5 h-full text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||
class="inline-flex items-center justify-center rounded-xl p-2.5 min-h-[44px] min-w-[44px] text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 touch-manipulation select-none"
|
||||
:class="[
|
||||
canSendMessage
|
||||
? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500 border-blue-700 dark:border-blue-800'
|
||||
: 'bg-gray-400 dark:bg-zinc-500 focus-visible:outline-gray-500 border-gray-500 dark:border-zinc-600 cursor-not-allowed',
|
||||
? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500'
|
||||
: 'bg-gray-400 dark:bg-zinc-500 focus-visible:outline-gray-500 cursor-not-allowed',
|
||||
]"
|
||||
@click="showMenu"
|
||||
:title="compactTitle"
|
||||
@pointerdown="onCompactPointerDown"
|
||||
@pointerup="onCompactPointerUp"
|
||||
@pointercancel="onCompactPointerCancel"
|
||||
@click="onCompactClick"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<svg
|
||||
v-if="!isSendingMessage"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
|
||||
clip-rule="evenodd"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
|
||||
/>
|
||||
</svg>
|
||||
<span v-else class="text-xs font-semibold opacity-90">...</span>
|
||||
</button>
|
||||
<!-- dropdown menu -->
|
||||
<Transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
:disabled="!canSendMessage"
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-l-xl px-4 py-2.5 text-sm font-semibold text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||
:class="[
|
||||
canSendMessage
|
||||
? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500'
|
||||
: 'bg-gray-400 dark:bg-zinc-500 focus-visible:outline-gray-500 cursor-not-allowed',
|
||||
]"
|
||||
:title="isSendingMessage ? sendingTooltip : ''"
|
||||
@click="send"
|
||||
>
|
||||
<div
|
||||
v-if="isShowingMenu"
|
||||
v-click-outside="hideMenu"
|
||||
class="absolute bottom-full right-0 mb-1 z-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-none overflow-hidden min-w-[200px]"
|
||||
<svg
|
||||
v-if="!isSendingMessage"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<div class="py-1">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap border-b border-gray-100 dark:border-zinc-800"
|
||||
@click="setDeliveryMethod(null)"
|
||||
>
|
||||
Send Automatically
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap"
|
||||
@click="setDeliveryMethod('direct')"
|
||||
>
|
||||
Send over Direct Link
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap"
|
||||
@click="setDeliveryMethod('opportunistic')"
|
||||
>
|
||||
Send Opportunistically
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap"
|
||||
@click="setDeliveryMethod('propagated')"
|
||||
>
|
||||
Send to Propagation Node
|
||||
</button>
|
||||
</div>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"
|
||||
/>
|
||||
</svg>
|
||||
<span :class="isSendingMessage ? 'opacity-60' : ''">
|
||||
<span v-if="deliveryMethod === 'direct'">Send (Direct)</span>
|
||||
<span v-else-if="deliveryMethod === 'opportunistic'">Send (Opportunistic)</span>
|
||||
<span v-else-if="deliveryMethod === 'propagated'">Send (Propagated)</span>
|
||||
<span v-else>Send</span>
|
||||
</span>
|
||||
</button>
|
||||
<div class="relative self-stretch">
|
||||
<button
|
||||
:disabled="!canSendMessage"
|
||||
type="button"
|
||||
class="border-l relative inline-flex items-center justify-center rounded-r-xl px-2.5 h-full text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||
:class="[
|
||||
canSendMessage
|
||||
? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500 border-blue-700 dark:border-blue-800'
|
||||
: 'bg-gray-400 dark:bg-zinc-500 focus-visible:outline-gray-500 border-gray-500 dark:border-zinc-600 cursor-not-allowed',
|
||||
]"
|
||||
@click="showMenu"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-if="isShowingMenu"
|
||||
v-click-outside="hideMenu"
|
||||
class="absolute bottom-full right-0 mb-1 z-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-none overflow-hidden min-w-[200px]"
|
||||
>
|
||||
<div class="py-1">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap border-b border-gray-100 dark:border-zinc-800"
|
||||
@click="setDeliveryMethod(null)"
|
||||
>
|
||||
Send Automatically
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap"
|
||||
@click="setDeliveryMethod('direct')"
|
||||
>
|
||||
Send over Direct Link
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap"
|
||||
@click="setDeliveryMethod('opportunistic')"
|
||||
>
|
||||
Send Opportunistically
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full block text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 whitespace-nowrap"
|
||||
@click="setDeliveryMethod('propagated')"
|
||||
>
|
||||
Send to Propagation Node
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -126,14 +150,74 @@ export default {
|
||||
},
|
||||
canSendMessage: Boolean,
|
||||
isSendingMessage: Boolean,
|
||||
compact: Boolean,
|
||||
sendingTooltip: {
|
||||
type: String,
|
||||
default:
|
||||
"Resolving route to peer (finding path). This can take a while on first contact or after links change. Paths are remembered until they expire.",
|
||||
},
|
||||
},
|
||||
emits: ["delivery-method-changed", "send"],
|
||||
data() {
|
||||
return {
|
||||
isShowingMenu: false,
|
||||
compactLongPressTimer: null,
|
||||
compactTapArmed: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
compactTitle() {
|
||||
if (this.isSendingMessage) {
|
||||
return this.sendingTooltip;
|
||||
}
|
||||
return "Send (hold for delivery options)";
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.clearCompactLongPressTimer();
|
||||
},
|
||||
methods: {
|
||||
clearCompactLongPressTimer() {
|
||||
if (this.compactLongPressTimer != null) {
|
||||
clearTimeout(this.compactLongPressTimer);
|
||||
this.compactLongPressTimer = null;
|
||||
}
|
||||
},
|
||||
onCompactPointerDown() {
|
||||
if (!this.compact || !this.canSendMessage) {
|
||||
return;
|
||||
}
|
||||
this.compactTapArmed = true;
|
||||
this.clearCompactLongPressTimer();
|
||||
this.compactLongPressTimer = window.setTimeout(() => {
|
||||
this.compactLongPressTimer = null;
|
||||
this.compactTapArmed = false;
|
||||
this.showMenu();
|
||||
}, 500);
|
||||
},
|
||||
onCompactPointerUp() {
|
||||
if (!this.compact) {
|
||||
return;
|
||||
}
|
||||
this.clearCompactLongPressTimer();
|
||||
},
|
||||
onCompactPointerCancel() {
|
||||
if (!this.compact) {
|
||||
return;
|
||||
}
|
||||
this.clearCompactLongPressTimer();
|
||||
this.compactTapArmed = false;
|
||||
},
|
||||
onCompactClick() {
|
||||
if (!this.compact) {
|
||||
return;
|
||||
}
|
||||
if (!this.compactTapArmed) {
|
||||
return;
|
||||
}
|
||||
this.compactTapArmed = false;
|
||||
this.send();
|
||||
},
|
||||
showMenu() {
|
||||
this.isShowingMenu = true;
|
||||
},
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
</div>
|
||||
|
||||
<!-- header -->
|
||||
<div class="flex p-2 border-b border-gray-300 dark:border-zinc-800">
|
||||
<div class="flex items-center gap-1 p-2 border-b border-gray-300 dark:border-zinc-800 min-w-0">
|
||||
<!-- favourite button -->
|
||||
<div class="my-auto mr-2">
|
||||
<div class="my-auto shrink-0 mr-1">
|
||||
<IconButton
|
||||
v-if="isFavourite(selectedNode.destination_hash)"
|
||||
class="text-yellow-500 dark:text-yellow-300"
|
||||
@@ -67,15 +67,15 @@
|
||||
</div>
|
||||
|
||||
<!-- node info -->
|
||||
<div class="my-auto dark:text-gray-100 flex-1 min-w-0 flex items-baseline gap-1">
|
||||
<div class="my-auto dark:text-gray-100 flex-1 min-w-0 flex items-baseline gap-1 overflow-hidden">
|
||||
<span
|
||||
class="font-semibold truncate inline-block max-w-xs sm:max-w-sm flex-shrink"
|
||||
class="font-semibold truncate inline-block min-w-0 max-w-[min(100%,12rem)] sm:max-w-xs md:max-w-sm"
|
||||
:title="selectedNode.display_name"
|
||||
>{{ selectedNode.display_name }}</span
|
||||
>
|
||||
<span
|
||||
v-if="selectedNodePath"
|
||||
class="text-sm cursor-pointer whitespace-nowrap flex-shrink-0"
|
||||
class="text-sm cursor-pointer whitespace-nowrap flex-shrink-0 hidden sm:inline"
|
||||
@click="onDestinationPathClick(selectedNodePath)"
|
||||
>
|
||||
- {{ selectedNodePath.hops }}
|
||||
@@ -89,19 +89,8 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- identify button -->
|
||||
<div class="my-auto ml-auto mr-2">
|
||||
<IconButton
|
||||
class="text-gray-700 dark:text-gray-300"
|
||||
title="Identify"
|
||||
@click="identify(selectedNode.destination_hash)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="fingerprint" class="size-5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<!-- archive button -->
|
||||
<div v-if="pageArchives.length > 0 || nodePageContent" class="my-auto mr-2 relative">
|
||||
<div v-if="pageArchives.length > 0 || nodePageContent" class="my-auto shrink-0 relative">
|
||||
<IconButton
|
||||
class="text-gray-700 dark:text-gray-300"
|
||||
:class="{ 'text-blue-500 dark:text-blue-400': pageArchives.length > 0 }"
|
||||
@@ -153,19 +142,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- popout button -->
|
||||
<div class="my-auto mr-2">
|
||||
<div class="hidden md:flex items-center gap-1 shrink-0">
|
||||
<IconButton
|
||||
class="text-gray-700 dark:text-gray-300"
|
||||
:title="$t('messages.pop_out_chat')"
|
||||
:title="$t('nomadnet.identify')"
|
||||
@click="identify(selectedNode.destination_hash)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="fingerprint" class="size-5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
class="text-gray-700 dark:text-gray-300"
|
||||
:title="$t('nomadnet.pop_out_browser')"
|
||||
@click="openNomadnetPopout"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="open-in-new" class="size-5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<!-- close button -->
|
||||
<div class="my-auto mr-2">
|
||||
<IconButton
|
||||
class="text-gray-700 dark:text-gray-300"
|
||||
:title="$t('common.cancel')"
|
||||
@@ -174,27 +165,61 @@
|
||||
<MaterialDesignIcon icon-name="close" class="w-5 h-5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<DropDownMenu class="md:hidden shrink-0">
|
||||
<template #button>
|
||||
<IconButton :title="$t('messages.more_actions')" class="text-gray-700 dark:text-gray-300">
|
||||
<MaterialDesignIcon icon-name="dots-horizontal" class="size-5" />
|
||||
</IconButton>
|
||||
</template>
|
||||
<template #items>
|
||||
<DropDownMenuItem @click="identify(selectedNode.destination_hash)">
|
||||
<MaterialDesignIcon icon-name="fingerprint" class="size-5" />
|
||||
<span>{{ $t("nomadnet.identify") }}</span>
|
||||
</DropDownMenuItem>
|
||||
<DropDownMenuItem @click="openNomadnetPopout">
|
||||
<MaterialDesignIcon icon-name="open-in-new" class="size-5" />
|
||||
<span>{{ $t("nomadnet.pop_out_browser") }}</span>
|
||||
</DropDownMenuItem>
|
||||
<DropDownMenuItem @click="onCloseNodeViewer">
|
||||
<MaterialDesignIcon icon-name="close" class="size-5" />
|
||||
<span>{{ $t("common.cancel") }}</span>
|
||||
</DropDownMenuItem>
|
||||
</template>
|
||||
</DropDownMenu>
|
||||
</div>
|
||||
|
||||
<!-- browser navigation -->
|
||||
<div class="flex items-center w-full border-gray-300 dark:border-zinc-800 border-b p-2 gap-1">
|
||||
<IconButton title="Home" @click="loadNodePage(selectedNode.destination_hash, defaultNodePagePath)">
|
||||
<div
|
||||
class="flex items-center w-full min-w-0 border-gray-300 dark:border-zinc-800 border-b p-2 gap-0.5 overflow-x-auto"
|
||||
>
|
||||
<IconButton
|
||||
class="shrink-0"
|
||||
title="Home"
|
||||
@click="loadNodePage(selectedNode.destination_hash, defaultNodePagePath)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="home" class="w-5 h-5" />
|
||||
</IconButton>
|
||||
<IconButton :title="$t('common.refresh')" @click="reloadNodePage">
|
||||
<IconButton class="shrink-0" :title="$t('common.refresh')" @click="reloadNodePage">
|
||||
<MaterialDesignIcon icon-name="refresh" class="w-5 h-5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
class="shrink-0"
|
||||
:title="$t('app.toggle_source')"
|
||||
:class="{ 'bg-green-500/10 text-green-600 dark:text-green-400': isShowingNodePageSource }"
|
||||
@click="toggleNodePageSource"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="code-tags" class="size-5" />
|
||||
</IconButton>
|
||||
<IconButton title="Back" :disabled="nodePagePathHistory.length === 0" @click="loadPreviousNodePage">
|
||||
<IconButton
|
||||
class="shrink-0"
|
||||
title="Back"
|
||||
:disabled="nodePagePathHistory.length === 0"
|
||||
@click="loadPreviousNodePage"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="arrow-left" class="w-5 h-5" />
|
||||
</IconButton>
|
||||
<div class="my-auto mx-1 w-full">
|
||||
<div class="my-auto mx-1 min-w-0 flex-1">
|
||||
<input
|
||||
v-model="nodePagePathUrlInput"
|
||||
type="text"
|
||||
@@ -203,7 +228,7 @@
|
||||
@keyup.enter="onNodePageUrlClick(nodePagePathUrlInput)"
|
||||
/>
|
||||
</div>
|
||||
<IconButton title="Go" @click="onNodePageUrlClick(nodePagePathUrlInput)">
|
||||
<IconButton class="shrink-0" title="Go" @click="onNodePageUrlClick(nodePagePathUrlInput)">
|
||||
<MaterialDesignIcon icon-name="arrow-right" class="w-5 h-5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -362,6 +387,8 @@ import Utils from "../../js/Utils";
|
||||
import ToastUtils from "../../js/ToastUtils";
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import IconButton from "../IconButton.vue";
|
||||
import DropDownMenu from "../DropDownMenu.vue";
|
||||
import DropDownMenuItem from "../DropDownMenuItem.vue";
|
||||
import GlobalState from "../../js/GlobalState";
|
||||
|
||||
export default {
|
||||
@@ -370,6 +397,8 @@ export default {
|
||||
NomadNetworkSidebar,
|
||||
MaterialDesignIcon,
|
||||
IconButton,
|
||||
DropDownMenu,
|
||||
DropDownMenuItem,
|
||||
},
|
||||
props: {
|
||||
destinationHash: {
|
||||
|
||||
@@ -212,8 +212,8 @@ import LxmfUserIcon from "../LxmfUserIcon.vue";
|
||||
import ToastUtils from "../../js/ToastUtils";
|
||||
import ColourPickerDropdown from "../ColourPickerDropdown.vue";
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import GlobalState from "../../js/GlobalState";
|
||||
import GlobalEmitter from "../../js/GlobalEmitter";
|
||||
import { mergeGlobalConfig } from "../../js/GlobalState";
|
||||
|
||||
export default {
|
||||
name: "ProfileIconPage",
|
||||
@@ -316,7 +316,11 @@ export default {
|
||||
async getConfig() {
|
||||
try {
|
||||
const response = await window.api.get("/api/v1/config");
|
||||
this.config = response.data.config;
|
||||
const next = response.data?.config;
|
||||
if (next && typeof next === "object") {
|
||||
this.config = next;
|
||||
mergeGlobalConfig(next);
|
||||
}
|
||||
} catch (e) {
|
||||
ToastUtils.error(this.$t("messages.failed_load_config"));
|
||||
console.error(e);
|
||||
@@ -325,9 +329,13 @@ export default {
|
||||
async updateConfig(config, silent = false) {
|
||||
try {
|
||||
const response = await window.api.patch("/api/v1/config", config);
|
||||
this.config = response.data.config;
|
||||
GlobalState.config = response.data.config;
|
||||
GlobalEmitter.emit("config-updated", response.data.config);
|
||||
const next = response.data?.config;
|
||||
if (!next || typeof next !== "object") {
|
||||
return false;
|
||||
}
|
||||
mergeGlobalConfig(next);
|
||||
this.config = next;
|
||||
GlobalEmitter.emit("config-updated", next);
|
||||
this.saveOriginalValues();
|
||||
|
||||
if (!silent) {
|
||||
|
||||
@@ -768,6 +768,26 @@
|
||||
Message Bubbles
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-start gap-3 rounded-xl border border-gray-200 dark:border-zinc-700 px-3 py-2.5"
|
||||
>
|
||||
<input
|
||||
id="detailed-outbound-send-status"
|
||||
type="checkbox"
|
||||
class="mt-1 rounded border-gray-300 dark:border-zinc-600"
|
||||
:checked="GlobalState.detailedOutboundSendStatus"
|
||||
@change="onDetailedOutboundSendStatusChange"
|
||||
/>
|
||||
<label for="detailed-outbound-send-status" class="min-w-0 cursor-pointer">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $t("app.detailed_outbound_send_status") }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-zinc-400 mt-0.5">
|
||||
{{ $t("app.detailed_outbound_send_status_description") }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
@@ -1796,6 +1816,7 @@ import ShortcutRecorder from "./ShortcutRecorder.vue";
|
||||
import KeyboardShortcuts from "../../js/KeyboardShortcuts";
|
||||
import ElectronUtils from "../../js/ElectronUtils";
|
||||
import LxmfUserIcon from "../LxmfUserIcon.vue";
|
||||
import GlobalState from "../../js/GlobalState";
|
||||
|
||||
export default {
|
||||
name: "SettingsPage",
|
||||
@@ -1807,6 +1828,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
GlobalState,
|
||||
ElectronUtils,
|
||||
KeyboardShortcuts,
|
||||
config: {
|
||||
@@ -2250,6 +2272,15 @@ export default {
|
||||
);
|
||||
}, 1000);
|
||||
},
|
||||
onDetailedOutboundSendStatusChange(event) {
|
||||
const checked = event.target.checked;
|
||||
GlobalState.detailedOutboundSendStatus = checked;
|
||||
try {
|
||||
localStorage.setItem("meshchatx_detailed_outbound_send_status", checked ? "true" : "false");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
async onLanguageChange() {
|
||||
await this.updateConfig(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user