mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-10-23 18:02:43 +00:00
Add stranger protection features and related UI enhancements
- Updated NotificationBell to handle additional notification types for missed calls and voicemails. - Added buttons in CallPage for messaging and calling contacts directly from call history. - Enhanced ContactsPage with context menu options for messaging and calling contacts. - Introduced a stranger trust banner in ConversationViewer to inform users about non-contacts and provide options to add them. - Implemented settings in SettingsPage to block attachments and messages from strangers, with corresponding configuration updates.
This commit is contained in:
5 files changed
+208
-3
No files matched your search
@@ -319,9 +319,12 @@ export default {
|
||||
},
|
||||
async onWebsocketMessage(message) {
|
||||
const json = JSON.parse(message.data);
|
||||
if (json.type === "lxmf.delivery") {
|
||||
if (
|
||||
json.type === "lxmf.delivery" ||
|
||||
json.type === "telephone_missed_call" ||
|
||||
json.type === "new_voicemail"
|
||||
) {
|
||||
await this.loadNotifications();
|
||||
// If dropdown is open, mark new notifications as viewed
|
||||
if (this.isDropdownOpen) {
|
||||
await this.markNotificationsAsViewed();
|
||||
}
|
||||
|
||||
@@ -777,6 +777,17 @@
|
||||
>
|
||||
<MaterialDesignIcon icon-name="account-remove" class="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5 rounded-lg text-gray-400 hover:text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all shrink-0"
|
||||
:title="$t('contacts.send_message')"
|
||||
@click="openMessageFromHistory(entry)"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
icon-name="message-text-outline"
|
||||
class="size-4"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-3 py-1 bg-blue-600 text-white rounded-lg text-[10px] font-bold hover:bg-blue-500 transition-all shadow-md shadow-blue-500/10 shrink-0"
|
||||
@@ -2734,6 +2745,11 @@ export default {
|
||||
};
|
||||
this.isContactModalOpen = true;
|
||||
},
|
||||
openMessageFromHistory(entry) {
|
||||
const hash = entry.remote_destination_hash || entry.remote_identity_hash;
|
||||
if (!hash) return;
|
||||
this.$router.push({ name: "messages", params: { destinationHash: hash } });
|
||||
},
|
||||
async getHistory(loadMore = false) {
|
||||
try {
|
||||
if (!loadMore) {
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<div
|
||||
v-for="contact in contacts"
|
||||
:key="contact.id"
|
||||
class="rounded-2xl border border-gray-100 dark:border-zinc-800 bg-white/70 dark:bg-zinc-900/50 px-4 py-3 flex items-center gap-3 hover:border-blue-300 dark:hover:border-blue-700 transition-colors cursor-default"
|
||||
class="group rounded-2xl border border-gray-100 dark:border-zinc-800 bg-white/70 dark:bg-zinc-900/50 px-4 py-3 flex items-center gap-3 hover:border-blue-300 dark:hover:border-blue-700 transition-colors cursor-default"
|
||||
@contextmenu.prevent="openContextMenu($event, contact)"
|
||||
>
|
||||
<div class="flex-shrink-0">
|
||||
@@ -84,6 +84,24 @@
|
||||
{{ contact.lxmf_address || contact.remote_identity_hash }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5 rounded-lg text-gray-500 dark:text-zinc-400 hover:bg-blue-100 dark:hover:bg-blue-900/40 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
||||
:title="$t('contacts.send_message')"
|
||||
@click.stop="openConversation(contact)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="message-text-outline" class="size-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5 rounded-lg text-gray-500 dark:text-zinc-400 hover:bg-green-100 dark:hover:bg-green-900/40 hover:text-green-600 dark:hover:text-green-400 transition-colors"
|
||||
:title="$t('contacts.call_contact')"
|
||||
@click.stop="callContact(contact)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="phone-outline" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5 rounded-lg text-gray-500 dark:text-zinc-400 hover:bg-gray-100 dark:hover:bg-zinc-800 hover:text-gray-700 dark:hover:text-zinc-200 transition-colors"
|
||||
@@ -114,6 +132,15 @@
|
||||
class="fixed z-[210] min-w-48 rounded-xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 shadow-xl"
|
||||
:style="{ top: `${contextMenu.y}px`, left: `${contextMenu.x}px` }"
|
||||
>
|
||||
<button type="button" class="context-item" @click="openConversation(contextMenu.contact)">
|
||||
<MaterialDesignIcon icon-name="message-text-outline" class="size-4" />
|
||||
{{ $t("contacts.send_message") }}
|
||||
</button>
|
||||
<button type="button" class="context-item" @click="callContact(contextMenu.contact)">
|
||||
<MaterialDesignIcon icon-name="phone-outline" class="size-4" />
|
||||
{{ $t("contacts.call_contact") }}
|
||||
</button>
|
||||
<div class="border-t border-gray-100 dark:border-zinc-800 my-1"></div>
|
||||
<button type="button" class="context-item" @click="shareContact(contextMenu.contact)">
|
||||
<MaterialDesignIcon icon-name="share-variant" class="size-4" />
|
||||
{{ $t("contacts.share_contact") }}
|
||||
@@ -122,6 +149,7 @@
|
||||
<MaterialDesignIcon icon-name="content-copy" class="size-4" />
|
||||
{{ $t("contacts.copy_contact_uri") }}
|
||||
</button>
|
||||
<div class="border-t border-gray-100 dark:border-zinc-800 my-1"></div>
|
||||
<button
|
||||
type="button"
|
||||
class="context-item text-red-600 dark:text-red-400"
|
||||
@@ -531,6 +559,19 @@ export default {
|
||||
ToastUtils.error(this.$t("contacts.failed_remove_contact"));
|
||||
}
|
||||
},
|
||||
openConversation(contact) {
|
||||
this.closeContextMenu();
|
||||
const hash = contact.remote_destination_hash || contact.lxmf_address || contact.remote_identity_hash;
|
||||
if (!hash) return;
|
||||
this.$router.push({ name: "messages", params: { destinationHash: hash } });
|
||||
},
|
||||
callContact(contact) {
|
||||
this.closeContextMenu();
|
||||
const hash =
|
||||
contact.remote_telephony_hash || contact.remote_destination_hash || contact.remote_identity_hash;
|
||||
if (!hash) return;
|
||||
this.$router.push({ name: "call", query: { destination_hash: hash, tab: "phone" } });
|
||||
},
|
||||
openContextMenu(event, contact) {
|
||||
this.contextMenu.visible = true;
|
||||
this.contextMenu.contact = contact;
|
||||
|
||||
@@ -372,6 +372,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- stranger trust banner -->
|
||||
<div
|
||||
v-if="isStrangerPeer && !strangerBannerDismissed"
|
||||
class="mx-3 mt-2 mb-0 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-300 dark:border-amber-700 rounded-lg flex items-center gap-3 text-sm"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 9v3.75m0 3.75h.008v-.008H12v.008Zm9.303-5.626a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="flex-1 text-amber-900 dark:text-amber-200">
|
||||
{{ $t("messages.stranger_banner_text") }}
|
||||
</span>
|
||||
<button
|
||||
class="px-3 py-1 text-xs font-medium rounded-md bg-amber-600 hover:bg-amber-700 text-white transition-colors"
|
||||
@click="addStrangerAsContact"
|
||||
>
|
||||
{{ $t("messages.add_to_contacts") }}
|
||||
</button>
|
||||
<button
|
||||
class="px-2 py-1 text-xs text-amber-600 dark:text-amber-400 hover:text-amber-800 dark:hover:text-amber-200 transition-colors"
|
||||
@click="strangerBannerDismissed = true"
|
||||
>
|
||||
{{ $t("messages.dismiss") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- chat items -->
|
||||
<div
|
||||
id="messages"
|
||||
@@ -1739,6 +1775,8 @@ export default {
|
||||
expandedMessageInfo: null,
|
||||
imageModalUrl: null,
|
||||
isSelectedPeerBlocked: false,
|
||||
isStrangerPeer: false,
|
||||
strangerBannerDismissed: false,
|
||||
isGeneratingPaperMessage: false,
|
||||
generatedPaperMessageUri: null,
|
||||
isPaperMessageResultModalOpen: false,
|
||||
@@ -1991,6 +2029,8 @@ export default {
|
||||
this.saveDraft(oldPeer.destination_hash);
|
||||
}
|
||||
this.checkIfSelectedPeerBlocked();
|
||||
this.strangerBannerDismissed = false;
|
||||
this.checkIfStrangerPeer();
|
||||
this.initialLoad();
|
||||
if (newPeer) {
|
||||
this.loadDraft(newPeer.destination_hash);
|
||||
@@ -2030,6 +2070,9 @@ export default {
|
||||
// listen for compose new message event
|
||||
GlobalEmitter.on("compose-new-message", this.onComposeNewMessageEvent);
|
||||
|
||||
// listen for contact updates to refresh stranger banner
|
||||
GlobalEmitter.on("contact-updated", this.onContactUpdatedForBanner);
|
||||
|
||||
// check translator
|
||||
this.checkTranslator();
|
||||
|
||||
@@ -2049,6 +2092,7 @@ export default {
|
||||
// stop listening for websocket messages
|
||||
WebSocketConnection.off("message", this.onWebsocketMessage);
|
||||
GlobalEmitter.off("compose-new-message", this.onComposeNewMessageEvent);
|
||||
GlobalEmitter.off("contact-updated", this.onContactUpdatedForBanner);
|
||||
if (this.propagationStatusInterval) {
|
||||
clearInterval(this.propagationStatusInterval);
|
||||
}
|
||||
@@ -2177,6 +2221,54 @@ export default {
|
||||
(b) => b.destination_hash === this.selectedPeer.destination_hash
|
||||
);
|
||||
},
|
||||
onContactUpdatedForBanner(data) {
|
||||
if (this.selectedPeer?.destination_hash === data?.remote_identity_hash) {
|
||||
this.isStrangerPeer = false;
|
||||
this.strangerBannerDismissed = true;
|
||||
}
|
||||
},
|
||||
async checkIfStrangerPeer() {
|
||||
if (!this.selectedPeer) {
|
||||
this.isStrangerPeer = false;
|
||||
return;
|
||||
}
|
||||
if (this.selectedPeer.is_contact || this.selectedPeer.contact_image) {
|
||||
this.isStrangerPeer = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await window.axios.get(
|
||||
`/api/v1/telephone/contacts/check/${this.selectedPeer.destination_hash}`
|
||||
);
|
||||
this.isStrangerPeer = !response.data.is_contact;
|
||||
} catch {
|
||||
this.isStrangerPeer = !this.selectedPeer.is_contact && !this.selectedPeer.contact_image;
|
||||
}
|
||||
},
|
||||
async addStrangerAsContact() {
|
||||
if (!this.selectedPeer) return;
|
||||
try {
|
||||
const displayName =
|
||||
this.selectedPeer.custom_display_name ?? this.selectedPeer.display_name ?? "Unknown";
|
||||
await fetch(`/api/v1/telephone/contacts`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
remote_identity_hash: this.selectedPeer.destination_hash,
|
||||
lxmf_address: this.selectedPeer.destination_hash,
|
||||
name: displayName,
|
||||
}),
|
||||
});
|
||||
this.isStrangerPeer = false;
|
||||
this.strangerBannerDismissed = true;
|
||||
GlobalEmitter.emit("contact-updated", {
|
||||
remote_identity_hash: this.selectedPeer.destination_hash,
|
||||
});
|
||||
this.$emit("reload-conversations");
|
||||
} catch (e) {
|
||||
console.error("Failed to add contact:", e);
|
||||
}
|
||||
},
|
||||
loadDraft(destinationHash) {
|
||||
try {
|
||||
const drafts = JSON.parse(localStorage.getItem("meshchat.drafts") || "{}");
|
||||
|
||||
@@ -142,6 +142,49 @@
|
||||
<!-- settings grid -->
|
||||
<div v-show="hasSearchResults" class="columns-1 lg:columns-2 gap-4 space-y-4">
|
||||
<!-- Banishment -->
|
||||
<section
|
||||
v-show="matchesSearch('stranger', 'attachments', 'trust', 'block')"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Security</div>
|
||||
<h2>{{ $t("app.stranger_protection") }}</h2>
|
||||
<p>{{ $t("app.stranger_protection_description") }}</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="glass-card__body space-y-4">
|
||||
<label class="setting-toggle">
|
||||
<Toggle
|
||||
id="block-attachments-from-strangers"
|
||||
v-model="config.block_attachments_from_strangers"
|
||||
@update:model-value="onStrangerAttachmentBlockChange"
|
||||
/>
|
||||
<span class="setting-toggle__label">
|
||||
<span class="setting-toggle__title">{{
|
||||
$t("app.block_stranger_attachments")
|
||||
}}</span>
|
||||
<span class="setting-toggle__description">{{
|
||||
$t("app.block_stranger_attachments_description")
|
||||
}}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="setting-toggle">
|
||||
<Toggle
|
||||
id="block-all-from-strangers"
|
||||
v-model="config.block_all_from_strangers"
|
||||
@update:model-value="onBlockAllFromStrangersChange"
|
||||
/>
|
||||
<span class="setting-toggle__label">
|
||||
<span class="setting-toggle__title">{{ $t("app.block_all_from_strangers") }}</span>
|
||||
<span class="setting-toggle__description">{{
|
||||
$t("app.block_all_from_strangers_description")
|
||||
}}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-show="matchesSearch(...sectionKeywords.banishment)"
|
||||
class="glass-card break-inside-avoid"
|
||||
@@ -1630,6 +1673,8 @@ export default {
|
||||
lxmf_preferred_propagation_node_auto_select: null,
|
||||
archives_max_storage_gb: 1,
|
||||
backup_max_count: 5,
|
||||
block_attachments_from_strangers: true,
|
||||
block_all_from_strangers: false,
|
||||
banished_effect_enabled: true,
|
||||
banished_text: "BANISHED",
|
||||
banished_color: "#dc2626",
|
||||
@@ -2147,6 +2192,14 @@ export default {
|
||||
);
|
||||
}, 1000);
|
||||
},
|
||||
async onStrangerAttachmentBlockChange(value) {
|
||||
this.config.block_attachments_from_strangers = value;
|
||||
await this.updateConfig({ block_attachments_from_strangers: value }, "stranger_protection");
|
||||
},
|
||||
async onBlockAllFromStrangersChange(value) {
|
||||
this.config.block_all_from_strangers = value;
|
||||
await this.updateConfig({ block_all_from_strangers: value }, "stranger_protection");
|
||||
},
|
||||
async onBanishedEffectEnabledChange(value) {
|
||||
this.config.banished_effect_enabled = value;
|
||||
await this.updateConfig(
|
||||
|
||||
Reference in new issue
Block a user