From dc74f35573ca53de6fb7dc64181146f8c0281528 Mon Sep 17 00:00:00 2001 From: Attila Gazso <230163+agazso@users.noreply.github.com> Date: Tue, 19 Sep 2023 19:35:41 +0200 Subject: [PATCH] feat: safe waku (#400) --- src/lib/adapters/waku/index.ts | 212 ++++------------ src/lib/adapters/waku/safe-waku.ts | 307 ++++++++++++++++++++++++ src/lib/adapters/waku/waku.ts | 29 ++- src/lib/stores/chat.ts | 3 +- src/routes/chat/[id]/+page.svelte | 8 +- src/routes/group/chat/[id]/+page.svelte | 9 +- 6 files changed, 384 insertions(+), 184 deletions(-) create mode 100644 src/lib/adapters/waku/safe-waku.ts diff --git a/src/lib/adapters/waku/index.ts b/src/lib/adapters/waku/index.ts index fea17107..5cdd192a 100644 --- a/src/lib/adapters/waku/index.ts +++ b/src/lib/adapters/waku/index.ts @@ -6,13 +6,12 @@ import { type Message, isGroupChatId, type DataMessage, - getLastMessageTime, getLastSeenMessageTime, type ChatData, + getLastMessageTime, } from '$lib/stores/chat' import type { User } from '$lib/types' -import { PageDirection, type LightNode, type TimeFilter } from '@waku/interfaces' -import { connectWaku, sendMessage } from './waku' +import type { TimeFilter } from '@waku/interfaces' import type { BaseWallet, Wallet } from 'ethers' import { get } from 'svelte/store' import { objectStore, objectKey } from '$lib/stores/objects' @@ -36,16 +35,10 @@ import { makeWakustore } from './wakustore' import type { StorageChat, StorageChatEntry, StorageObjectEntry, StorageProfile } from './types' import { genRandomHex } from '$lib/utils' import { walletStore } from '$lib/stores/wallet' +import { SafeWaku } from './safe-waku' const MAX_MESSAGES = 100 -interface QueuedMessage { - message: Message - address: string - id: string - adapter: WakuObjectAdapter -} - function createPrivateChat(chatId: string, user: User, ownAddress: string): string { const ownProfile = get(profile) const ownUser = { @@ -162,36 +155,21 @@ async function executeOnDataMessage( } export default class WakuAdapter implements Adapter { - private waku: LightNode | undefined + private safeWaku = new SafeWaku() private subscriptions: Array<() => void> = [] - private numWaitingSaveChats = 0 - private isSavingChats = false - private queuedMessages: QueuedMessage[] = [] - private isHandlingMessage = false async onLogIn(wallet: BaseWallet): Promise { const address = wallet.address - this.waku = await connectWaku({ - onDisconnect: () => { - console.debug('❌ disconnected from waku') - }, - onConnect: () => { - console.debug('✅ connected to waku') - }, - }) + const ws = await this.makeWakustore() const wakuObjectAdapter = makeWakuObjectAdapter(this, wallet) - const ws = makeWakustore(this.waku) - const storageProfile = await ws.getDoc('profile', address) profile.update((state) => ({ ...state, ...storageProfile, address, loading: false })) const storageChatEntries = await ws.getDoc('chats', address) chats.update((state) => ({ ...state, chats: new Map(storageChatEntries), loading: false })) - // eslint-disable-next-line @typescript-eslint/no-this-alias - const adapter = this const allChats = Array.from(get(chats).chats) // private chats @@ -206,27 +184,36 @@ export default class WakuAdapter implements Adapter { await this.subscribeToPrivateMessages(address, address, wakuObjectAdapter, timeFilter) // group chats - const groupChatIds = allChats.filter(([id]) => isGroupChatId(id)).map(([id]) => id) - - for (const groupChatId of groupChatIds) { - await this.subscribeToGroupChat(groupChatId, address, wakuObjectAdapter) + const groupChats = allChats.filter(([id]) => isGroupChatId(id)).map(([, chat]) => chat) + + for (const groupChat of groupChats) { + const groupChatId = groupChat.chatId + const lastSeenMessageTime = getLastMessageTime(groupChat) + const now = new Date() + const timeFilter = { + startTime: new Date(lastSeenMessageTime + 1), + endTime: now, + } + await this.subscribeToGroupChat(groupChatId, address, wakuObjectAdapter, timeFilter) } // chat store + let firstChatStoreSave = true + let chatSaveTimeout: ReturnType | undefined = undefined const subscribeChatStore = chats.subscribe(async () => { - if (this.isSavingChats) { - this.numWaitingSaveChats++ + if (firstChatStoreSave) { + firstChatStoreSave = false return } + // debounce saving changes + if (chatSaveTimeout) { + clearTimeout(chatSaveTimeout) + } - this.isSavingChats = true - - do { - this.numWaitingSaveChats = 0 + chatSaveTimeout = setTimeout(async () => { + chatSaveTimeout = undefined await this.saveChatStore(address) - } while (this.numWaitingSaveChats > 0) - - this.isSavingChats = false + }, 1000) }) this.subscriptions.push(subscribeChatStore) @@ -239,8 +226,10 @@ export default class WakuAdapter implements Adapter { loading: false, })) + let firstObjectStoreSave = true const subscribeObjectStore = objectStore.subscribe(async (objects) => { - if (!adapter.waku) { + if (firstObjectStoreSave) { + firstObjectStoreSave = false return } await ws.setDoc('objects', address, Array.from(objects.objects)) @@ -253,16 +242,13 @@ export default class WakuAdapter implements Adapter { } async onLogOut() { + await this.safeWaku.unsubscribeAll() this.subscriptions.forEach((s) => s()) this.subscriptions = [] profile.set({ loading: false }) } async saveUserProfile(address: string, name?: string, avatar?: string): Promise { - if (!this.waku) { - this.waku = await connectWaku() - } - const defaultProfile: StorageProfile = { name: name ?? address } const storageProfile = (await this.fetchStorageProfile(address)) || defaultProfile @@ -270,24 +256,17 @@ export default class WakuAdapter implements Adapter { if (name) storageProfile.name = name if (avatar || name) { - const ws = makeWakustore(this.waku) + const ws = await this.makeWakustore() ws.setDoc('profile', address, storageProfile) profile.update((state) => ({ ...state, address, name, avatar })) } } async getUserProfile(address: string): Promise { - if (!this.waku) { - this.waku = await connectWaku() - } return this.storageProfileToUser(address) } async startChat(address: string, peerAddress: string): Promise { - if (!this.waku) { - this.waku = await connectWaku() - } - const chatId = peerAddress const user = await this.storageProfileToUser(chatId) if (!user) { @@ -305,9 +284,6 @@ export default class WakuAdapter implements Adapter { name: string, avatar?: string, ): Promise { - if (!this.waku) { - this.waku = await connectWaku() - } if (memberAddresses.length === 0) { throw 'invalid chat' } @@ -325,7 +301,7 @@ export default class WakuAdapter implements Adapter { createGroupChat(chatId, chat.users, name, avatar, true) - const ws = makeWakustore(this.waku) + const ws = await this.makeWakustore() await ws.setDoc('group-chats', chatId, storageChat) await this.subscribeToGroupChat(chatId, wallet.address, wakuObjectAdapter) @@ -333,11 +309,7 @@ export default class WakuAdapter implements Adapter { } async addMemberToGroupChat(chatId: string, users: string[]): Promise { - if (!this.waku) { - this.waku = await connectWaku() - } - - const ws = makeWakustore(this.waku) + const ws = await this.makeWakustore() const groupChat = await ws.getDoc('group-chats', chatId) if (!groupChat) { @@ -352,11 +324,7 @@ export default class WakuAdapter implements Adapter { } async removeFromGroupChat(chatId: string, address: string): Promise { - if (!this.waku) { - this.waku = await connectWaku() - } - - const ws = makeWakustore(this.waku) + const ws = await this.makeWakustore() const groupChat = await ws.getDoc('group-chats', chatId) if (!groupChat) { @@ -371,11 +339,7 @@ export default class WakuAdapter implements Adapter { } async saveGroupChatProfile(chatId: string, name?: string, avatar?: string): Promise { - if (!this.waku) { - this.waku = await connectWaku() - } - - const ws = makeWakustore(this.waku) + const ws = await this.makeWakustore() const groupChat = await ws.getDoc('group-chats', chatId) if (!groupChat) { @@ -391,10 +355,6 @@ export default class WakuAdapter implements Adapter { } async sendChatMessage(wallet: BaseWallet, chatId: string, text: string): Promise { - if (!this.waku) { - this.waku = await connectWaku() - } - const fromAddress = wallet.address const message: Message = { type: 'user', @@ -406,7 +366,7 @@ export default class WakuAdapter implements Adapter { const wakuObjectAdapter = makeWakuObjectAdapter(this, wallet) await addMessageToChat(fromAddress, wakuObjectAdapter, chatId, message) - await sendMessage(this.waku, chatId, message) + await this.safeWaku.sendMessage(chatId, message) } async sendData( @@ -416,10 +376,6 @@ export default class WakuAdapter implements Adapter { instanceId: string, data: JSONSerializable, ): Promise { - if (!this.waku) { - this.waku = await connectWaku() - } - const fromAddress = wallet.address const message: Message = { type: 'data', @@ -434,14 +390,10 @@ export default class WakuAdapter implements Adapter { const send = (data: JSONValue) => this.sendData(wallet, chatId, objectId, instanceId, data) await addMessageToChat(fromAddress, wakuObjectAdapter, chatId, message, send) - await sendMessage(this.waku, chatId, message) + await this.safeWaku.sendMessage(chatId, message) } async sendInvite(wallet: BaseWallet, chatId: string, users: string[]): Promise { - if (!this.waku) { - this.waku = await connectWaku() - } - if (!isGroupChatId(chatId)) { throw 'chat id is private' } @@ -455,7 +407,7 @@ export default class WakuAdapter implements Adapter { } for (const user of users) { - await sendMessage(this.waku, user, message) + await this.safeWaku.sendMessage(user, message) } } @@ -466,10 +418,6 @@ export default class WakuAdapter implements Adapter { instanceId: string, updater: (state: JSONSerializable) => JSONSerializable, ): Promise { - if (!this.waku) { - this.waku = await connectWaku() - } - const key = objectKey(objectId, instanceId) const wakuObjectStore = get(objectStore) @@ -497,11 +445,12 @@ export default class WakuAdapter implements Adapter { } } - private async storageChatToChat(chatId: string, storageChat: StorageChat): Promise { - if (!this.waku) { - throw 'no waku' - } + private async makeWakustore() { + const waku = await this.safeWaku.connect() + return makeWakustore(waku) + } + private async storageChatToChat(chatId: string, storageChat: StorageChat): Promise { const userPromises = storageChat.users.map((user) => this.storageProfileToUser(user)) const allUsers = await Promise.all(userPromises) const users = allUsers.filter((user) => user) as User[] @@ -518,11 +467,7 @@ export default class WakuAdapter implements Adapter { // fetches the profile from the network private async fetchStorageProfile(address: string): Promise { - if (!this.waku) { - throw 'no waku' - } - - const ws = makeWakustore(this.waku) + const ws = await this.makeWakustore() const storageProfile = await ws.getDoc('profile', address) return storageProfile @@ -559,11 +504,7 @@ export default class WakuAdapter implements Adapter { wakuObjectAdapter: WakuObjectAdapter, timeFilter?: TimeFilter, ) { - if (!this.waku) { - return - } - - const ws = makeWakustore(this.waku) + const ws = await this.makeWakustore() const groupChatSubscription = await ws.onSnapshot( ws.docQuery('group-chats', groupChatId), @@ -593,60 +534,9 @@ export default class WakuAdapter implements Adapter { wakuObjectAdapter: WakuObjectAdapter, timeFilter?: TimeFilter, ) { - if (!this.waku) { - return - } - - const ws = makeWakustore(this.waku) - - const startTime = new Date(getLastMessageTime(get(chats).chats.get(id)) + 1) - const endTime = new Date() - - const subscription = await ws.onSnapshot( - ws.collectionQuery('private-message', id, { - timeFilter: timeFilter || { startTime, endTime }, - pageDirection: PageDirection.BACKWARD, - pageSize: 1000, - }), - (message) => { - this.queueMessage(message, address, id, wakuObjectAdapter) - }, + this.safeWaku.subscribe(id, timeFilter, (message, chatId) => + this.handleMessage(message, address, chatId, wakuObjectAdapter), ) - this.subscriptions.push(subscription) - } - - private async queueMessage( - message: Message, - address: string, - id: string, - adapter: WakuObjectAdapter, - ) { - this.queuedMessages.push({ - message, - address, - id, - adapter, - }) - - if (this.isHandlingMessage) { - return - } - - this.isHandlingMessage = true - - while (this.queuedMessages.length > 0) { - const queuedMessage = this.queuedMessages.shift() - if (queuedMessage) { - await this.handleMessage( - queuedMessage.message, - queuedMessage.address, - queuedMessage.id, - queuedMessage.adapter, - ) - } - } - - this.isHandlingMessage = false } private async handleMessage( @@ -757,11 +647,7 @@ export default class WakuAdapter implements Adapter { } private async saveChatStore(address: string) { - if (!this.waku) { - return - } - - const ws = makeWakustore(this.waku) + const ws = await this.makeWakustore() const chatData: ChatData = get(chats) const result = await ws.setDoc('chats', address, Array.from(chatData.chats)) diff --git a/src/lib/adapters/waku/safe-waku.ts b/src/lib/adapters/waku/safe-waku.ts new file mode 100644 index 00000000..50a1bda0 --- /dev/null +++ b/src/lib/adapters/waku/safe-waku.ts @@ -0,0 +1,307 @@ +import { connectWaku, type ConnectWakuOptions, sendMessage } from '$lib/adapters/waku/waku' +import { isGroupChatId, type Message } from '$lib/stores/chat' +import type { LightNode, TimeFilter, Unsubscribe } from '@waku/interfaces' +import { PageDirection } from '@waku/interfaces' +import { makeWakustore } from './wakustore' + +type Callback = (message: Message, chatId: string) => Promise + +interface QueuedMessage { + chatMessage: Message + chatId: string + callback: Callback +} + +interface Subscription { + unsubscribe: Unsubscribe + callback: Callback +} + +async function sleep(msec: number) { + return await new Promise((r) => setTimeout(r, msec)) +} + +export class SafeWaku { + public lightNode: LightNode | undefined = undefined + private subscriptions = new Map() + private lastMessages = new Map() + private lastSentTimestamp = 0 + private isReSubscribing = false + public readonly errors = { + numDisconnect: 0, + numSendError: 0, + } + private queuedMessages: QueuedMessage[] = [] + private isHandlingMessage = false + private logging = true + private logDateTime = true + private isConnecting = false + + constructor(public readonly options?: ConnectWakuOptions) {} + + async connect() { + if (this.lightNode) { + return this.lightNode + } + + this.lightNode = await this.safeConnectWaku() + return this.lightNode + } + + async subscribe( + chatId: string, + timeFilter: TimeFilter | undefined, + callback: (message: Message, chatId: string) => Promise, + ) { + if (!this.lightNode) { + this.lightNode = await this.safeConnectWaku() + } + + const lastMessageTime = this.lastMessages.get(chatId)?.timestamp || 0 + const startTime = new Date(lastMessageTime + 1) + const endTime = new Date() + const calculatedTimeFilter = lastMessageTime + ? { startTime, endTime } + : { startTime: endTime, endTime } + timeFilter = timeFilter || calculatedTimeFilter + + const talkEmoji = isGroupChatId(chatId) ? '🗫' : '🗩' + this.log(`${talkEmoji} subscribe to ${chatId}`) + + const ws = makeWakustore(this.lightNode) + const unsubscribe = await ws.onSnapshot( + ws.collectionQuery('private-message', chatId, { + timeFilter, + pageDirection: PageDirection.FORWARD, + pageSize: 1000, + }), + (message) => this.queueMessage(callback, message, chatId), + ) + + const subscription = { + unsubscribe, + callback, + } + + this.subscriptions.set(chatId, subscription) + } + + async unsubscribeAll() { + for (const subscription of this.subscriptions.values()) { + await subscription.unsubscribe() + } + + this.subscriptions = new Map() + } + + async sendMessage(id: string, message: Message) { + if (!this.lightNode) { + this.lightNode = await this.safeConnectWaku() + } + + let error = undefined + let timeout = 1_000 + + const start = Date.now() + + if (message.timestamp === this.lastSentTimestamp) { + message.timestamp++ + } + this.lastSentTimestamp = message.timestamp + + do { + try { + error = await sendMessage(this.lightNode, id, message) + } catch (e) { + error = e + } finally { + if (error) { + this.errors.numSendError++ + this.log(`⁉️ Error: ${error}`) + this.log(`🕓 Waiting ${timeout} milliseconds...`) + await sleep(timeout) + if (timeout < 120_000) { + timeout += timeout + } + } + } + } while (error) + + const elapsed = Date.now() - start + + if (elapsed > 1000) { + this.log(`⏰ sending message took ${elapsed} milliseconds`) + } + } + + private async safeConnectWaku() { + if (this.isConnecting) { + while (!this.lightNode) { + await sleep(100) + } + return this.lightNode + } + + this.isConnecting = true + + let waku: LightNode | undefined + while (!waku) { + try { + waku = await this.connectWaku() + } catch (e) { + this.log(`⁉️ Error while connecting: ${e}`) + } + } + + this.isConnecting = false + + return waku + } + + private async connectWaku() { + const waku = await connectWaku({ + onConnect: (connections) => { + this.log('✅ connected to waku', { connections }) + this.safeResubscribe() + + if (this.options?.onConnect) { + this.options?.onConnect(connections) + } + }, + onDisconnect: () => { + this.log('❌ disconnected from waku') + this.errors.numDisconnect++ + + if (this.options?.onDisconnect) { + this.options.onDisconnect() + } + }, + }) + return waku + } + + private async safeResubscribe() { + if (this.isReSubscribing) { + return + } + + this.isReSubscribing = true + + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await this.resubscribe() + break + } catch (e) { + this.log(`⁉️ Error while resubscribing: ${e}`) + + // sleep to avoid infinite looping + await sleep(100) + } + } + + this.isReSubscribing = false + } + + private async resubscribe() { + if (!this.lightNode) { + return + } + + const chatIds = this.subscriptions.keys() + for (const subscription of this.subscriptions.values()) { + await subscription.unsubscribe() + } + + const oldSubscriptions = this.subscriptions + this.subscriptions = new Map() + + let subscribeError = undefined + for (const chatId of chatIds) { + const callback = oldSubscriptions.get(chatId)?.callback + if (!callback) { + continue + } + + if (!subscribeError) { + try { + await this.subscribe(chatId, undefined, callback) + } catch (e) { + subscribeError = e + this.subscribeEmpty(chatId, callback) + } + } else { + this.subscribeEmpty(chatId, callback) + } + } + + if (subscribeError) { + throw subscribeError + } + } + + private subscribeEmpty(chatId: string, callback: Callback) { + const unsubscribe = () => { + /* empty */ + } + const subscription = { + callback, + unsubscribe, + } + this.subscriptions.set(chatId, subscription) + } + + private async queueMessage(callback: Callback, chatMessage: Message, chatId: string) { + this.queuedMessages.push({ + callback, + chatMessage, + chatId, + }) + + if (this.isHandlingMessage) { + return + } + + this.isHandlingMessage = true + + while (this.queuedMessages.length > 0) { + const queuedMessage = this.queuedMessages.shift() + if (queuedMessage) { + // deduplicate already seen messages + const message = queuedMessage.chatMessage + const lastMessage = this.lastMessages.get(chatId) + if ( + lastMessage && + lastMessage.timestamp === message.timestamp && + lastMessage.type === message.type && + lastMessage.fromAddress === message.fromAddress + ) { + this.log('🙈 ignoring duplicate message', { message, lastMessage }) + continue + } + + this.lastMessages.set(chatId, message) + + try { + await callback(queuedMessage.chatMessage, queuedMessage.chatId) + } catch (e) { + this.log(`⁉️ Error in callback: ${e}`) + } + } + } + + this.isHandlingMessage = false + } + + private log(...args: unknown[]) { + if (!this.logging) { + return + } + if (!this.logDateTime) { + console.debug(...args) + return + } + const isoTime = new Date().toISOString().replace('T', ' ').replace('Z', '') + console.debug(isoTime, ...args) + } +} diff --git a/src/lib/adapters/waku/waku.ts b/src/lib/adapters/waku/waku.ts index 2b72a543..464e41d3 100644 --- a/src/lib/adapters/waku/waku.ts +++ b/src/lib/adapters/waku/waku.ts @@ -16,13 +16,17 @@ import { type Unsubscribe, } from '@waku/interfaces' -const peerMultiaddr = multiaddr( +const peers = [ + // use this address for local testing + // '/ip4/127.0.0.1/tcp/8000/ws/p2p/16Uiu2HAm53sojJN72rFbYg6GV2LpRRER9XeWkiEAhjKy3aL9cN5Z', + // '/dns4/ws.waku.apyos.dev/tcp/443/wss/p2p/16Uiu2HAm5wH4dPAV6zDfrBHkWt9Wu9iiXT4ehHdUArDUbEevzmBY', '/dns4/ws.waku-1.apyos.dev/tcp/443/wss/p2p/16Uiu2HAm8gXHntr3SB5sde11pavjptaoiqyvwoX3GyEZWKMPiuBu', - // use this address for local testing - // '/ip4/127.0.0.1/tcp/8000/ws/p2p/16Uiu2HAm53sojJN72rFbYg6GV2LpRRER9XeWkiEAhjKy3aL9cN5Z', -) + // '/dns4/waku.gra.nomad.apyos.dev/tcp/443/wss/p2p/16Uiu2HAmDvywnsGaB32tFqwjTsg8sfC1ZV2EXo3xjxM4V2gvH6Up', + // '/dns4/waku.bhs.nomad.apyos.dev/tcp/443/wss/p2p/16Uiu2HAkvrRkEHRMfe26F8NCWUfzMuaCfyCzwoPSUYG7yminM5Bn', + // '/dns4/waku.de.nomad.apyos.dev/tcp/443/wss/p2p/16Uiu2HAmRgjA134DcoyK8r44pKWJQ69C7McLSWtRgxUVwkKAsbGx', +] export type ContentTopic = 'private-message' | 'profile' | 'chats' | 'objects' | 'group-chats' @@ -35,7 +39,7 @@ function getTopic(contentTopic: ContentTopic, id: string | '' = '') { return `/${topicApp}/${topicVersion}/${contentTopic}/${id}` } -interface ConnectWakuOptions { +export interface ConnectWakuOptions { onDisconnect?: () => void onConnect?: (connections: unknown[]) => void } @@ -57,7 +61,10 @@ export async function connectWaku(options?: ConnectWakuOptions) { }) await waku.start() - await waku.dial(peerMultiaddr) + for (const peer of peers) { + const addr = multiaddr(peer) + await waku.dial(addr) + } await waitForRemotePeer(waku, [Protocols.Filter, Protocols.LightPush, Protocols.Store]) return waku @@ -86,10 +93,8 @@ export async function storeDocument( const json = JSON.stringify(document) const payload = utf8ToBytes(json) - const { error } = await waku.lightPush.send(encoder, { payload }) - if (error) { - console.error(error) - } + const sendResult = await waku.lightPush.send(encoder, { payload }) + return sendResult.error } export async function readStore( @@ -114,6 +119,6 @@ export async function sendMessage(waku: LightNode, id: string, message: unknown) const contentTopic = getTopic('private-message', id) const encoder = createEncoder({ contentTopic }) - const { error } = await waku.lightPush.send(encoder, { payload }) - return error + const sendResult = await waku.lightPush.send(encoder, { payload }) + return sendResult.error } diff --git a/src/lib/stores/chat.ts b/src/lib/stores/chat.ts index 7fa37545..9e122bf2 100644 --- a/src/lib/stores/chat.ts +++ b/src/lib/stores/chat.ts @@ -104,7 +104,8 @@ function createChatStore(): ChatStore { return state } const newMap = new Map(state.chats) - newMap.set(chatId, update(oldChat)) + const newChat = update(oldChat) + newMap.set(chatId, newChat) return { ...state, diff --git a/src/routes/chat/[id]/+page.svelte b/src/routes/chat/[id]/+page.svelte index 47e39f23..75ef73d3 100644 --- a/src/routes/chat/[id]/+page.svelte +++ b/src/routes/chat/[id]/+page.svelte @@ -55,14 +55,14 @@ }) $: messages = $chats.chats.get($page.params.id)?.messages || [] - let loading = false + let isSending = false let text = '' const sendMessage = async (wallet: HDNodeWallet) => { - loading = true + isSending = true await adapters.sendChatMessage(wallet, $page.params.id, text) text = '' - loading = false + isSending = false } @@ -133,7 +133,7 @@ }} /> {#if text.length > 0} - {/if} diff --git a/src/routes/group/chat/[id]/+page.svelte b/src/routes/group/chat/[id]/+page.svelte index a40b579d..2323a77b 100644 --- a/src/routes/group/chat/[id]/+page.svelte +++ b/src/routes/group/chat/[id]/+page.svelte @@ -65,15 +65,15 @@ }) $: messages = $chats.chats.get($page.params.id)?.messages || [] - let loading = false + let isSending = false let text = '' const sendMessage = async (wallet: HDNodeWallet) => { - loading = true + isSending = true const messageText = replaceNamesWithAddresses(text) await adapters.sendChatMessage(wallet, $page.params.id, messageText) text = '' - loading = false + isSending = false } $: inviter = chat?.users.find((user) => user.address === chat?.inviter) @@ -224,6 +224,7 @@