diff --git a/package.json b/package.json index c2f57c3..3fc7f94 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@sveltejs/vite-plugin-svelte": "^7.0.0", "@testing-library/svelte": "^5.3.1", "oidc-client-ts": "^3.5.0", + "openpgp": "^6.3.0", "spacetimedb": "^2.1.0", "svelte": "^5.55.1", "svelte-check": "^4.4.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b33200..5e1014b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: oidc-client-ts: specifier: ^3.5.0 version: 3.5.0 + openpgp: + specifier: ^6.3.0 + version: 6.3.0 spacetimedb: specifier: ^2.1.0 version: 2.1.0(react@18.3.1)(svelte@5.55.1) @@ -1779,6 +1782,10 @@ packages: resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} engines: {node: '>=18'} + openpgp@6.3.0: + resolution: {integrity: sha512-pLzCU8IgyKXPSO11eeharQkQ4GzOKNWhXq79pQarIRZEMt1/ssyr+MIuWBv1mNoenJLg04gvPx+fi4gcKZ4bag==} + engines: {node: '>= 18.0.0'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3672,6 +3679,8 @@ snapshots: dependencies: jwt-decode: 4.0.0 + openpgp@6.3.0: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 diff --git a/spacetimedb/src/lib.rs b/spacetimedb/src/lib.rs index f92d16e..e5acd5a 100644 --- a/spacetimedb/src/lib.rs +++ b/spacetimedb/src/lib.rs @@ -29,6 +29,7 @@ pub fn init(ctx: &ReducerContext) { banner_id: None, biography: Some("I am the Zep system assistant.".to_string()), status: Some("Online".to_string()), + public_key: None, }); } @@ -102,6 +103,7 @@ pub fn on_connect(ctx: &ReducerContext) { banner_id: None, biography: None, status: None, + public_key: None, }); // Minimal auto-join diff --git a/spacetimedb/src/reducers.rs b/spacetimedb/src/reducers.rs index 329db0f..e236f0f 100644 --- a/spacetimedb/src/reducers.rs +++ b/spacetimedb/src/reducers.rs @@ -383,7 +383,18 @@ pub fn set_banner(ctx: &ReducerContext, banner_id: Option) { } #[spacetimedb::reducer] -pub fn set_biography(ctx: &ReducerContext, biography: Option) { +pub fn update_public_key(ctx: &ReducerContext, public_key: Option) { + if let Some(mut user) = ctx.db.user().identity().find(ctx.sender()) { + user.public_key = public_key; + ctx.db.user().identity().update(user); + } else { + panic!("User not found"); + } +} + +#[spacetimedb::reducer] +pub fn set_biography( +ctx: &ReducerContext, biography: Option) { let mut user = ctx .db .user() @@ -849,13 +860,14 @@ pub fn create_thread_with_message( parent_message_id: u64, text: String, image_ids: Vec, + is_encrypted: bool, ) { if text.trim().is_empty() && image_ids.is_empty() { panic!("Messages must not be empty"); } - if !text.is_empty() { + if !text.is_empty() && !is_encrypted { validate_message_length(&ctx.db, &text).expect("Message too long"); } @@ -899,6 +911,7 @@ pub fn create_thread_with_message( thread_name: None, thread_reply_count: 0, edited: false, + is_encrypted, }); let seq_id = get_next_seq_id(&ctx.db, channel_id); @@ -926,13 +939,15 @@ pub fn create_thread_with_message( thread_id: Some(t.id), sent: ctx.timestamp, seq_id, - reactions: Vec::new(), - image_ids, + reactions: msg.reactions, + image_ids: msg.image_ids, thread_name: None, thread_reply_count: 0, edited: false, + is_encrypted, }); + // Update parent message metadata if let Some(mut parent_msg) = ctx.db.message().id().find(parent_message_id) { parent_msg.thread_name = Some(name.clone()); @@ -969,13 +984,14 @@ pub fn send_message( channel_id: u64, thread_id: Option, image_ids: Vec, + is_encrypted: bool, ) { if text.trim().is_empty() && image_ids.is_empty() { panic!("Messages must not be empty"); } - if !text.is_empty() { + if !text.is_empty() && !is_encrypted { validate_message_length(&ctx.db, &text).expect("Message too long"); } @@ -1002,6 +1018,7 @@ pub fn send_message( thread_name: None, thread_reply_count: 0, edited: false, + is_encrypted, }); let seq_id = get_next_seq_id(&ctx.db, channel_id); @@ -1029,13 +1046,15 @@ pub fn send_message( thread_id, sent: ctx.timestamp, seq_id, - reactions: Vec::new(), - image_ids, + reactions: msg.reactions, + image_ids: msg.image_ids, thread_name: None, thread_reply_count: 0, edited: false, + is_encrypted, }); + // If it's a thread message, update parent message metadata if let Some(tid) = thread_id { if let Some(thread) = ctx.db.thread().id().find(tid) { @@ -1121,6 +1140,7 @@ pub fn bootstrap_sequences(ctx: &ReducerContext) { thread_name: msg.thread_name.clone(), thread_reply_count: msg.thread_reply_count, edited: msg.edited, + is_encrypted: msg.is_encrypted, }); } } diff --git a/spacetimedb/src/tables.rs b/spacetimedb/src/tables.rs index a4c3844..635d752 100644 --- a/spacetimedb/src/tables.rs +++ b/spacetimedb/src/tables.rs @@ -20,6 +20,7 @@ pub struct User { pub banner_id: Option, pub biography: Option, pub status: Option, + pub public_key: Option, } #[derive(spacetimedb::SpacetimeType, Clone, Debug)] @@ -210,6 +211,7 @@ pub struct Message { pub thread_name: Option, pub thread_reply_count: u32, pub edited: bool, + pub is_encrypted: bool, } #[spacetimedb::table(accessor = custom_emoji, public)] @@ -262,6 +264,7 @@ pub struct RecentMessage { pub thread_name: Option, pub thread_reply_count: u32, pub edited: bool, + pub is_encrypted: bool, } #[spacetimedb::table(accessor = system_configuration, public)] diff --git a/spacetimedb/src/utils.rs b/spacetimedb/src/utils.rs index eeac4a6..9b71203 100644 --- a/spacetimedb/src/utils.rs +++ b/spacetimedb/src/utils.rs @@ -384,6 +384,7 @@ pub fn internal_send_message(db: &Local, sender: Identity, channel_id: u64, text thread_name: None, thread_reply_count: 0, edited: false, + is_encrypted: false, }); let seq_id = get_next_seq_id(db, channel_id); @@ -408,6 +409,7 @@ pub fn internal_send_message(db: &Local, sender: Identity, channel_id: u64, text thread_reply_count: msg.thread_reply_count, edited: msg.edited, server_id: 0, // DMs have server_id 0 + is_encrypted: false, }); let limit = get_recent_message_limit(db); diff --git a/spacetimedb/src/views.rs b/spacetimedb/src/views.rs index acb8305..19c08cf 100644 --- a/spacetimedb/src/views.rs +++ b/spacetimedb/src/views.rs @@ -24,6 +24,7 @@ pub struct VisibleMessageRow { pub thread_name: Option, pub thread_reply_count: u32, pub edited: bool, + pub is_encrypted: bool, } #[derive(spacetimedb::SpacetimeType)] @@ -282,6 +283,7 @@ pub fn visible_scrollback_messages(ctx: &ViewContext) -> Vec thread_name: msg.thread_name.clone(), thread_reply_count: msg.thread_reply_count, edited: msg.edited, + is_encrypted: msg.is_encrypted, }); } } diff --git a/src/App.css b/src/App.css index 5796bb8..05cdec3 100644 --- a/src/App.css +++ b/src/App.css @@ -612,6 +612,21 @@ body { z-index: 10; } +.encryption-indicator { + display: flex; + align-items: center; + gap: 4px; + background-color: rgba(88, 101, 242, 0.1); + border: 1px solid rgba(88, 101, 242, 0.3); + color: var(--brand); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.65rem; + font-weight: 700; + text-transform: uppercase; + margin-left: 4px; +} + .message-list { flex: 1; overflow-y: auto; diff --git a/src/chat/ChatContainer.svelte b/src/chat/ChatContainer.svelte index 82d13be..10ccea2 100644 --- a/src/chat/ChatContainer.svelte +++ b/src/chat/ChatContainer.svelte @@ -148,8 +148,14 @@ {@const otherIdentity = dm.sender.toHexString() === myIdHex ? dm.recipient : dm.sender} {@const recipient = chat.users.find(u => u.identity.toHexString() === otherIdentity.toHexString())} {#if recipient} - +

