feat(ui): implement warning for stranger links and configurable messages sidebar position

This commit is contained in:
Ivan
2026-04-16 23:33:24 -05:00
parent 6194933ce6
commit 111c686ff7
5 changed files with 142 additions and 17 deletions

View File

@@ -1783,6 +1783,9 @@ export default {
showUnknownContactBanner() {
return GlobalState.config?.show_unknown_contact_banner !== false;
},
warnOnStrangerLinksEnabled() {
return GlobalState.config?.warn_on_stranger_links !== false;
},
filteredContacts() {
if (!this.contactsSearch) return this.contacts;
const s = this.contactsSearch.toLowerCase();
@@ -2162,7 +2165,7 @@ export default {
}
return base;
},
handleMessageClick(event) {
async handleMessageClick(event) {
const nomadnetLink = event.target.closest(".nomadnet-link");
if (nomadnetLink) {
event.preventDefault();
@@ -2177,6 +2180,7 @@ export default {
query: { path: path },
});
}
return;
}
const lxmfLink = event.target.closest(".lxmf-link");
@@ -2189,7 +2193,31 @@ export default {
params: { destinationHash: address },
});
}
return;
}
const standardLink = event.target.closest("a[href]");
if (!standardLink) {
return;
}
const href = String(standardLink.getAttribute("href") || "").trim();
if (!/^https?:\/\//i.test(href)) {
event.preventDefault();
return;
}
event.preventDefault();
if (this.isStrangerPeer && this.warnOnStrangerLinksEnabled) {
const proceed = await DialogUtils.confirm(
this.$t("messages.stranger_link_open_confirm", { url: href })
);
if (!proceed) {
return;
}
}
window.open(href, "_blank", "noopener");
},
async updatePropagationNodeStatus() {
try {

View File

@@ -1,10 +1,11 @@
<!-- SPDX-License-Identifier: 0BSD AND MIT -->
<template>
<div class="flex flex-1 min-w-0 h-full overflow-hidden">
<div class="flex flex-1 min-w-0 h-full overflow-hidden" :class="{ 'flex-row-reverse': messagesSidebarOnRight }">
<MessagesSidebar
v-if="!isPopoutMode"
:class="{ 'hidden sm:flex': destinationHash }"
:sidebar-position="messagesSidebarPosition"
:collapsed="messagesListSidebarCollapsed"
:conversations="conversations"
:peers="peers"
@@ -202,6 +203,13 @@ export default {
isPopoutMode() {
return this.popoutRouteType === "conversation";
},
messagesSidebarPosition() {
const p = this.config?.messages_sidebar_position;
return p === "right" ? "right" : "left";
},
messagesSidebarOnRight() {
return this.messagesSidebarPosition === "right";
},
},
watch: {
conversations() {

View File

@@ -4,17 +4,21 @@
<div :class="sidebarRootClass">
<div
v-if="effectiveCollapsed"
class="flex flex-col h-full min-h-0 bg-white dark:bg-zinc-950 border-r border-gray-200 dark:border-zinc-800"
:class="[
'flex flex-col h-full min-h-0 bg-white dark:bg-zinc-950 border-gray-200 dark:border-zinc-800',
edgeBorderClass,
]"
>
<div
class="hidden sm:flex h-12 shrink-0 items-center justify-end border-b border-gray-200 dark:border-zinc-800 px-2"
class="hidden sm:flex h-12 shrink-0 items-center border-b border-gray-200 dark:border-zinc-800 px-2"
:class="collapsedHeaderJustifyClass"
>
<button
type="button"
class="p-1.5 rounded-lg text-gray-500 hover:bg-gray-100 dark:text-zinc-400 dark:hover:bg-zinc-800 transition-colors"
@click="$emit('toggle-collapse')"
>
<MaterialDesignIcon icon-name="chevron-right" class="size-5" />
<MaterialDesignIcon :icon-name="collapsedStripChevronIcon" class="size-5" />
</button>
</div>
<div class="flex flex-col items-center gap-1 py-2 px-1 border-b border-gray-200 dark:border-zinc-800">
@@ -73,8 +77,8 @@
</div>
<template v-else>
<!-- tabs (h-12 matches App.vue main sidebar collapse row) -->
<div class="bg-white dark:bg-zinc-950 border-b border-r border-gray-200 dark:border-zinc-800">
<div class="-mb-px flex h-12 min-w-0 items-stretch">
<div :class="['bg-white dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800', edgeBorderClass]">
<div class="-mb-px flex h-12 min-w-0 items-stretch" :class="{ 'flex-row-reverse': isRightSidebar }">
<div class="flex min-w-0 flex-1">
<div
class="flex w-full cursor-pointer items-center justify-center border-b-2 px-1 text-center text-sm font-semibold uppercase tracking-wide transition"
@@ -104,7 +108,7 @@
class="hidden sm:flex shrink-0 items-center border-b-2 border-transparent px-1.5 text-gray-500 hover:bg-gray-100 dark:text-zinc-400 dark:hover:bg-zinc-800 transition-colors"
@click="$emit('toggle-collapse')"
>
<MaterialDesignIcon icon-name="chevron-left" class="size-5" />
<MaterialDesignIcon :icon-name="expandedTabBarChevronIcon" class="size-5" />
</button>
</div>
</div>
@@ -112,7 +116,10 @@
<!-- conversations -->
<div
v-if="tab === 'conversations'"
class="flex-1 flex flex-col bg-white dark:bg-zinc-950 border-r border-gray-200 dark:border-zinc-800 overflow-hidden min-h-0"
:class="[
'flex-1 flex flex-col bg-white dark:bg-zinc-950 border-gray-200 dark:border-zinc-800 overflow-hidden min-h-0',
edgeBorderClass,
]"
>
<!-- Folders Section -->
<div class="border-b border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950">
@@ -394,9 +401,11 @@
selectedHashes.has(conversation.destination_hash),
pinnedSet.has(conversation.destination_hash),
timeAgoTick,
isRightSidebar,
]"
class="flex cursor-pointer p-2 border-l-2 relative group conversation-item"
:class="[
'flex cursor-pointer p-2 relative group conversation-item',
selectionEdgeBorderClass,
conversation.destination_hash === selectedDestinationHash
? 'bg-gray-100 dark:bg-zinc-700 border-blue-500 dark:border-blue-400'
: 'bg-white dark:bg-zinc-950 border-transparent hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-gray-200 dark:hover:border-zinc-600',
@@ -640,7 +649,10 @@
<!-- discover -->
<div
v-if="tab === 'announces'"
class="flex-1 flex flex-col bg-white dark:bg-zinc-950 border-r border-gray-200 dark:border-zinc-800 overflow-hidden min-h-0"
:class="[
'flex-1 flex flex-col bg-white dark:bg-zinc-950 border-gray-200 dark:border-zinc-800 overflow-hidden min-h-0',
edgeBorderClass,
]"
>
<!-- search -->
<div class="p-1 border-b border-gray-200 dark:border-zinc-800">
@@ -667,9 +679,11 @@
selectedDestinationHash === peer.destination_hash,
GlobalState.config.banished_effect_enabled && isBlocked(peer.destination_hash),
timeAgoTick,
isRightSidebar,
]"
class="flex cursor-pointer p-2 border-l-2 relative"
:class="[
'flex cursor-pointer p-2 relative',
selectionEdgeBorderClass,
peer.destination_hash === selectedDestinationHash
? 'bg-gray-100 dark:bg-zinc-700 border-blue-500 dark:border-blue-400'
: 'bg-white dark:bg-zinc-950 border-transparent hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-gray-200 dark:hover:border-zinc-600',
@@ -862,6 +876,11 @@ export default {
type: Boolean,
default: false,
},
sidebarPosition: {
type: String,
default: "left",
validator: (v) => v === "left" || v === "right",
},
},
emits: [
"conversation-click",
@@ -925,6 +944,24 @@ export default {
collapsedSidebarConversations() {
return this.displayedConversations.slice(0, 5);
},
isRightSidebar() {
return this.sidebarPosition === "right";
},
edgeBorderClass() {
return this.isRightSidebar ? "border-l" : "border-r";
},
selectionEdgeBorderClass() {
return this.isRightSidebar ? "border-r-2" : "border-l-2";
},
collapsedHeaderJustifyClass() {
return this.isRightSidebar ? "justify-start" : "justify-end";
},
collapsedStripChevronIcon() {
return this.isRightSidebar ? "chevron-left" : "chevron-right";
},
expandedTabBarChevronIcon() {
return this.isRightSidebar ? "chevron-right" : "chevron-left";
},
collapsedConversationIconStyle() {
return { width: "36px", height: "36px" };
},

View File

@@ -195,6 +195,19 @@
}}</span>
</span>
</label>
<label class="setting-toggle">
<Toggle
id="warn-on-stranger-links"
v-model="config.warn_on_stranger_links"
@update:model-value="onWarnOnStrangerLinksChange"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">{{ $t("app.warn_on_stranger_links") }}</span>
<span class="setting-toggle__description">{{
$t("app.warn_on_stranger_links_description")
}}</span>
</span>
</label>
</SettingsSectionBlock>
<section
@@ -832,6 +845,20 @@
</select>
</div>
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("app.messages_sidebar_position") }}
</div>
<select
v-model="config.messages_sidebar_position"
class="input-field"
@change="onMessagesSidebarPositionChange"
>
<option value="left">{{ $t("app.messages_sidebar_position_left") }}</option>
<option value="right">{{ $t("app.messages_sidebar_position_right") }}</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">
@@ -2208,6 +2235,7 @@ export default {
block_attachments_from_strangers: true,
block_all_from_strangers: false,
show_unknown_contact_banner: true,
warn_on_stranger_links: true,
banished_effect_enabled: true,
banished_text: "BANISHED",
banished_color: "#dc2626",
@@ -2221,6 +2249,7 @@ export default {
announce_search_max_fetch: 2000,
discovered_interfaces_max_return: 500,
message_font_size: 14,
messages_sidebar_position: "left",
message_icon_size: 28,
ui_transparency: 0,
ui_glass_enabled: true,
@@ -2359,6 +2388,9 @@ export default {
"app.theme",
"app.light_theme",
"app.dark_theme",
"app.messages_sidebar_position",
"app.messages_sidebar_position_left",
"app.messages_sidebar_position_right",
"app.ui_transparency",
"app.ui_glass_enabled",
"app.reset_appearance_defaults",
@@ -2708,6 +2740,16 @@ export default {
"theme"
);
},
async onMessagesSidebarPositionChange() {
const v = this.config.messages_sidebar_position === "right" ? "right" : "left";
this.config.messages_sidebar_position = v;
await this.updateConfig(
{
messages_sidebar_position: v,
},
"messages_sidebar_position"
);
},
async onMessageFontSizeChange() {
if (this.saveTimeouts.message_font_size) clearTimeout(this.saveTimeouts.message_font_size);
this.saveTimeouts.message_font_size = setTimeout(async () => {
@@ -2765,6 +2807,7 @@ export default {
},
async resetAppearanceDefaults() {
this.config.theme = "light";
this.config.messages_sidebar_position = "left";
this.config.message_font_size = 14;
this.config.message_icon_size = 28;
this.config.ui_transparency = 0;
@@ -2776,6 +2819,7 @@ export default {
await this.updateConfig(
{
theme: "light",
messages_sidebar_position: "left",
message_font_size: 14,
message_icon_size: 28,
ui_transparency: 0,
@@ -3064,6 +3108,10 @@ export default {
this.config.show_unknown_contact_banner = value;
await this.updateConfig({ show_unknown_contact_banner: value }, "stranger_protection");
},
async onWarnOnStrangerLinksChange(value) {
this.config.warn_on_stranger_links = value;
await this.updateConfig({ warn_on_stranger_links: value }, "stranger_protection");
},
async onBanishedEffectEnabledChange(value) {
this.config.banished_effect_enabled = value;
await this.updateConfig(

View File

@@ -70,9 +70,7 @@
:key="bot.id"
:class="[
'relative rounded-lg border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-3 sm:p-4',
editingBotId === bot.id
? 'pr-28 sm:pr-40'
: 'pr-10 sm:pr-12',
editingBotId === bot.id ? 'pr-28 sm:pr-40' : 'pr-10 sm:pr-12',
]"
>
<div
@@ -151,7 +149,11 @@
<div class="min-w-0 flex-1 space-y-1.5 sm:pr-2">
<div
class="flex items-center gap-1 min-w-0"
:class="editingBotId === bot.id ? 'max-w-[min(100%,14rem)] sm:max-w-[16rem]' : ''"
:class="
editingBotId === bot.id
? 'max-w-[min(100%,14rem)] sm:max-w-[16rem]'
: ''
"
>
<template v-if="editingBotId === bot.id">
<input
@@ -193,7 +195,9 @@
</button>
</template>
</div>
<div class="flex items-center gap-2 text-[11px] text-gray-600 dark:text-gray-300">
<div
class="flex items-center gap-2 text-[11px] text-gray-600 dark:text-gray-300"
>
<span
class="inline-block size-2 rounded-full shrink-0"
:class="bot.running ? 'bg-emerald-500' : 'bg-gray-400 dark:bg-gray-500'"