feat(icons): update icon sizes across various components for improved consistency and visual clarity; adjust padding and styles in documentation and settings pages
This commit is contained in:
@@ -315,6 +315,7 @@
|
||||
:icon-name="config?.lxmf_user_icon_name"
|
||||
:icon-foreground-colour="config?.lxmf_user_icon_foreground_colour"
|
||||
:icon-background-colour="config?.lxmf_user_icon_background_colour"
|
||||
icon-class="size-7"
|
||||
/>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="iconName"
|
||||
class="p-[15%] rounded-full shrink-0 flex items-center justify-center"
|
||||
class="p-[10%] rounded-full shrink-0 flex items-center justify-center"
|
||||
:style="{ 'background-color': finalBackgroundColor }"
|
||||
:class="iconClass || 'size-6'"
|
||||
>
|
||||
<MaterialDesignIcon :icon-name="iconName" class="w-full h-full" :style="{ color: finalForegroundColor }" />
|
||||
<MaterialDesignIcon :icon-name="iconName" class="size-full" :style="{ color: finalForegroundColor }" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
fill="currentColor"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style="display: inline-block; vertical-align: middle; shape-rendering: inherit"
|
||||
style="display: block"
|
||||
class="antialiased"
|
||||
>
|
||||
<path :d="iconPath" />
|
||||
<path :d="iconPath" fill="currentColor" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<!-- icon and name -->
|
||||
<div class="flex flex-col items-center mb-4">
|
||||
<div
|
||||
class="p-4 rounded-full mb-3"
|
||||
class="p-2 rounded-full mb-3"
|
||||
:class="
|
||||
isEnded || wasDeclined ? 'bg-red-100 dark:bg-red-900/30' : 'bg-blue-100 dark:bg-blue-900/30'
|
||||
"
|
||||
@@ -69,7 +69,7 @@
|
||||
:icon-name="activeCall.remote_icon ? activeCall.remote_icon.icon_name : ''"
|
||||
:icon-foreground-colour="activeCall.remote_icon ? activeCall.remote_icon.foreground_colour : ''"
|
||||
:icon-background-colour="activeCall.remote_icon ? activeCall.remote_icon.background_colour : ''"
|
||||
icon-class="size-8"
|
||||
icon-class="size-14"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-center w-full min-w-0">
|
||||
@@ -246,7 +246,7 @@
|
||||
:icon-name="activeCall.remote_icon ? activeCall.remote_icon.icon_name : ''"
|
||||
:icon-foreground-colour="activeCall.remote_icon ? activeCall.remote_icon.foreground_colour : ''"
|
||||
:icon-background-colour="activeCall.remote_icon ? activeCall.remote_icon.background_colour : ''"
|
||||
icon-class="size-5 shrink-0"
|
||||
icon-class="size-6 shrink-0"
|
||||
/>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-zinc-200 truncate block">
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
? (activeCall || lastCall).remote_icon.background_colour
|
||||
: ''
|
||||
"
|
||||
icon-class="size-20"
|
||||
icon-class="size-28"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,46 +3,98 @@
|
||||
<div class="flex flex-col h-full bg-slate-50 dark:bg-zinc-950 overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="p-3 border-b border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 flex items-center justify-between z-30"
|
||||
class="p-2 md:p-3 border-b border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 flex items-center gap-4 z-30"
|
||||
>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<!-- Title Section -->
|
||||
<div class="flex items-center space-x-3 shrink-0">
|
||||
<div class="hidden md:block p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<MaterialDesignIcon
|
||||
icon-name="book-open-variant"
|
||||
class="w-5 h-5 text-blue-600 dark:text-blue-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-sm font-bold text-gray-900 dark:text-zinc-100">{{ $t("docs.title") }}</h1>
|
||||
<div class="shrink-0">
|
||||
<h1 class="text-xs md:text-sm font-bold text-gray-900 dark:text-zinc-100">
|
||||
{{ $t("docs.title") }}
|
||||
</h1>
|
||||
<div
|
||||
v-if="status.has_docs || status.has_meshchatx_docs"
|
||||
class="flex items-center text-[10px] text-gray-500"
|
||||
class="hidden md:flex items-center text-[10px] text-gray-500"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full bg-green-500 mr-1.5"></span>
|
||||
Offline Ready
|
||||
Ready
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Export Docs Button -->
|
||||
<button
|
||||
v-if="status.has_docs || status.has_meshchatx_docs"
|
||||
class="p-2 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
|
||||
title="Export all documentation as ZIP"
|
||||
@click="exportDocs"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="download" class="w-5 h-5" />
|
||||
</button>
|
||||
<!-- Search & Navigation (Desktop) -->
|
||||
<div class="hidden lg:flex flex-1 items-center gap-4 max-w-3xl">
|
||||
<!-- Tabs -->
|
||||
<div class="flex bg-gray-100 dark:bg-zinc-800 p-0.5 rounded-lg shrink-0">
|
||||
<button
|
||||
class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
|
||||
:class="
|
||||
activeTab === 'meshchatx'
|
||||
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300'
|
||||
"
|
||||
@click="activeTab = 'meshchatx'"
|
||||
>
|
||||
MeshChatX
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
|
||||
:class="
|
||||
activeTab === 'reticulum'
|
||||
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300'
|
||||
"
|
||||
@click="activeTab = 'reticulum'"
|
||||
>
|
||||
Reticulum
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'reticulum' && otherLanguages.length > 0 && status.has_docs" class="relative">
|
||||
<!-- Search Input -->
|
||||
<div v-if="status.has_docs || status.has_meshchatx_docs" class="relative flex-1">
|
||||
<div class="absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none">
|
||||
<MaterialDesignIcon icon-name="magnify" class="h-3.5 w-3.5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="block w-full pl-8 pr-8 py-1.5 border border-gray-200 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 text-[11px] focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
||||
placeholder="Search documentation..."
|
||||
@input="debounceSearch"
|
||||
/>
|
||||
<div v-if="isSearching" class="absolute inset-y-0 right-0 pr-2.5 flex items-center">
|
||||
<MaterialDesignIcon icon-name="loading" class="h-3 w-3 text-gray-400 animate-spin" />
|
||||
</div>
|
||||
<button
|
||||
v-else-if="searchQuery"
|
||||
class="absolute inset-y-0 right-0 pr-2.5 flex items-center"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
icon-name="close"
|
||||
class="h-3 w-3 text-gray-400 hover:text-gray-600 dark:hover:text-zinc-200 cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Section -->
|
||||
<div class="flex items-center space-x-1 md:space-x-2 ml-auto shrink-0">
|
||||
<!-- Language Selector -->
|
||||
<div v-if="activeTab === 'reticulum' && status.has_docs" class="relative">
|
||||
<button
|
||||
v-click-outside="() => (showLanguages = false)"
|
||||
class="p-2 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
|
||||
class="p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors flex items-center gap-1.5"
|
||||
:class="{ 'bg-gray-100 dark:bg-zinc-800': showLanguages }"
|
||||
@click="showLanguages = !showLanguages"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="translate" class="w-5 h-5" />
|
||||
<MaterialDesignIcon icon-name="translate" class="w-4 h-4 md:w-5 md:h-5" />
|
||||
<span class="hidden xl:inline text-[10px] font-bold uppercase">{{ currentLang }}</span>
|
||||
</button>
|
||||
<div
|
||||
v-if="showLanguages"
|
||||
@@ -60,9 +112,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Button -->
|
||||
<button
|
||||
v-if="status.has_docs || status.has_meshchatx_docs"
|
||||
class="p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
|
||||
title="Export all documentation as ZIP"
|
||||
@click="exportDocs"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="download" class="w-4 h-4 md:w-5 md:h-5" />
|
||||
</button>
|
||||
|
||||
<!-- Update Button -->
|
||||
<button
|
||||
:disabled="status.status === 'downloading' || status.status === 'extracting'"
|
||||
class="p-2 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors disabled:opacity-50"
|
||||
class="p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors disabled:opacity-50"
|
||||
:title="status.has_docs ? $t('docs.btn_update') : $t('docs.btn_download')"
|
||||
@click="updateDocs"
|
||||
>
|
||||
@@ -71,32 +134,33 @@
|
||||
status.status === 'downloading' || status.status === 'extracting' ? 'loading' : 'refresh'
|
||||
"
|
||||
:class="{ 'animate-spin': status.status === 'downloading' || status.status === 'extracting' }"
|
||||
class="w-5 h-5"
|
||||
class="w-4 h-4 md:w-5 md:h-5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- Open External -->
|
||||
<a
|
||||
v-if="status.has_docs"
|
||||
:href="localDocsUrl"
|
||||
target="_blank"
|
||||
class="flex items-center px-3 py-1.5 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 rounded-lg hover:opacity-90 transition-opacity font-bold text-xs shadow-sm"
|
||||
class="hidden sm:flex items-center px-2.5 py-1.5 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 rounded-lg hover:opacity-90 transition-opacity font-bold text-[10px] shadow-sm"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="open-in-new" class="w-3.5 h-3.5 mr-1.5" />
|
||||
<MaterialDesignIcon icon-name="open-in-new" class="w-3 h-3 mr-1.5" />
|
||||
Open
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<!-- Secondary Navigation (Mobile/Tablet) -->
|
||||
<div
|
||||
v-if="status.has_docs || status.has_meshchatx_docs"
|
||||
class="px-4 py-2 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 z-10"
|
||||
v-if="(status.has_docs || status.has_meshchatx_docs) && !isSearching"
|
||||
class="lg:hidden px-3 py-2 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 z-10"
|
||||
>
|
||||
<div class="flex flex-col md:flex-row items-center gap-4 max-w-4xl mx-auto w-full">
|
||||
<div class="flex flex-col md:flex-row items-center gap-2 w-full">
|
||||
<!-- Tabs -->
|
||||
<div class="flex bg-gray-100 dark:bg-zinc-800 p-1 rounded-xl shrink-0 w-full md:w-auto">
|
||||
<div class="flex bg-gray-100 dark:bg-zinc-800 p-0.5 rounded-lg w-full md:w-auto">
|
||||
<button
|
||||
class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-lg transition-all"
|
||||
class="flex-1 md:flex-none px-4 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
|
||||
:class="
|
||||
activeTab === 'meshchatx'
|
||||
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
|
||||
@@ -107,7 +171,7 @@
|
||||
MeshChatX
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-lg transition-all"
|
||||
class="flex-1 md:flex-none px-4 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
|
||||
:class="
|
||||
activeTab === 'reticulum'
|
||||
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
|
||||
@@ -120,14 +184,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Search Input -->
|
||||
<div class="relative flex-1 w-full">
|
||||
<div class="relative w-full">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<MaterialDesignIcon icon-name="magnify" class="h-4 w-4 text-gray-400" />
|
||||
<MaterialDesignIcon icon-name="magnify" class="h-3.5 w-3.5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="block w-full pl-10 pr-10 py-1.5 border border-gray-200 dark:border-zinc-700 rounded-xl bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
||||
class="block w-full pl-9 pr-9 py-2 border border-gray-200 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
|
||||
placeholder="Search all documentation..."
|
||||
@input="debounceSearch"
|
||||
/>
|
||||
@@ -325,27 +389,6 @@
|
||||
|
||||
<div v-if="selectedDocContent" class="flex-1 overflow-y-auto p-6 md:p-10 scroll-smooth">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<!-- Share Actions -->
|
||||
<div
|
||||
class="flex items-center justify-between mb-8 pb-4 border-b border-gray-100 dark:border-zinc-800"
|
||||
>
|
||||
<div class="flex items-center space-x-2 text-gray-400">
|
||||
<MaterialDesignIcon icon-name="clock-outline" class="w-3 h-3" />
|
||||
<span class="text-[10px] font-mono uppercase tracking-tighter"
|
||||
>Ready for sharing</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
class="flex items-center space-x-2 px-3 py-1.5 bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 rounded-lg text-[10px] font-bold text-gray-600 dark:text-zinc-300 transition-colors"
|
||||
@click="copyDocLink"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="share-variant" class="w-3.5 h-3.5" />
|
||||
<span>Share Link</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-none break-words" v-html="selectedDocContent.html"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -476,10 +519,16 @@ export default {
|
||||
try {
|
||||
const response = await window.axios.get("/api/v1/docs/status");
|
||||
this.status = response.data;
|
||||
|
||||
// Auto-download Reticulum docs if missing and we're not already doing something
|
||||
if (!this.status.has_docs && this.status.status === "idle" && !this.status.last_error) {
|
||||
this.updateDocs();
|
||||
}
|
||||
|
||||
// If we don't have Reticulum docs but have MeshChatX docs, default to MeshChatX tab
|
||||
if (!this.status.has_docs && this.status.has_meshchatx_docs) {
|
||||
if (!this.status.has_docs && this.status.has_meshchatx_docs && this.activeTab === "reticulum") {
|
||||
this.activeTab = "meshchatx";
|
||||
} else if (this.status.has_docs && !this.status.has_meshchatx_docs) {
|
||||
} else if (this.status.has_docs && !this.status.has_meshchatx_docs && this.activeTab === "meshchatx") {
|
||||
this.activeTab = "reticulum";
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -620,4 +669,28 @@ export default {
|
||||
iframe {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
/* Markdown styling for the rendered HTML */
|
||||
:deep(.max-w-none) pre {
|
||||
color: #f4f4f5 !important; /* zinc-100 */
|
||||
}
|
||||
|
||||
:deep(.max-w-none) pre code {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
:deep(.max-w-none) code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.dark :deep(.max-w-none) p {
|
||||
color: #e4e4e7; /* zinc-200 */
|
||||
}
|
||||
|
||||
.dark :deep(.max-w-none) h1,
|
||||
.dark :deep(.max-w-none) h2,
|
||||
.dark :deep(.max-w-none) h3,
|
||||
.dark :deep(.max-w-none) h4 {
|
||||
color: #f4f4f5; /* zinc-100 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
<!-- telemetry marker overlay -->
|
||||
<div
|
||||
v-if="selectedMarker"
|
||||
class="absolute bottom-4 left-4 right-4 sm:left-4 sm:right-auto sm:w-80 z-20 bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden"
|
||||
class="absolute bottom-4 left-4 right-4 sm:left-4 sm:right-auto sm:w-80 z-20 bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden text-gray-900 dark:text-zinc-100"
|
||||
>
|
||||
<div class="p-4 border-b border-gray-200 dark:border-zinc-800 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -319,7 +319,7 @@
|
||||
<!-- export configuration overlay -->
|
||||
<div
|
||||
v-if="isExportMode && selectedBbox"
|
||||
class="absolute top-4 left-1/2 -translate-x-1/2 z-20 w-80 bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden"
|
||||
class="absolute top-4 left-1/2 -translate-x-1/2 z-20 w-80 bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden text-gray-900 dark:text-zinc-100"
|
||||
>
|
||||
<div class="p-4 border-b border-gray-200 dark:border-zinc-800 flex items-center justify-between">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-zinc-100">{{ $t("map.export_area") }}</h3>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
:icon-background-colour="
|
||||
selectedPeer.lxmf_user_icon ? selectedPeer.lxmf_user_icon.background_colour : ''
|
||||
"
|
||||
icon-class="size-10"
|
||||
icon-class="size-11"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -114,17 +114,17 @@
|
||||
|
||||
<!-- call button -->
|
||||
<IconButton title="Start a Call" @click="onStartCall">
|
||||
<MaterialDesignIcon icon-name="phone" class="w-5 h-5" />
|
||||
<MaterialDesignIcon icon-name="phone" class="size-6" />
|
||||
</IconButton>
|
||||
|
||||
<!-- share contact button -->
|
||||
<IconButton title="Share Contact" @click="openShareContactModal">
|
||||
<MaterialDesignIcon icon-name="notebook-outline" class="w-5 h-5" />
|
||||
<MaterialDesignIcon icon-name="notebook-outline" class="size-6" />
|
||||
</IconButton>
|
||||
|
||||
<!-- close button -->
|
||||
<IconButton title="Close" @click="close">
|
||||
<MaterialDesignIcon icon-name="close" class="size-5" />
|
||||
<MaterialDesignIcon icon-name="close" class="size-6" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,12 +243,80 @@
|
||||
<!-- content -->
|
||||
<div
|
||||
v-if="chatItem.lxmf_message.content"
|
||||
class="text-sm leading-relaxed whitespace-pre-wrap break-words"
|
||||
style="font-family: inherit"
|
||||
class="leading-relaxed whitespace-pre-wrap break-words"
|
||||
:style="{
|
||||
'font-family': 'inherit',
|
||||
'font-size': (config?.message_font_size || 14) + 'px',
|
||||
}"
|
||||
>
|
||||
{{ chatItem.lxmf_message.content }}
|
||||
</div>
|
||||
|
||||
<!-- parsed items (contacts / paper messages) -->
|
||||
<div v-if="getParsedItems(chatItem)" class="mt-2 space-y-2">
|
||||
<!-- contact -->
|
||||
<div
|
||||
v-if="getParsedItems(chatItem).contact && !chatItem.is_outbound"
|
||||
class="flex flex-col gap-2 p-3 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800/30"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-blue-700 dark:text-blue-300">
|
||||
<MaterialDesignIcon icon-name="account-plus-outline" class="size-5" />
|
||||
<span class="text-sm font-bold">Contact Shared</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="size-10 flex items-center justify-center rounded-full bg-blue-100 dark:bg-blue-800 text-blue-600 dark:text-blue-200 font-bold"
|
||||
>
|
||||
{{ getParsedItems(chatItem).contact.name.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-bold text-gray-900 dark:text-white truncate">
|
||||
{{ getParsedItems(chatItem).contact.name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-[10px] font-mono text-gray-500 dark:text-zinc-400 truncate"
|
||||
>
|
||||
{{ getParsedItems(chatItem).contact.hash }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-xs font-bold transition-colors shadow-sm"
|
||||
@click="
|
||||
addContact(
|
||||
getParsedItems(chatItem).contact.name,
|
||||
getParsedItems(chatItem).contact.hash
|
||||
)
|
||||
"
|
||||
>
|
||||
Add to Contacts
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- paper message auto-conversion -->
|
||||
<div
|
||||
v-if="getParsedItems(chatItem).paperMessage"
|
||||
class="flex flex-col gap-2 p-3 rounded-xl bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-100 dark:border-emerald-800/30"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-emerald-700 dark:text-emerald-300">
|
||||
<MaterialDesignIcon icon-name="qrcode-scan" class="size-5" />
|
||||
<span class="text-sm font-bold">Paper Message detected</span>
|
||||
</div>
|
||||
<p class="text-xs text-emerald-600/80 dark:text-emerald-400/80 leading-relaxed">
|
||||
This message contains a signed LXMF URI that can be ingested into your
|
||||
conversations.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold transition-colors shadow-sm"
|
||||
@click="ingestPaperMessage(getParsedItems(chatItem).paperMessage)"
|
||||
>
|
||||
Ingest Message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- image field -->
|
||||
<div v-if="chatItem.lxmf_message.fields?.image" class="relative group mt-1 -mx-1">
|
||||
<img
|
||||
@@ -414,15 +482,6 @@
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
||||
<!-- share as paper message -->
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-1.5 rounded-lg bg-blue-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-blue-600 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 ml-2"
|
||||
@click.stop="shareAsPaperMessage(chatItem)"
|
||||
>
|
||||
Paper Message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -765,6 +824,24 @@
|
||||
<MaterialDesignIcon icon-name="crosshairs-question" class="w-4 h-4" />
|
||||
<span class="hidden sm:inline">{{ $t("messages.request") }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="attachment-action-button"
|
||||
:title="$t('messages.generate_paper_message')"
|
||||
:disabled="!canSendMessage || isGeneratingPaperMessage"
|
||||
@click="generatePaperMessageFromComposition"
|
||||
>
|
||||
<template v-if="isGeneratingPaperMessage">
|
||||
<div
|
||||
class="size-4 border-2 border-blue-500/20 border-t-blue-500 rounded-full animate-spin"
|
||||
></div>
|
||||
<span class="hidden sm:inline">Generating...</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<MaterialDesignIcon icon-name="qrcode-plus" class="w-4 h-4" />
|
||||
<span class="hidden sm:inline">LXM</span>
|
||||
</template>
|
||||
</button>
|
||||
<button
|
||||
v-if="hasTranslator && newMessageText"
|
||||
type="button"
|
||||
@@ -871,6 +948,15 @@
|
||||
:message-hash="paperMessageHash"
|
||||
@close="isPaperMessageModalOpen = false"
|
||||
/>
|
||||
|
||||
<PaperMessageModal
|
||||
v-if="isPaperMessageResultModalOpen"
|
||||
:initial-uri="generatedPaperMessageUri"
|
||||
@close="
|
||||
isPaperMessageResultModalOpen = false;
|
||||
generatedPaperMessageUri = null;
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -967,6 +1053,9 @@ export default {
|
||||
expandedMessageInfo: null,
|
||||
imageModalUrl: null,
|
||||
isSelectedPeerBlocked: false,
|
||||
isGeneratingPaperMessage: false,
|
||||
generatedPaperMessageUri: null,
|
||||
isPaperMessageResultModalOpen: false,
|
||||
lxmfAudioModeToCodec2ModeMap: {
|
||||
// https://github.com/markqvist/LXMF/blob/master/LXMF/LXMF.py#L21
|
||||
0x01: "450PWB", // AM_CODEC2_450PWB
|
||||
@@ -1282,6 +1371,77 @@ export default {
|
||||
this.isLoadingPrevious = false;
|
||||
}
|
||||
},
|
||||
getParsedItems(chatItem) {
|
||||
const content = chatItem.lxmf_message.content;
|
||||
if (!content) return null;
|
||||
|
||||
const items = {
|
||||
contact: null,
|
||||
paperMessage: null,
|
||||
};
|
||||
|
||||
// Parse contact: Contact: ivan <ca314c30b27eacec5f6ca6ac504e94c9>
|
||||
const contactMatch = content.match(/^Contact:\s+(.+?)\s+<([a-fA-F0-9]{32})>$/i);
|
||||
if (contactMatch) {
|
||||
items.contact = {
|
||||
name: contactMatch[1],
|
||||
hash: contactMatch[2],
|
||||
};
|
||||
}
|
||||
|
||||
// Parse paper message link
|
||||
const paperMatch = content.match(/(lxm|lxmf):\/\/[a-zA-Z0-9+/=]+/i);
|
||||
if (paperMatch) {
|
||||
items.paperMessage = paperMatch[0];
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
async addContact(name, hash) {
|
||||
try {
|
||||
// Check if contact already exists
|
||||
const checkResponse = await window.axios.get(`/api/v1/telephone/contacts/check/${hash}`);
|
||||
if (checkResponse.data?.id) {
|
||||
ToastUtils.info(`${name} is already in your contacts`);
|
||||
return;
|
||||
}
|
||||
|
||||
await window.axios.post("/api/v1/telephone/contacts", {
|
||||
name: name,
|
||||
remote_identity_hash: hash,
|
||||
});
|
||||
ToastUtils.success(`Added ${name} to contacts`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to add contact");
|
||||
}
|
||||
},
|
||||
async ingestPaperMessage(uri) {
|
||||
try {
|
||||
WebSocketConnection.send(
|
||||
JSON.stringify({
|
||||
type: "lxm.ingest_uri",
|
||||
uri: uri,
|
||||
})
|
||||
);
|
||||
ToastUtils.info("Ingesting paper message...");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to ingest paper message");
|
||||
}
|
||||
},
|
||||
async generatePaperMessageFromComposition() {
|
||||
if (!this.canSendMessage) return;
|
||||
|
||||
this.isGeneratingPaperMessage = true;
|
||||
WebSocketConnection.send(
|
||||
JSON.stringify({
|
||||
type: "lxm.generate_paper_uri",
|
||||
destination_hash: this.selectedPeer.destination_hash,
|
||||
content: this.newMessageText,
|
||||
})
|
||||
);
|
||||
},
|
||||
async onWebsocketMessage(message) {
|
||||
const json = JSON.parse(message.data);
|
||||
switch (json.type) {
|
||||
@@ -1313,6 +1473,26 @@ export default {
|
||||
this.onLxmfMessageDeleted(json.hash);
|
||||
break;
|
||||
}
|
||||
case "lxm.generate_paper_uri.result": {
|
||||
this.isGeneratingPaperMessage = false;
|
||||
if (json.status === "success") {
|
||||
this.generatedPaperMessageUri = json.uri;
|
||||
this.isPaperMessageResultModalOpen = true;
|
||||
} else {
|
||||
ToastUtils.error(json.message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "lxm.ingest_uri.result": {
|
||||
if (json.status === "success") {
|
||||
ToastUtils.success(json.message);
|
||||
} else if (json.status === "error") {
|
||||
ToastUtils.error(json.message);
|
||||
} else {
|
||||
ToastUtils.warning(json.message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
openLXMFAddress() {
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
:icon-background-colour="
|
||||
conversation.lxmf_user_icon ? conversation.lxmf_user_icon.background_colour : ''
|
||||
"
|
||||
icon-class="w-6 h-6"
|
||||
icon-class="size-7"
|
||||
/>
|
||||
</div>
|
||||
<div class="mr-auto w-full pr-2 min-w-0">
|
||||
@@ -263,22 +263,12 @@
|
||||
</div>
|
||||
|
||||
<div class="my-auto mr-2">
|
||||
<div
|
||||
v-if="peer.lxmf_user_icon"
|
||||
class="p-2 rounded"
|
||||
:style="{
|
||||
color: peer.lxmf_user_icon.foreground_colour,
|
||||
'background-color': peer.lxmf_user_icon.background_colour,
|
||||
}"
|
||||
>
|
||||
<MaterialDesignIcon :icon-name="peer.lxmf_user_icon.icon_name" class="w-6 h-6" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="bg-gray-200 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-2 rounded"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="account-outline" class="w-6 h-6" />
|
||||
</div>
|
||||
<LxmfUserIcon
|
||||
:icon-name="peer.lxmf_user_icon?.icon_name"
|
||||
:icon-foreground-colour="peer.lxmf_user_icon?.foreground_colour"
|
||||
:icon-background-colour="peer.lxmf_user_icon?.background_colour"
|
||||
icon-class="size-7"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div
|
||||
|
||||
@@ -126,18 +126,30 @@ export default {
|
||||
props: {
|
||||
messageHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
initialUri: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ["close"],
|
||||
data() {
|
||||
return {
|
||||
uri: null,
|
||||
isLoading: true,
|
||||
uri: this.initialUri,
|
||||
isLoading: !this.initialUri,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.fetchUri();
|
||||
if (!this.uri && this.messageHash) {
|
||||
await this.fetchUri();
|
||||
} else if (this.uri) {
|
||||
this.$nextTick(() => {
|
||||
this.renderQRCode();
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchUri() {
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
:icon-name="iconName"
|
||||
:icon-foreground-colour="iconForegroundColour"
|
||||
:icon-background-colour="iconBackgroundColour"
|
||||
icon-class="size-16"
|
||||
icon-class="size-24"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-zinc-400 text-center max-w-md">
|
||||
@@ -154,7 +154,7 @@
|
||||
:icon-background-colour="
|
||||
iconName === mdiIconName ? iconBackgroundColour : '#e5e7eb'
|
||||
"
|
||||
icon-class="size-8"
|
||||
icon-class="size-12"
|
||||
/>
|
||||
<div
|
||||
class="mt-2 text-xs text-center text-gray-600 dark:text-zinc-400 truncate w-full"
|
||||
|
||||
@@ -368,11 +368,41 @@
|
||||
<p>{{ $t("app.appearance_description") }}</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="glass-card__body space-y-3">
|
||||
<select v-model="config.theme" class="input-field" @change="onThemeChange">
|
||||
<option value="light">{{ $t("app.light_theme") }}</option>
|
||||
<option value="dark">{{ $t("app.dark_theme") }}</option>
|
||||
</select>
|
||||
<div class="glass-card__body space-y-4">
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $t("app.theme") }}
|
||||
</div>
|
||||
<select v-model="config.theme" class="input-field" @change="onThemeChange">
|
||||
<option value="light">{{ $t("app.light_theme") }}</option>
|
||||
<option value="dark">{{ $t("app.dark_theme") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Message Font Size
|
||||
</div>
|
||||
<div class="text-xs font-mono text-blue-500 dark:text-blue-400">
|
||||
{{ config.message_font_size || 14 }}px
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-gray-400">A</span>
|
||||
<input
|
||||
v-model.number="config.message_font_size"
|
||||
type="range"
|
||||
min="10"
|
||||
max="32"
|
||||
step="1"
|
||||
class="flex-1 h-1.5 bg-gray-200 dark:bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||
@input="onMessageFontSizeChange"
|
||||
/>
|
||||
<span class="text-lg text-gray-400">A</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-300 border border-dashed border-gray-200 dark:border-zinc-800 rounded-2xl px-3 py-2"
|
||||
>
|
||||
@@ -926,6 +956,17 @@ export default {
|
||||
"theme"
|
||||
);
|
||||
},
|
||||
async onMessageFontSizeChange() {
|
||||
if (this.saveTimeouts.message_font_size) clearTimeout(this.saveTimeouts.message_font_size);
|
||||
this.saveTimeouts.message_font_size = setTimeout(async () => {
|
||||
await this.updateConfig(
|
||||
{
|
||||
message_font_size: this.config.message_font_size,
|
||||
},
|
||||
"message_font_size"
|
||||
);
|
||||
}, 1000);
|
||||
},
|
||||
async onLanguageChange() {
|
||||
await this.updateConfig(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user