{recipient.name || "Unknown User"}

+ {#if chat.getRecipientPublicKey(otherIdentity)} +
+ + Encryption Available +
+ {/if} {/if} {/if} {:else} diff --git a/src/chat/components/Avatar.svelte b/src/chat/components/Avatar.svelte index e84391e..cfd5312 100644 --- a/src/chat/components/Avatar.svelte +++ b/src/chat/components/Avatar.svelte @@ -1,7 +1,6 @@
+ + {#if showEncryptionToggle} + + {/if}
@@ -522,6 +544,27 @@ opacity: 0.3; } + .encryption-toggle-btn { + background: none; + border: none; + color: var(--interactive-normal); + cursor: pointer; + padding: 10px 0 10px 16px; + font-size: 1.1rem; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + } + + .encryption-toggle-btn:hover { + color: var(--interactive-hover); + } + + .encryption-toggle-btn.active { + color: var(--brand); + } + .message-input-inner { display: flex; flex-direction: column; diff --git a/src/chat/components/MessageItem.svelte b/src/chat/components/MessageItem.svelte index 1b8178c..967d9ac 100644 --- a/src/chat/components/MessageItem.svelte +++ b/src/chat/components/MessageItem.svelte @@ -133,6 +133,25 @@ horizontal: 'left' | 'right' } | null>(null); + // E2EE Decryption + let decryptedText = $state(null); + let isDecrypting = $state(false); + + $effect(() => { + if (msg.isEncrypted && !decryptedText && !isDecrypting && chat.isEncryptionReady) { + isDecrypting = true; + chat.decrypt(msg.text).then(result => { + decryptedText = result || "⚠️ Decryption failed (Missing or invalid key)"; + isDecrypting = false; + if (onContentLoad) tick().then(onContentLoad); + }).catch(e => { + console.error("[MessageItem] Decryption error", e); + decryptedText = "⚠️ Decryption error"; + isDecrypting = false; + }); + } + }); + let tooltip = $state<{ text: string; x: number; @@ -270,7 +289,7 @@ -
+
{#if chat.isFullyAuthenticated} {#if msg.sender.isEqual(chat.identity!)} @@ -393,13 +412,36 @@
{:else} - + {#if msg.isEncrypted} + {#if isDecrypting} +
+ Decrypting... +
+ {:else if decryptedText} +
+ + +
+ {:else} +
+ Encrypted message. Set up your keys in settings to unlock. +
+ {/if} + {:else} + + {/if} {/if}
@@ -522,6 +564,15 @@ background-color: rgba(0, 0, 0, 0.05); } + .message-item.is-encrypted { + border-left: 2px solid var(--brand); + background-color: rgba(88, 101, 242, 0.03); + } + + .message-item.is-encrypted:hover { + background-color: rgba(88, 101, 242, 0.06); + } + .message-item.active { background-color: var(--background-modifier-hover); } @@ -750,6 +801,37 @@ animation: pulse 1.5s infinite ease-in-out; } + .locked-message, .decrypting-placeholder { + color: var(--text-muted); + font-size: 0.9rem; + padding: 4px 8px; + background-color: var(--background-secondary); + border-radius: 4px; + display: flex; + align-items: center; + gap: 8px; + margin: 4px 0; + } + + .locked-message i { + color: var(--status-danger); + } + + .encrypted-badge-wrapper { + position: relative; + display: flex; + align-items: flex-start; + gap: 8px; + } + + .encrypted-badge-wrapper > i.fa-lock { + font-size: 0.7rem; + color: var(--brand); + opacity: 0.6; + margin-top: 6px; + flex-shrink: 0; + } + @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 0.8; } diff --git a/src/chat/components/SettingsPanel.svelte b/src/chat/components/SettingsPanel.svelte index 1d11d0a..bf147df 100644 --- a/src/chat/components/SettingsPanel.svelte +++ b/src/chat/components/SettingsPanel.svelte @@ -10,6 +10,7 @@ import CustomizationSettings from "./settings/CustomizationSettings.svelte"; import AudioSettings from "./settings/AudioSettings.svelte"; import ScreenSharingSettings from "./settings/ScreenSharingSettings.svelte"; + import SecuritySettings from "./settings/SecuritySettings.svelte"; let { onClose, currentUser }: { onClose: () => void, currentUser: Types.User | undefined } = $props(); @@ -133,6 +134,7 @@ const categories = [ { id: "account", name: "My Account", icon: "fas fa-user" }, { id: "customization", name: "Customization", icon: "fas fa-palette" }, + { id: "security", name: "Security & Privacy", icon: "fas fa-shield-alt" }, { id: "voice", name: "Voice & Video", icon: "fas fa-microphone" }, { id: "screen", name: "Screen Sharing", icon: "fas fa-desktop" }, ]; @@ -203,6 +205,8 @@ /> {:else if activeCategory === "customization"} + {:else if activeCategory === "security"} + {:else if activeCategory === "voice"} {:else if activeCategory === "screen"} diff --git a/src/chat/components/settings/SecuritySettings.svelte b/src/chat/components/settings/SecuritySettings.svelte new file mode 100644 index 0000000..f34807d --- /dev/null +++ b/src/chat/components/settings/SecuritySettings.svelte @@ -0,0 +1,252 @@ + + +
+
+

