mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-10-23 04:23:36 +00:00
feat(ui): introduce UI transparency settings and glass effect toggle; enhance message display and scrolling behavior in conversation viewer
This commit is contained in:
7 files changed
+1201
-1150
No files matched your search
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{ dark: config?.theme === 'dark' }"
|
||||
class="h-screen w-full flex flex-col bg-slate-50 dark:bg-zinc-950 transition-colors"
|
||||
class="h-screen w-full flex flex-col transition-colors"
|
||||
:style="shellCanvasStyle"
|
||||
>
|
||||
<!-- emergency banner -->
|
||||
<div
|
||||
@@ -34,7 +35,11 @@
|
||||
<RouterView v-if="$route.name === 'auth'" />
|
||||
|
||||
<template v-else>
|
||||
<div v-if="isPopoutMode" class="flex flex-1 h-full w-full overflow-hidden bg-slate-50 dark:bg-zinc-950">
|
||||
<div
|
||||
v-if="isPopoutMode"
|
||||
class="flex flex-1 h-full w-full overflow-hidden transition-colors"
|
||||
:style="shellCanvasStyle"
|
||||
>
|
||||
<RouterView class="flex-1" />
|
||||
</div>
|
||||
|
||||
@@ -122,7 +127,8 @@
|
||||
<!-- middle -->
|
||||
<div
|
||||
ref="middle"
|
||||
class="flex flex-1 w-full overflow-hidden bg-slate-50 dark:bg-zinc-950 transition-colors"
|
||||
class="flex flex-1 w-full overflow-hidden transition-colors"
|
||||
:style="shellCanvasStyle"
|
||||
>
|
||||
<!-- sidebar backdrop for mobile -->
|
||||
<div
|
||||
@@ -709,6 +715,17 @@ export default {
|
||||
showWsDisconnectedBanner() {
|
||||
return this.shellRunning && this.wsDisconnected && this.$route?.name !== "auth";
|
||||
},
|
||||
shellCanvasStyle() {
|
||||
const raw = Number(this.config?.ui_transparency ?? 0);
|
||||
const t = Number.isFinite(raw) ? Math.max(0, Math.min(100, raw)) : 0;
|
||||
const factor = t / 100;
|
||||
const alpha = 1 - factor * 0.42;
|
||||
const isDark = this.config?.theme === "dark";
|
||||
if (isDark) {
|
||||
return { backgroundColor: `rgba(9, 9, 11, ${alpha})` };
|
||||
}
|
||||
return { backgroundColor: `rgba(248, 250, 252, ${alpha})` };
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
@@ -729,6 +746,7 @@ export default {
|
||||
if (newConfig && "theme" in newConfig) {
|
||||
this.applyThemePreference(newConfig.theme ?? "light");
|
||||
}
|
||||
this.applyShellAppearance();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
@@ -758,6 +776,7 @@ export default {
|
||||
// ignore
|
||||
}
|
||||
this.startShellAuthWatch();
|
||||
this.applyShellAppearance();
|
||||
if (ElectronUtils.isElectron()) {
|
||||
window.electron.onProtocolLink((url) => {
|
||||
this.handleProtocolLink(url);
|
||||
@@ -981,6 +1000,14 @@ export default {
|
||||
if (themeName && typeof themeName === "object" && "value" in themeName) {
|
||||
themeName.value = mode;
|
||||
}
|
||||
this.applyShellAppearance();
|
||||
},
|
||||
applyShellAppearance() {
|
||||
if (typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
const glassOn = this.config?.ui_glass_enabled !== false;
|
||||
document.documentElement.dataset.uiGlass = glassOn ? "1" : "0";
|
||||
},
|
||||
getHashPopoutValue() {
|
||||
const hash = window.location.hash || "";
|
||||
|
||||
@@ -2425,9 +2425,7 @@ export default {
|
||||
},
|
||||
logWebAudioFailure(stage, error) {
|
||||
const appImage = Boolean(
|
||||
window.electron &&
|
||||
typeof navigator?.userAgent === "string" &&
|
||||
navigator.userAgent.includes("AppImage"),
|
||||
window.electron && typeof navigator?.userAgent === "string" && navigator.userAgent.includes("AppImage")
|
||||
);
|
||||
console.error(
|
||||
`[CallPage:web-audio] ${stage}`,
|
||||
@@ -2436,7 +2434,7 @@ export default {
|
||||
isAppImage: appImage,
|
||||
userAgent: navigator?.userAgent || "unknown",
|
||||
},
|
||||
error,
|
||||
error
|
||||
);
|
||||
},
|
||||
async disableWebAudioBridgeWithError(errorKey, error, stage = "unknown") {
|
||||
@@ -2498,7 +2496,7 @@ export default {
|
||||
await this.disableWebAudioBridgeWithError(
|
||||
"call.web_audio_not_available",
|
||||
new Error("navigator.mediaDevices is unavailable"),
|
||||
"start-preflight-media-devices",
|
||||
"start-preflight-media-devices"
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -2508,7 +2506,7 @@ export default {
|
||||
await this.disableWebAudioBridgeWithError(
|
||||
"call.no_audio_input_found",
|
||||
new Error("No audio input devices detected"),
|
||||
"start-no-input-devices",
|
||||
"start-no-input-devices"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -407,11 +407,7 @@
|
||||
ref="messagesScroll"
|
||||
class="flex-1 min-h-0 overflow-y-scroll bg-white dark:bg-zinc-950 transition-none"
|
||||
style="overflow-anchor: none; overscroll-behavior-y: contain"
|
||||
:class="
|
||||
!messagesViewportReady
|
||||
? 'invisible opacity-0 pointer-events-none select-none'
|
||||
: ''
|
||||
"
|
||||
:class="!messagesViewportReady ? 'invisible opacity-0 pointer-events-none select-none' : ''"
|
||||
:data-message-list-mode="useVirtualMessageList ? 'virtual' : 'reverse'"
|
||||
:aria-busy="!messagesViewportReady ? 'true' : undefined"
|
||||
@scroll="onMessagesScroll"
|
||||
@@ -427,55 +423,55 @@
|
||||
<ConversationMessageEntry :entry="entry" :cv="conversationViewerSelf" />
|
||||
</template>
|
||||
<!-- load previous -->
|
||||
<button
|
||||
v-show="!isLoadingPrevious && hasMorePrevious"
|
||||
id="load-previous"
|
||||
type="button"
|
||||
class="flex items-center gap-2 mx-auto mt-4 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-sm text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
|
||||
@click="loadPrevious"
|
||||
>
|
||||
<svg
|
||||
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="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Load Previous</span>
|
||||
</button>
|
||||
<button
|
||||
v-show="!isLoadingPrevious && hasMorePrevious"
|
||||
id="load-previous"
|
||||
type="button"
|
||||
class="flex items-center gap-2 mx-auto mt-4 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-sm text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
|
||||
@click="loadPrevious"
|
||||
>
|
||||
<svg
|
||||
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="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Load Previous</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
v-show="!isLoadingPrevious && hasMorePrevious"
|
||||
id="load-previous"
|
||||
type="button"
|
||||
class="absolute top-2 left-1/2 z-20 -translate-x-1/2 flex items-center gap-2 bg-white/95 dark:bg-zinc-950/95 backdrop-blur border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-sm text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
|
||||
@click="loadPrevious"
|
||||
>
|
||||
<svg
|
||||
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"
|
||||
<button
|
||||
v-show="!isLoadingPrevious && hasMorePrevious"
|
||||
id="load-previous"
|
||||
type="button"
|
||||
class="absolute top-2 left-1/2 z-20 -translate-x-1/2 flex items-center gap-2 bg-white/95 dark:bg-zinc-950/95 backdrop-blur border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-sm text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
|
||||
@click="loadPrevious"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Load Previous</span>
|
||||
</button>
|
||||
<svg
|
||||
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="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Load Previous</span>
|
||||
</button>
|
||||
<ConversationMessageListVirtual
|
||||
ref="messageListVirtual"
|
||||
:groups="selectedPeerChatDisplayGroupsOldestFirst"
|
||||
@@ -487,7 +483,10 @@
|
||||
</div>
|
||||
|
||||
<Transition name="scroll-fab">
|
||||
<div v-if="!autoScrollOnNewMessage && messagesViewportReady" class="flex justify-center pb-1.5 pt-0.5 shrink-0">
|
||||
<div
|
||||
v-if="!autoScrollOnNewMessage && messagesViewportReady"
|
||||
class="flex justify-center pb-1.5 pt-0.5 shrink-0"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center size-8 rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur border border-gray-200 dark:border-zinc-700 shadow text-gray-500 dark:text-zinc-400 hover:bg-gray-100 dark:hover:bg-zinc-700 hover:text-gray-700 dark:hover:text-zinc-200 transition-colors"
|
||||
@@ -500,11 +499,7 @@
|
||||
</Transition>
|
||||
|
||||
<Transition name="scroll-fab">
|
||||
<div
|
||||
v-if="reactionPickerChatItem"
|
||||
class="absolute inset-0 z-40"
|
||||
@click.self="closeReactionPicker"
|
||||
>
|
||||
<div v-if="reactionPickerChatItem" class="absolute inset-0 z-40" @click.self="closeReactionPicker">
|
||||
<div
|
||||
ref="reactionPickerPanel"
|
||||
class="absolute w-[min(24rem,calc(100%-1rem))] rounded-2xl overflow-hidden border border-gray-200 dark:border-zinc-700 shadow-2xl bg-white dark:bg-zinc-900"
|
||||
@@ -515,7 +510,9 @@
|
||||
@mousedown.prevent="onReactionPickerDragStart"
|
||||
@touchstart.prevent="onReactionPickerDragStart"
|
||||
>
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-zinc-400">{{ $t("messages.react") }}</span>
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-zinc-400">{{
|
||||
$t("messages.react")
|
||||
}}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-400 dark:text-zinc-500"
|
||||
@@ -1744,7 +1741,7 @@ export default {
|
||||
columbaReactionEmojis: COLUMBA_REACTION_EMOJIS,
|
||||
reactionPickerChatItem: null,
|
||||
reactionPickerPos: null,
|
||||
_reactionDrag: null,
|
||||
reactionDragState: null,
|
||||
userStickers: [],
|
||||
isStickerPickerOpen: false,
|
||||
emojiStickerTab: "emoji",
|
||||
@@ -1759,9 +1756,9 @@ export default {
|
||||
windowWidth: typeof window !== "undefined" ? window.innerWidth : 1024,
|
||||
peerHeaderCompact: false,
|
||||
peerHeaderResizeObserver: null,
|
||||
_scrollBottomGen: 0,
|
||||
_prevScrollWantedLoadPrevious: false,
|
||||
_initialLoadActive: false,
|
||||
scrollBottomGen: 0,
|
||||
prevScrollWantedLoadPrevious: false,
|
||||
initialLoadActive: false,
|
||||
messagesViewportReady: true,
|
||||
};
|
||||
},
|
||||
@@ -2094,7 +2091,7 @@ export default {
|
||||
this.saveDraft(oldPeer.destination_hash);
|
||||
}
|
||||
this.teardownPeerHeaderResizeObserver();
|
||||
this._scrollBottomGen += 1;
|
||||
this.scrollBottomGen += 1;
|
||||
this.messagesViewportReady = false;
|
||||
if (!newPeer) {
|
||||
this.peerHeaderCompact = false;
|
||||
@@ -2102,7 +2099,7 @@ export default {
|
||||
this.checkIfSelectedPeerBlocked();
|
||||
this.strangerBannerDismissed = false;
|
||||
this.checkIfStrangerPeer();
|
||||
this._prevScrollWantedLoadPrevious = false;
|
||||
this.prevScrollWantedLoadPrevious = false;
|
||||
this.initialLoad();
|
||||
if (newPeer) {
|
||||
this.loadDraft(newPeer.destination_hash);
|
||||
@@ -2113,7 +2110,7 @@ export default {
|
||||
},
|
||||
useVirtualMessageList: {
|
||||
handler(value) {
|
||||
if (!value && !this._initialLoadActive) {
|
||||
if (!value && !this.initialLoadActive) {
|
||||
this.messagesViewportReady = true;
|
||||
}
|
||||
},
|
||||
@@ -2182,7 +2179,7 @@ export default {
|
||||
window.addEventListener("resize", this._onWindowResize);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this._scrollBottomGen += 1;
|
||||
this.scrollBottomGen += 1;
|
||||
this.teardownPeerHeaderResizeObserver();
|
||||
if (this.selectedPeer) {
|
||||
this.saveDraft(this.selectedPeer.destination_hash);
|
||||
@@ -2455,13 +2452,13 @@ export default {
|
||||
this.autoScrollOnNewMessage = isNearBottom(element);
|
||||
|
||||
const wantLoad = shouldLoadPreviousMessages(element);
|
||||
if (wantLoad && !this._prevScrollWantedLoadPrevious) {
|
||||
if (wantLoad && !this.prevScrollWantedLoadPrevious) {
|
||||
this.loadPrevious();
|
||||
}
|
||||
this._prevScrollWantedLoadPrevious = wantLoad;
|
||||
this.prevScrollWantedLoadPrevious = wantLoad;
|
||||
},
|
||||
async initialLoad() {
|
||||
this._initialLoadActive = true;
|
||||
this.initialLoadActive = true;
|
||||
this.messagesViewportReady = false;
|
||||
this.chatItems = [];
|
||||
this.hasMorePrevious = true;
|
||||
@@ -2469,7 +2466,7 @@ export default {
|
||||
this.selectedPeerLxmfStampInfo = null;
|
||||
this.selectedPeerSignalMetrics = null;
|
||||
if (!this.selectedPeer) {
|
||||
this._initialLoadActive = false;
|
||||
this.initialLoadActive = false;
|
||||
this.messagesViewportReady = true;
|
||||
return;
|
||||
}
|
||||
@@ -2483,7 +2480,7 @@ export default {
|
||||
|
||||
await this.loadPrevious();
|
||||
|
||||
this._initialLoadActive = false;
|
||||
this.initialLoadActive = false;
|
||||
this.scrollMessagesToBottom();
|
||||
|
||||
this.autoLoadAudioAttachments();
|
||||
@@ -2964,9 +2961,9 @@ export default {
|
||||
);
|
||||
},
|
||||
scrollMessagesToBottom: function () {
|
||||
this._scrollBottomGen += 1;
|
||||
const gen = this._scrollBottomGen;
|
||||
const stale = () => gen !== this._scrollBottomGen;
|
||||
this.scrollBottomGen += 1;
|
||||
const gen = this.scrollBottomGen;
|
||||
const stale = () => gen !== this.scrollBottomGen;
|
||||
this.$nextTick(() => {
|
||||
if (stale()) return;
|
||||
this.$nextTick(() => {
|
||||
@@ -3007,7 +3004,6 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
isLxmfMessageInUi: function (hash) {
|
||||
return this.chatItems.findIndex((chatItem) => chatItem.lxmf_message?.hash === hash) !== -1;
|
||||
},
|
||||
@@ -3219,14 +3215,14 @@ export default {
|
||||
closeReactionPicker() {
|
||||
this.reactionPickerChatItem = null;
|
||||
this.reactionPickerPos = null;
|
||||
this._reactionDrag = null;
|
||||
this.reactionDragState = null;
|
||||
},
|
||||
onReactionPickerDragStart(e) {
|
||||
const evt = e.touches ? e.touches[0] : e;
|
||||
const panel = this.$refs.reactionPickerPanel;
|
||||
if (!panel) return;
|
||||
const rect = panel.getBoundingClientRect();
|
||||
this._reactionDrag = {
|
||||
this.reactionDragState = {
|
||||
startX: evt.clientX,
|
||||
startY: evt.clientY,
|
||||
originX: rect.left,
|
||||
@@ -3234,11 +3230,11 @@ export default {
|
||||
};
|
||||
const onMove = (me) => {
|
||||
const mv = me.touches ? me.touches[0] : me;
|
||||
const dx = mv.clientX - this._reactionDrag.startX;
|
||||
const dy = mv.clientY - this._reactionDrag.startY;
|
||||
const dx = mv.clientX - this.reactionDragState.startX;
|
||||
const dy = mv.clientY - this.reactionDragState.startY;
|
||||
this.reactionPickerPos = {
|
||||
x: this._reactionDrag.originX + dx,
|
||||
y: this._reactionDrag.originY + dy,
|
||||
x: this.reactionDragState.originX + dx,
|
||||
y: this.reactionDragState.originY + dy,
|
||||
};
|
||||
};
|
||||
const onUp = () => {
|
||||
@@ -5246,7 +5242,9 @@ export default {
|
||||
|
||||
.scroll-fab-enter-active,
|
||||
.scroll-fab-leave-active {
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
transition:
|
||||
opacity 0.15s ease,
|
||||
transform 0.15s ease;
|
||||
}
|
||||
.scroll-fab-enter-from,
|
||||
.scroll-fab-leave-to {
|
||||
|
||||
@@ -887,6 +887,57 @@
|
||||
</div>
|
||||
</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">
|
||||
{{ $t("app.ui_transparency") }}
|
||||
</div>
|
||||
<div class="text-xs font-mono text-blue-500 dark:text-blue-400">
|
||||
{{ Math.max(0, Math.min(100, Number(config.ui_transparency) || 0)) }}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-gray-400">0</span>
|
||||
<input
|
||||
v-model.number="config.ui_transparency"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
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="onUiTransparencyChange"
|
||||
/>
|
||||
<span class="text-xs text-gray-400">100</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ $t("app.ui_transparency_description") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="setting-toggle">
|
||||
<Toggle
|
||||
id="ui-glass-enabled"
|
||||
v-model="config.ui_glass_enabled"
|
||||
@update:model-value="onUiGlassEnabledChange"
|
||||
/>
|
||||
<span class="setting-toggle__label">
|
||||
<span class="setting-toggle__title">{{ $t("app.ui_glass_enabled") }}</span>
|
||||
<span class="setting-toggle__description">{{
|
||||
$t("app.ui_glass_enabled_description")
|
||||
}}</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="pt-1">
|
||||
<button
|
||||
type="button"
|
||||
class="p-0 border-0 bg-transparent text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline cursor-pointer"
|
||||
@click="resetAppearanceDefaults"
|
||||
>
|
||||
{{ $t("app.reset_appearance_defaults") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-3 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"
|
||||
>
|
||||
@@ -1483,7 +1534,6 @@
|
||||
<span class="setting-toggle__description">{{
|
||||
$t("app.transport_toggle_description")
|
||||
}}</span>
|
||||
<span class="setting-toggle__hint">{{ $t("app.requires_restart") }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -2008,8 +2058,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- System / RNS Reload -->
|
||||
<!--
|
||||
<section class="settings-section break-inside-avoid">
|
||||
<header class="settings-section__header">
|
||||
<div>
|
||||
@@ -2021,24 +2069,29 @@
|
||||
<div class="settings-section__body space-y-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<button
|
||||
class="btn btn--secondary w-full justify-center gap-2 py-3"
|
||||
class="btn btn--secondary w-full justify-center py-3"
|
||||
:disabled="reloadingRns"
|
||||
@click="reloadRns"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
:icon-name="reloadingRns ? 'refresh' : 'restart'"
|
||||
class="w-5 h-5"
|
||||
:class="{ 'animate-spin-reverse': reloadingRns }"
|
||||
/>
|
||||
<span>{{ reloadingRns ? $t("app.reloading_rns") : $t("app.reload_rns") }}</span>
|
||||
<span>{{ $t("app.reload_rns") }}</span>
|
||||
</button>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("app.reload_rns_description") }}
|
||||
</p>
|
||||
<p
|
||||
v-if="reloadRnsStatusMessage"
|
||||
class="text-xs"
|
||||
:class="
|
||||
reloadingRns
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
"
|
||||
>
|
||||
{{ reloadRnsStatusMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<!-- Keyboard Shortcuts (Full width at bottom) -->
|
||||
@@ -2149,6 +2202,8 @@ export default {
|
||||
discovered_interfaces_max_return: 500,
|
||||
message_font_size: 14,
|
||||
message_icon_size: 28,
|
||||
ui_transparency: 0,
|
||||
ui_glass_enabled: true,
|
||||
message_outbound_bubble_color: "#4f46e5",
|
||||
message_inbound_bubble_color: null,
|
||||
message_failed_bubble_color: "#ef4444",
|
||||
@@ -2176,6 +2231,7 @@ export default {
|
||||
lastRememberedInboundStampCost: 8,
|
||||
shortcuts: [],
|
||||
reloadingRns: false,
|
||||
reloadRnsStatusMessage: "",
|
||||
searchQuery: "",
|
||||
trustedTelemetryPeers: [],
|
||||
stickerCount: 0,
|
||||
@@ -2280,6 +2336,9 @@ export default {
|
||||
"app.theme",
|
||||
"app.light_theme",
|
||||
"app.dark_theme",
|
||||
"app.ui_transparency",
|
||||
"app.ui_glass_enabled",
|
||||
"app.reset_appearance_defaults",
|
||||
"Message Font Size",
|
||||
"Icon Size",
|
||||
"Message Bubbles",
|
||||
@@ -2505,6 +2564,22 @@ export default {
|
||||
this.shortcuts = json.shortcuts;
|
||||
break;
|
||||
}
|
||||
case "reticulum_reload_status": {
|
||||
const message = json.message || this.$t("app.reloading_rns");
|
||||
this.reloadRnsStatusMessage = message;
|
||||
this.reloadingRns = json.in_progress !== false;
|
||||
const toastKey = "settings-rns-reload";
|
||||
if (json.level === "error") {
|
||||
ToastUtils.dismiss(toastKey);
|
||||
ToastUtils.error(message, 7000);
|
||||
} else if (json.level === "success") {
|
||||
ToastUtils.dismiss(toastKey);
|
||||
ToastUtils.success(message, 5000);
|
||||
} else {
|
||||
ToastUtils.info(message, 2500, toastKey);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
async getConfig() {
|
||||
@@ -2660,6 +2735,53 @@ export default {
|
||||
);
|
||||
}, 1000);
|
||||
},
|
||||
onUiTransparencyChange() {
|
||||
if (this.saveTimeouts.ui_transparency) clearTimeout(this.saveTimeouts.ui_transparency);
|
||||
this.saveTimeouts.ui_transparency = setTimeout(async () => {
|
||||
const n = Number(this.config.ui_transparency);
|
||||
const v = Number.isFinite(n) ? Math.max(0, Math.min(100, Math.round(n))) : 0;
|
||||
this.config.ui_transparency = v;
|
||||
await this.updateConfig(
|
||||
{
|
||||
ui_transparency: v,
|
||||
},
|
||||
"ui_transparency"
|
||||
);
|
||||
}, 400);
|
||||
},
|
||||
async onUiGlassEnabledChange() {
|
||||
await this.updateConfig(
|
||||
{
|
||||
ui_glass_enabled: this.config.ui_glass_enabled,
|
||||
},
|
||||
"ui_glass_enabled"
|
||||
);
|
||||
},
|
||||
async resetAppearanceDefaults() {
|
||||
this.config.theme = "light";
|
||||
this.config.message_font_size = 14;
|
||||
this.config.message_icon_size = 28;
|
||||
this.config.ui_transparency = 0;
|
||||
this.config.ui_glass_enabled = true;
|
||||
this.config.message_outbound_bubble_color = "#4f46e5";
|
||||
this.config.message_inbound_bubble_color = null;
|
||||
this.config.message_failed_bubble_color = "#ef4444";
|
||||
this.config.message_waiting_bubble_color = "#e5e7eb";
|
||||
await this.updateConfig(
|
||||
{
|
||||
theme: "light",
|
||||
message_font_size: 14,
|
||||
message_icon_size: 28,
|
||||
ui_transparency: 0,
|
||||
ui_glass_enabled: true,
|
||||
message_outbound_bubble_color: "#4f46e5",
|
||||
message_inbound_bubble_color: null,
|
||||
message_failed_bubble_color: "#ef4444",
|
||||
message_waiting_bubble_color: "#e5e7eb",
|
||||
},
|
||||
"appearance"
|
||||
);
|
||||
},
|
||||
async onMessageBubbleColorChange(type) {
|
||||
const timeoutKey = `message_${type}_bubble_color`;
|
||||
if (this.saveTimeouts[timeoutKey]) clearTimeout(this.saveTimeouts[timeoutKey]);
|
||||
@@ -3090,13 +3212,19 @@ export default {
|
||||
async onIsTransportEnabledChange() {
|
||||
if (this.config.is_transport_enabled) {
|
||||
try {
|
||||
await window.api.post("/api/v1/reticulum/enable-transport");
|
||||
const response = await window.api.post("/api/v1/reticulum/enable-transport");
|
||||
if (response?.data?.message) {
|
||||
ToastUtils.success(response.data.message);
|
||||
}
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("settings.failed_enable_transport"));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await window.api.post("/api/v1/reticulum/disable-transport");
|
||||
const response = await window.api.post("/api/v1/reticulum/disable-transport");
|
||||
if (response?.data?.message) {
|
||||
ToastUtils.success(response.data.message);
|
||||
}
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("settings.failed_disable_transport"));
|
||||
}
|
||||
@@ -3107,11 +3235,16 @@ export default {
|
||||
|
||||
try {
|
||||
this.reloadingRns = true;
|
||||
this.reloadRnsStatusMessage = this.$t("app.reloading_rns");
|
||||
ToastUtils.loading(this.$t("app.reloading_rns"), 0, "settings-rns-reload");
|
||||
const response = await window.api.post("/api/v1/reticulum/reload");
|
||||
ToastUtils.success(response.data.message);
|
||||
if (response?.data?.message) {
|
||||
this.reloadRnsStatusMessage = response.data.message;
|
||||
}
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("settings.failed_reload_reticulum"));
|
||||
} finally {
|
||||
ToastUtils.dismiss("settings-rns-reload");
|
||||
this.reloadingRns = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
@apply bg-sem-glass backdrop-blur border border-sem-border-card rounded-3xl shadow-xl px-4 py-4;
|
||||
}
|
||||
|
||||
html[data-ui-glass="0"] .glass-card {
|
||||
background-color: var(--mc-surface) !important;
|
||||
border-color: var(--mc-border-card) !important;
|
||||
@apply shadow-xl px-4 py-4;
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply bg-sem-surface-muted border border-sem-border text-sm rounded-2xl focus:ring-2 focus:ring-sem-focus focus:border-sem-focus-border block w-full p-2.5 text-sem-fg transition;
|
||||
}
|
||||
@@ -35,10 +43,10 @@ select.input-field option {
|
||||
}
|
||||
|
||||
.primary-chip {
|
||||
@apply inline-flex items-center justify-center gap-x-2 rounded-full bg-sem-accent px-4 py-2 text-xs font-bold text-white shadow-sm transition-all !important;
|
||||
@apply inline-flex items-center justify-center gap-x-2 rounded-full bg-sem-action-primary px-4 py-2 text-xs font-bold text-white shadow-sm transition-all !important;
|
||||
}
|
||||
.primary-chip:hover {
|
||||
@apply bg-sem-accent-hover -translate-y-0.5 !important;
|
||||
@apply bg-sem-action-primary-hover -translate-y-0.5 !important;
|
||||
}
|
||||
.primary-chip:active {
|
||||
@apply scale-95 translate-y-0 !important;
|
||||
@@ -126,11 +134,11 @@ select.input-field option {
|
||||
}
|
||||
|
||||
.file-input::file-selector-button {
|
||||
@apply mr-2 border-0 rounded-md bg-sem-accent text-white px-3 py-1 text-xs font-semibold cursor-pointer hover:bg-sem-accent-hover transition;
|
||||
@apply mr-2 border-0 rounded-md bg-sem-action-primary text-white px-3 py-1 text-xs font-semibold cursor-pointer hover:bg-sem-action-primary-hover transition;
|
||||
}
|
||||
|
||||
.dark .file-input::file-selector-button {
|
||||
@apply bg-sem-accent hover:bg-sem-accent-hover text-white;
|
||||
@apply bg-sem-action-primary hover:bg-sem-action-primary-hover text-white;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
|
||||
@@ -6,9 +6,20 @@ const {
|
||||
E2E_SCROLL_PEER_HASH,
|
||||
} = require("./helpers");
|
||||
|
||||
async function waitForMessagesViewportReady(page) {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const el = document.getElementById("messages");
|
||||
return el != null && el.getAttribute("aria-busy") !== "true";
|
||||
},
|
||||
null,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
}
|
||||
|
||||
async function scrollMetrics(page) {
|
||||
await waitForMessagesViewportReady(page);
|
||||
const loc = page.locator("#messages");
|
||||
await expect(loc).toBeVisible({ timeout: 30000 });
|
||||
return loc.evaluate((el) => ({
|
||||
scrollTop: el.scrollTop,
|
||||
scrollHeight: el.scrollHeight,
|
||||
@@ -87,6 +98,7 @@ test.describe("Messages conversation scroll", () => {
|
||||
.filter({ hasText: /E2E scroll seed/ })
|
||||
.first()
|
||||
.click();
|
||||
await waitForMessagesViewportReady(page);
|
||||
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
||||
await expect(
|
||||
page
|
||||
@@ -112,6 +124,7 @@ test.describe("Messages conversation scroll", () => {
|
||||
.filter({ hasText: /E2E scroll seed/ })
|
||||
.first()
|
||||
.click();
|
||||
await waitForMessagesViewportReady(page);
|
||||
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
||||
await waitForMessagesOverflow(page);
|
||||
|
||||
@@ -163,6 +176,7 @@ test.describe("Messages conversation scroll", () => {
|
||||
.filter({ hasText: /E2E scroll seed/ })
|
||||
.first()
|
||||
.click();
|
||||
await waitForMessagesViewportReady(page);
|
||||
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
||||
await waitForMessagesOverflow(page);
|
||||
|
||||
@@ -205,6 +219,7 @@ test.describe("Messages conversation scroll", () => {
|
||||
.filter({ hasText: /E2E scroll seed/ })
|
||||
.first()
|
||||
.click();
|
||||
await waitForMessagesViewportReady(page);
|
||||
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
||||
await waitForMessagesOverflow(page);
|
||||
|
||||
|
||||
Reference in new issue
Block a user