feat(App): add emergency banner for active mode and enhance changelog modal with version tracking; improve notification logic for incoming messages
Some checks failed
Tests / test (pull_request) Failing after 12m30s
Benchmarks / benchmark (push) Successful in 15m13s
Benchmarks / benchmark (pull_request) Successful in 23m54s
CI / test-backend (push) Successful in 4s
CI / test-backend (pull_request) Successful in 4s
Build and Publish Docker Image / build (pull_request) Has been skipped
Build Test / Build and Test (pull_request) Failing after 1m23s
CI / build-frontend (push) Successful in 1m31s
CI / lint (push) Failing after 5m4s
CI / lint (pull_request) Failing after 5m5s
Build Test / Build and Test (push) Successful in 6m4s
CI / build-frontend (pull_request) Successful in 9m43s
Tests / test (push) Failing after 9m53s
Build and Publish Docker Image / build-dev (pull_request) Successful in 12m43s

This commit is contained in:
2026-01-03 18:31:45 -06:00
parent d717679790
commit b51d04953f
10 changed files with 202 additions and 112 deletions

View File

@@ -3,6 +3,17 @@
:class="{ dark: config?.theme === 'dark' }"
class="h-screen w-full flex flex-col bg-slate-50 dark:bg-zinc-950 transition-colors"
>
<!-- emergency banner -->
<div
v-if="appInfo?.emergency"
class="relative z-[100] bg-red-600 text-white px-4 py-2 text-center text-sm font-bold shadow-md animate-pulse"
>
<div class="flex items-center justify-center gap-2">
<MaterialDesignIcon icon-name="alert-decagram" class="size-5" />
<span>{{ $t("app.emergency_mode_active") }}</span>
</div>
</div>
<RouterView v-if="$route.name === 'auth'" />
<template v-else>
@@ -437,7 +448,7 @@
<ConfirmDialog />
<CommandPalette />
<IntegrityWarningModal />
<ChangelogModal ref="changelogModal" />
<ChangelogModal ref="changelogModal" :app-version="appInfo?.version" />
<TutorialModal ref="tutorialModal" />
<!-- identity switching overlay -->
@@ -723,7 +734,8 @@ export default {
}
// show notification for new messages if window is not focussed
if (!document.hasFocus()) {
// only for incoming messages
if (!document.hasFocus() && json.lxmf_message?.is_incoming) {
NotificationUtils.showNewMessageNotification(
json.remote_identity_name,
json.lxmf_message?.content
@@ -761,8 +773,12 @@ export default {
this.hasCheckedForModals = true;
if (this.appInfo && !this.appInfo.tutorial_seen) {
this.$refs.tutorialModal.show();
} else if (this.appInfo && this.appInfo.changelog_seen_version !== this.appInfo.version) {
// show changelog if version changed
} else if (
this.appInfo &&
this.appInfo.changelog_seen_version !== "999.999.999" &&
this.appInfo.changelog_seen_version !== this.appInfo.version
) {
// show changelog if version changed and not silenced forever
this.$refs.changelogModal.show();
}
}

View File

@@ -50,9 +50,9 @@
<div v-else-if="error" class="flex flex-col items-center justify-center h-full text-center space-y-4">
<v-icon icon="mdi-alert-circle-outline" size="64" color="red"></v-icon>
<div class="text-red-500 font-bold text-lg">{{ error }}</div>
<v-btn color="#3b82f6" variant="flat" class="text-white font-bold" @click="fetchChangelog"
>Retry</v-btn
>
<v-btn color="blue" variant="flat" class="font-bold uppercase px-6" rounded="lg" @click="fetchChangelog">
Retry
</v-btn>
</div>
<div
@@ -66,20 +66,30 @@
<!-- Footer -->
<v-divider class="dark:border-zinc-800"></v-divider>
<v-card-actions class="px-6 py-4 bg-gray-50 dark:bg-zinc-950/50">
<v-checkbox
v-model="dontShowAgain"
:label="$t('app.do_not_show_again', 'Do not show again for this version')"
density="compact"
hide-details
color="blue"
class="my-0 text-gray-700 dark:text-zinc-300 font-medium"
></v-checkbox>
<v-card-actions class="px-6 py-4 bg-gray-50 dark:bg-zinc-950/50 flex-wrap gap-y-2">
<div class="flex flex-col">
<v-checkbox
v-model="dontShowAgain"
:label="$t('app.do_not_show_again', 'Do not show again for this version')"
density="compact"
hide-details
color="blue"
class="my-0 text-gray-700 dark:text-zinc-300 font-medium"
></v-checkbox>
<v-checkbox
v-model="dontShowEver"
:label="$t('app.do_not_show_ever', 'Do not show ever again')"
density="compact"
hide-details
color="red"
class="my-0 text-gray-700 dark:text-zinc-300 font-medium"
></v-checkbox>
</div>
<v-spacer></v-spacer>
<v-btn
variant="tonal"
variant="flat"
color="blue"
class="px-8 font-black tracking-tighter text-white uppercase"
class="px-8 font-black tracking-tighter uppercase text-white dark:text-zinc-900"
rounded="xl"
@click="close"
>
@@ -121,9 +131,9 @@
<div v-else-if="error" class="flex flex-col items-center justify-center py-20 text-center space-y-4">
<v-icon icon="mdi-alert-circle-outline" size="64" color="red"></v-icon>
<div class="text-red-500 font-bold text-lg">{{ error }}</div>
<v-btn color="#3b82f6" variant="flat" class="text-white font-bold" @click="fetchChangelog"
>Retry</v-btn
>
<v-btn color="blue" variant="flat" class="font-bold uppercase px-6" rounded="lg" @click="fetchChangelog">
Retry
</v-btn>
</div>
<div v-else class="changelog-content max-w-none prose dark:prose-invert pb-20">
@@ -138,6 +148,12 @@
<script>
export default {
name: "ChangelogModal",
props: {
appVersion: {
type: String,
default: "",
},
},
data() {
return {
visible: false,
@@ -146,9 +162,13 @@ export default {
changelogHtml: "",
version: "",
dontShowAgain: false,
dontShowEver: false,
};
},
computed: {
currentVersion() {
return this.version || this.appVersion;
},
isPage() {
return this.$route?.meta?.isPage === true;
},
@@ -187,18 +207,43 @@ export default {
}
},
async close() {
this.visible = false;
// logic moved to onVisibleUpdate
},
async onVisibleUpdate(val) {
if (!val && this.dontShowAgain) {
// mark as seen for current version automatically on close if not already marked
if (!this.dontShowEver && !this.dontShowAgain) {
try {
await window.axios.post("/api/v1/app/changelog/seen", {
version: this.version,
version: this.currentVersion || "0.0.0",
});
} catch (e) {
console.error("Failed to mark changelog as seen:", e);
console.error("Failed to auto-mark changelog as seen:", e);
}
} else {
await this.markAsSeen();
}
this.visible = false;
},
async markAsSeen() {
if (this.dontShowEver) {
try {
await window.axios.post("/api/v1/app/changelog/seen", {
version: "999.999.999",
});
} catch (e) {
console.error("Failed to mark changelog as seen forever:", e);
}
} else if (this.dontShowAgain) {
try {
await window.axios.post("/api/v1/app/changelog/seen", {
version: this.currentVersion,
});
} catch (e) {
console.error("Failed to mark changelog as seen for this version:", e);
}
}
},
async onVisibleUpdate(val) {
if (!val) {
// handle case where dialog is closed by clicking outside or ESC
await this.markAsSeen();
}
},
},

View File

@@ -22,7 +22,7 @@
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<div v-if="isShowingMenu" class="absolute left-0 z-10 ml-4">
<div v-if="isShowingMenu" class="absolute left-0 z-[100] mt-2">
<v-color-picker
v-model="colourPickerValue"
:modes="['hex']"

View File

@@ -1,16 +1,25 @@
<template>
<div v-if="customImage" class="rounded-full overflow-hidden" :class="iconClass">
<div
v-if="customImage"
class="rounded-full overflow-hidden shrink-0 flex items-center justify-center"
:class="iconClass || 'size-6'"
>
<img :src="customImage" class="w-full h-full object-cover" />
</div>
<div
v-else-if="iconName"
class="p-2 rounded-full"
:style="{ color: iconForegroundColour, 'background-color': iconBackgroundColour }"
class="p-[15%] rounded-full shrink-0 flex items-center justify-center"
:style="{ 'background-color': iconBackgroundColour }"
:class="iconClass || 'size-6'"
>
<MaterialDesignIcon :icon-name="iconName" :class="iconClass" />
<MaterialDesignIcon :icon-name="iconName" class="w-full h-full" :style="{ color: iconForegroundColour }" />
</div>
<div v-else class="bg-gray-200 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-2 rounded-full">
<MaterialDesignIcon icon-name="account-outline" :class="iconClass" />
<div
v-else
class="bg-gray-200 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-[15%] rounded-full shrink-0 flex items-center justify-center"
:class="iconClass || 'size-6'"
>
<MaterialDesignIcon icon-name="account-outline" class="w-full h-full" />
</div>
</template>
@@ -32,15 +41,15 @@ export default {
},
iconForegroundColour: {
type: String,
default: "",
default: "#6b7280",
},
iconBackgroundColour: {
type: String,
default: "",
default: "#e5e7eb",
},
iconClass: {
type: String,
default: "size-6",
default: "",
},
},
};

View File

@@ -5,6 +5,8 @@
role="img"
:aria-label="iconName"
fill="currentColor"
width="100%"
height="100%"
style="display: inline-block; vertical-align: middle; shape-rendering: inherit"
class="antialiased"
>
@@ -20,18 +22,26 @@ export default {
props: {
iconName: {
type: String,
required: true,
required: false,
default: "",
},
},
computed: {
mdiIconName() {
if (!this.iconName) return "mdiAccountOutline";
// if already starts with mdi and is camelCase, return as is
if (this.iconName.startsWith("mdi") && /[A-Z]/.test(this.iconName)) {
return this.iconName;
}
// convert icon name from lxmf icon appearance to format expected by the @mdi/js library
// e.g: alien-outline -> mdiAlienOutline
// https://pictogrammers.github.io/@mdi/font/5.4.55/
return (
"mdi" +
this.iconName
.split("-")
.filter(word => word.length > 0)
.map((word) => {
// capitalise first letter of each part
return word.charAt(0).toUpperCase() + word.slice(1);
@@ -40,8 +50,13 @@ export default {
);
},
iconPath() {
// find icon, otherwise fallback to question mark, and if that doesn't exist, show nothing...
return mdi[this.mdiIconName] || mdi["mdiProgressQuestion"] || "";
if (!mdi) return "";
const path = mdi[this.mdiIconName];
if (path) return path;
// fallback logic
return mdi["mdiHelpCircleOutline"] || mdi["mdiProgressQuestion"] || "";
},
},
};

View File

@@ -154,7 +154,8 @@
<div class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{{ $t("about.dependency_chain") }}
</div>
<div class="relative">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 relative">
<!-- Main Dependencies -->
<div class="flex flex-col space-y-4">
<!-- MeshChatX -->
<div class="flex items-center gap-4">
@@ -213,65 +214,65 @@
</div>
</div>
<!-- Side Dependencies -->
<div class="mt-8 pt-6 border-t border-gray-100 dark:border-zinc-800">
<div
class="text-xs font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-wider mb-3"
>
{{ $t("about.other_core_components", "Other Core Components") }}
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
<div v-if="appInfo.lxst_version" class="flex flex-col">
<span class="text-[10px] font-black text-blue-500 uppercase">LXST</span>
<span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
>v{{ appInfo.lxst_version }}</span
>
<!-- Side & Python Dependencies -->
<div class="space-y-8">
<!-- Side Dependencies -->
<div>
<div
class="text-xs font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-wider mb-3"
>
{{ $t("about.other_core_components", "Other Core Components") }}
</div>
<div class="flex flex-col">
<span class="text-[10px] font-black text-blue-500 uppercase">Python</span>
<span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
>v{{ appInfo.python_version }}</span
>
</div>
<div v-if="electronVersion" class="flex flex-col">
<span class="text-[10px] font-black text-blue-500 uppercase">Electron</span>
<span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
>v{{ electronVersion }}</span
>
</div>
<div v-if="chromeVersion" class="flex flex-col">
<span class="text-[10px] font-black text-blue-500 uppercase">Chrome</span>
<span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
>v{{ chromeVersion }}</span
>
</div>
<div v-if="nodeVersion" class="flex flex-col">
<span class="text-[10px] font-black text-blue-500 uppercase">Node</span>
<span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
>v{{ nodeVersion }}</span
>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
<div v-if="appInfo.lxst_version" class="flex flex-col">
<span class="text-[10px] font-black text-blue-500 uppercase">LXST</span>
<span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
>v{{ appInfo.lxst_version }}</span
>
</div>
<div class="flex flex-col">
<span class="text-[10px] font-black text-blue-500 uppercase">Python</span>
<span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
>v{{ appInfo.python_version }}</span
>
</div>
<div v-if="electronVersion" class="flex flex-col">
<span class="text-[10px] font-black text-blue-500 uppercase">Electron</span>
<span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
>v{{ electronVersion }}</span
>
</div>
<div v-if="chromeVersion" class="flex flex-col">
<span class="text-[10px] font-black text-blue-500 uppercase">Chrome</span>
<span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
>v{{ chromeVersion }}</span
>
</div>
<div v-if="nodeVersion" class="flex flex-col">
<span class="text-[10px] font-black text-blue-500 uppercase">Node</span>
<span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
>v{{ nodeVersion }}</span
>
</div>
</div>
</div>
</div>
<!-- Python Dependencies -->
<div
v-if="appInfo.dependencies"
class="mt-8 pt-6 border-t border-gray-100 dark:border-zinc-800"
>
<div
class="text-xs font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-wider mb-3"
>
{{ $t("about.backend_dependencies") }}
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
<div v-for="(version, name) in appInfo.dependencies" :key="name" class="flex flex-col">
<span class="text-[10px] font-black text-blue-500/70 uppercase">{{
name.replace("_", " ")
}}</span>
<span class="text-xs font-medium text-gray-600 dark:text-zinc-400"
>v{{ version }}</span
>
<!-- Python Dependencies -->
<div v-if="appInfo.dependencies" class="pt-6 border-t border-gray-100 dark:border-zinc-800">
<div
class="text-xs font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-wider mb-3"
>
{{ $t("about.backend_dependencies") }}
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
<div v-for="(version, name) in appInfo.dependencies" :key="name" class="flex flex-col">
<span class="text-[10px] font-black text-blue-500/70 uppercase">{{
name.replace("_", " ")
}}</span>
<span class="text-xs font-medium text-gray-600 dark:text-zinc-400"
>v{{ version }}</span
>
</div>
</div>
</div>
</div>

View File

@@ -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 : ''"
class="size-8"
icon-class="size-8"
/>
</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 : ''"
class="size-5 shrink-0"
icon-class="size-5 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
: ''
"
class="size-20"
icon-class="size-20"
/>
</div>
@@ -601,7 +601,7 @@
:icon-background-colour="
entry.remote_icon ? entry.remote_icon.background_colour : ''
"
class="size-10"
icon-class="size-10"
/>
<div
class="absolute -bottom-1 -right-1 bg-white dark:bg-zinc-900 rounded-full p-0.5 shadow-sm border border-gray-100 dark:border-zinc-800 shrink-0 flex items-center justify-center size-5"

View File

@@ -35,7 +35,7 @@
:icon-background-colour="
selectedPeer.lxmf_user_icon ? selectedPeer.lxmf_user_icon.background_colour : ''
"
class="size-10"
icon-class="size-10"
/>
</div>

View File

@@ -4,7 +4,7 @@
<div class="max-w-4xl mx-auto p-4 space-y-6">
<!-- Header with Preview -->
<div
class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800 overflow-hidden"
class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800"
>
<div class="p-6 border-b border-gray-200 dark:border-zinc-800">
<div class="flex items-center justify-between">
@@ -51,6 +51,7 @@
<div class="text-sm font-medium text-gray-700 dark:text-zinc-300">Preview</div>
<div class="p-8 bg-gray-50 dark:bg-zinc-800 rounded-2xl">
<LxmfUserIcon
:key="iconName + iconForegroundColour + iconBackgroundColour"
:icon-name="iconName"
:icon-foreground-colour="iconForegroundColour"
:icon-background-colour="iconBackgroundColour"
@@ -66,7 +67,7 @@
<!-- Color Selection -->
<div
class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800 overflow-hidden"
class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800"
>
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Colors</h3>
@@ -252,14 +253,17 @@ export default {
},
},
watch: {
config() {
if (this.config) {
this.iconName = this.config.lxmf_user_icon_name || null;
this.iconForegroundColour = this.config.lxmf_user_icon_foreground_colour || "#6b7280";
this.iconBackgroundColour = this.config.lxmf_user_icon_background_colour || "#e5e7eb";
config: {
handler() {
if (this.config) {
this.iconName = this.config.lxmf_user_icon_name || null;
this.iconForegroundColour = this.config.lxmf_user_icon_foreground_colour || "#6b7280";
this.iconBackgroundColour = this.config.lxmf_user_icon_background_colour || "#e5e7eb";
this.saveOriginalValues();
}
this.saveOriginalValues();
}
},
immediate: true,
},
iconForegroundColour() {
this.debouncedAutoSave();