Security & Privacy

+

Manage your end-to-end encryption keys.

+
+ +
+

End-to-End Encryption (GPG)

+ + {#if chat.isEncryptionReady} +
+
+ +
+
+
E2EE is Active
+
Your messages in Direct Messages will be automatically encrypted.
+
+
+ +
+ +
+
{chat.myPublicKey}
+ +
+

Share this key with others so they can send you encrypted messages (automatically handled by Zep).

+
+ +
+

Danger Zone

+

Generating a new key will prevent you from reading old encrypted messages.

+ +
+ {:else} +
+
+ +
+
+
E2EE is Not Set Up
+
You need to generate a keypair to enable private messaging encryption.
+
+
+ +
+

Generate New Keypair

+
+ + +
+
+ + +
+ +
+ {/if} +
+
+ + diff --git a/src/chat/services/chat.svelte.ts b/src/chat/services/chat.svelte.ts index 8f31a7f..9118c20 100644 --- a/src/chat/services/chat.svelte.ts +++ b/src/chat/services/chat.svelte.ts @@ -12,6 +12,7 @@ import { VoiceService } from "./voice.svelte"; import { AccountService } from "./account.svelte"; import { ServerManagementService } from "./server-management.svelte"; import { DirectMessagingService } from "./direct-messaging.svelte"; +import { EncryptionService } from "./encryption.svelte"; export class ChatService { #db: DatabaseService; @@ -23,6 +24,7 @@ export class ChatService { #account: AccountService; #server: ServerManagementService; #dm: DirectMessagingService; + #encryption: EncryptionService; identity = $state(null); #blobUrls = new Map(); @@ -45,6 +47,33 @@ export class ChatService { this.#account = new AccountService(); this.#server = new ServerManagementService(); this.#dm = new DirectMessagingService(this.#db, this.#nav, () => this.identity); + this.#encryption = new EncryptionService(this.#db, () => this.identity); + + this.#msg.onSendMessage = async (text, channelId) => { + // Check if user has manually enabled encryption for this message/channel + if (!this.#msg.encryptionOptIn.has(channelId.toString())) { + return { text, isEncrypted: false }; + } + + // Check if this is a DM channel + const dm = this.#db.directMessages.find(d => d.channelId === channelId); + if (dm) { + const myIdHex = this.identity?.toHexString(); + const otherIdentity = dm.sender.toHexString() === myIdHex ? dm.recipient : dm.sender; + const recipientPubKey = this.getRecipientPublicKey(otherIdentity); + + if (recipientPubKey && this.isEncryptionReady) { + console.log(`[ChatService] Encrypting message for DM ${channelId}`); + try { + const encrypted = await this.encrypt(text, recipientPubKey); + return { text: encrypted, isEncrypted: true }; + } catch (e) { + console.error("[ChatService] Encryption failed, sending as plain text", e); + } + } + } + return { text, isEncrypted: false }; + }; // Session-only image processing: creates Blob URLs directly from Database data. // This ditched the persistent IndexedDB cache to prevent stale data between reloads. @@ -334,6 +363,38 @@ export class ChatService { return this.#msg.isMessagesReady; } + get isEncryptionReady() { + return this.#encryption.isKeyReady; + } + + isEncryptionEnabledForChannel = (channelId: bigint) => + this.#msg.encryptionOptIn.has(channelId.toString()); + + toggleEncryptionForChannel = (channelId: bigint) => { + const chanStr = channelId.toString(); + if (this.#msg.encryptionOptIn.has(chanStr)) { + this.#msg.encryptionOptIn.delete(chanStr); + } else { + this.#msg.encryptionOptIn.add(chanStr); + } + }; + + get myPublicKey() { + return this.#encryption.publicKey; + } + + generateEncryptionKey = (name: string, email: string) => + this.#encryption.generateKeypair(name, email); + + getRecipientPublicKey = (recipientIdentity: Identity) => + this.#encryption.getRecipientPublicKey(recipientIdentity); + + encrypt = (text: string, pubKey: string) => + this.#encryption.encrypt(text, pubKey); + + decrypt = (text: string) => + this.#encryption.decrypt(text); + get maxMessageLength() { const config = this.#db.systemConfiguration.find(c => c.key === "max_message_length"); return config ? parseInt(config.value) : 262144; diff --git a/src/chat/services/direct-messaging.svelte.ts b/src/chat/services/direct-messaging.svelte.ts index ecf4cc0..7f35e53 100644 --- a/src/chat/services/direct-messaging.svelte.ts +++ b/src/chat/services/direct-messaging.svelte.ts @@ -41,11 +41,11 @@ export class DirectMessagingService { } } else { // No session, create it and wait for application to focus - this.#openDirectMessageReducer({ recipient }).onApplied(() => { + this.#openDirectMessageReducer({ recipient }).then(() => { // Find the newly created DM const newDm = this.#db.directMessages.find(dm => (dm.sender.isEqual(identity) && dm.recipient.isEqual(recipient)) || - (dm.sender.isEqual(recipient) && dm.identity.isEqual(identity)) // fallback + (dm.sender.isEqual(recipient) && dm.recipient.isEqual(identity)) ); if (newDm) { this.#nav.activeServerId = null; diff --git a/src/chat/services/encryption.svelte.ts b/src/chat/services/encryption.svelte.ts new file mode 100644 index 0000000..d79aee4 --- /dev/null +++ b/src/chat/services/encryption.svelte.ts @@ -0,0 +1,125 @@ +import * as openpgp from 'openpgp'; +import { useReducer } from "spacetimedb/svelte"; +import { reducers } from "../../module_bindings"; +import type { DatabaseService } from "./database.svelte"; +import type { Identity } from "spacetimedb"; + +const PRIVATE_KEY_PREFIX = "zep_private_key_"; + +export class EncryptionService { + #db: DatabaseService; + #identity: () => Identity | null; + #updatePublicKey = useReducer(reducers.updatePublicKey); + + isKeyReady = $state(false); + publicKey = $state(null); + #privateKey: string | null = null; + + constructor(db: DatabaseService, identity: () => Identity | null) { + this.#db = db; + this.#identity = identity; + + // Load key from storage if available + $effect(() => { + const id = this.#identity(); + if (!id) { + this.isKeyReady = false; + this.publicKey = null; + this.#privateKey = null; + return; + } + + const idHex = id.toHexString(); + const storedKey = localStorage.getItem(PRIVATE_KEY_PREFIX + idHex); + + if (storedKey) { + this.#privateKey = storedKey; + // Verify public key matches what's in DB + const user = this.#db.users.find(u => u.identity.isEqual(id)); + if (user?.publicKey) { + this.publicKey = user.publicKey; + this.isKeyReady = true; + } else { + // If we have a private key locally but no public key in DB, + // re-sync public key + this.extractPublicKeyFromPrivate(storedKey).then(pub => { + if (pub) { + this.publicKey = pub; + this.#updatePublicKey({ publicKey: pub }); + this.isKeyReady = true; + } + }); + } + } + }); + } + + async generateKeypair(name: string, email: string) { + const id = this.#identity(); + if (!id) return; + + const { privateKey, publicKey } = await openpgp.generateKey({ + type: 'rsa', + rsaBits: 4096, + userIDs: [{ name, email }], + }); + + const idHex = id.toHexString(); + localStorage.setItem(PRIVATE_KEY_PREFIX + idHex, privateKey); + this.#privateKey = privateKey; + this.publicKey = publicKey; + this.isKeyReady = true; + + this.#updatePublicKey({ publicKey }); + } + + async extractPublicKeyFromPrivate(armoredPrivKey: string): Promise { + try { + const privateKey = await openpgp.readPrivateKey({ armoredKey: armoredPrivKey }); + return privateKey.toPublic().armor(); + } catch (e) { + console.error("Failed to extract public key", e); + return null; + } + } + + async encrypt(text: string, recipientPublicKeyArmored: string): Promise { + const keys = []; + keys.push(await openpgp.readKey({ armoredKey: recipientPublicKeyArmored })); + + // Also encrypt for ourselves if we have a private key, so we can read our sent messages + if (this.#privateKey) { + const myPrivKey = await openpgp.readPrivateKey({ armoredKey: this.#privateKey }); + keys.push(myPrivKey.toPublic()); + } + + const message = await openpgp.createMessage({ text }); + const encrypted = await openpgp.encrypt({ + message, + encryptionKeys: keys, + }); + return encrypted as string; + } + + async decrypt(armoredMessage: string): Promise { + if (!this.#privateKey) return null; + + try { + const privateKey = await openpgp.readPrivateKey({ armoredKey: this.#privateKey }); + const message = await openpgp.readMessage({ armoredMessage }); + const { data: decrypted } = await openpgp.decrypt({ + message, + decryptionKeys: privateKey, + }); + return decrypted as string; + } catch (e) { + console.error("Decryption failed", e); + return null; + } + } + + getRecipientPublicKey(recipientIdentity: Identity): string | null { + const user = this.#db.users.find(u => u.identity.isEqual(recipientIdentity)); + return user?.publicKey || null; + } +} diff --git a/src/chat/services/messaging.svelte.ts b/src/chat/services/messaging.svelte.ts index 192a6d2..014e060 100644 --- a/src/chat/services/messaging.svelte.ts +++ b/src/chat/services/messaging.svelte.ts @@ -25,6 +25,12 @@ export class MessagingService { #extendSubscriptionReducer: any; #clearUploadStatusReducer: any; + /** + * Hook for ChatService to intercept and potentially encrypt messages. + * returns { text: string, isEncrypted: boolean } + */ + onSendMessage?: (text: string, channelId: bigint) => Promise<{ text: string, isEncrypted: boolean }>; + // Internal reactive state from SpacetimeDB #mySubscriptions = $state([]); @@ -37,6 +43,7 @@ export class MessagingService { isLoadingMore = $state(false); #readyChannels = new SvelteSet(); isGlobalSyncDone = $state(false); + encryptionOptIn = $state(new SvelteSet()); get isMessagesReady() { const cid = this.#nav.activeChannelId; @@ -283,19 +290,43 @@ export class MessagingService { } }; - handleSendMessage = (text: string, threadId?: bigint, imageIds: bigint[] = []) => { + handleSendMessage = async (text: string, threadId?: bigint, imageIds: bigint[] = []) => { if (this.#nav.activeChannelId) { + let finalParams = { text, isEncrypted: false }; + if (this.onSendMessage) { + finalParams = await this.onSendMessage(text, this.#nav.activeChannelId); + } + if (threadId) { - this.#sendMessageReducer({ channelId: this.#nav.activeChannelId, text, threadId, imageIds }); + this.#sendMessageReducer({ + channelId: this.#nav.activeChannelId, + text: finalParams.text, + threadId, + imageIds, + isEncrypted: finalParams.isEncrypted + }); } else if (this.#nav.pendingThreadParentMessageId) { const parentMsgId = this.#nav.pendingThreadParentMessageId; const parentMsg = this.allMessages.find((m) => m.id === parentMsgId); const name = (parentMsg?.text && parentMsg.text.trim().length > 0) ? parentMsg.text.substring(0, 32) : "New Thread"; - this.#createThreadWithMessageReducer({ name, channelId: this.#nav.activeChannelId, parentMessageId: parentMsgId, text, imageIds }); + this.#createThreadWithMessageReducer({ + name, + channelId: this.#nav.activeChannelId, + parentMessageId: parentMsgId, + text: finalParams.text, + imageIds, + isEncrypted: finalParams.isEncrypted + }); } else { - this.#sendMessageReducer({ channelId: this.#nav.activeChannelId, text, threadId: undefined, imageIds }); + this.#sendMessageReducer({ + channelId: this.#nav.activeChannelId, + text: finalParams.text, + threadId: undefined, + imageIds, + isEncrypted: finalParams.isEncrypted + }); } } }; diff --git a/utils/stress.ts b/utils/stress.ts index 5a3cd9e..aedb6ff 100644 --- a/utils/stress.ts +++ b/utils/stress.ts @@ -63,6 +63,7 @@ const startStressTest = () => { channelId, threadId: undefined, imageIds: [], + isEncrypted: false, }); if (msgCount % 100 === 0) {