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:
2026-01-03 19:22:14 -06:00
parent 7abd0571c9
commit 1e8651c645
12 changed files with 407 additions and 110 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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">

View File

@@ -127,7 +127,7 @@
? (activeCall || lastCall).remote_icon.background_colour
: ''
"
icon-class="size-20"
icon-class="size-28"
/>
</div>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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() {

View File

@@ -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

View File

@@ -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() {

View File

@@ -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"

View File

@@ -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(
{