diff --git a/frontend/openchat-client/src/stores/chat.ts b/frontend/openchat-client/src/stores/chat.ts index ec00de2e9b..373bd36ec2 100644 --- a/frontend/openchat-client/src/stores/chat.ts +++ b/frontend/openchat-client/src/stores/chat.ts @@ -55,6 +55,7 @@ import { draftMessagesStore } from "./draftMessages"; import { blockedUsers } from "./blockedUsers"; import { createLsBoolStore } from "./localStorageSetting"; import { configKeys } from "../utils/config"; +import { recentlySentMessagesStore } from "./recentlySentMessages"; let currentScope: ChatListScope = { kind: "direct_chat" }; chatListScopeStore.subscribe((s) => (currentScope = s)); @@ -544,6 +545,7 @@ export const threadEvents = derived( currentChatBlockedOrSuspendedUsers, currentUserIdStore, messageFiltersStore, + recentlySentMessagesStore, ], ([ $serverEvents, @@ -556,6 +558,7 @@ export const threadEvents = derived( $blockedOrSuspendedUsers, $currentUserId, $messageFilters, + $recentlySentMessagesStore, ]) => { if ($messageContext === undefined || $messageContext.threadRootMessageIndex === undefined) return []; @@ -574,6 +577,7 @@ export const threadEvents = derived( $blockedOrSuspendedUsers, $currentUserId, $messageFilters, + $recentlySentMessagesStore, ); }, ); @@ -728,6 +732,7 @@ export const eventsStore: Readable[]> = derived( currentChatBlockedOrSuspendedUsers, currentUserIdStore, messageFiltersStore, + recentlySentMessagesStore, ], ([ $serverEventsForSelectedChat, @@ -740,6 +745,7 @@ export const eventsStore: Readable[]> = derived( $blockedOrSuspendedUsers, $currentUserId, $messageFilters, + $recentlySentMessagesStore, ]) => { const chatId = get(selectedChatId) ?? { kind: "group_chat", groupId: "" }; const failedForChat = $failedMessages.get({ chatId }); @@ -756,6 +762,7 @@ export const eventsStore: Readable[]> = derived( $blockedOrSuspendedUsers, $currentUserId, $messageFilters, + $recentlySentMessagesStore, ); }, ); diff --git a/frontend/openchat-client/src/stores/mapStore.ts b/frontend/openchat-client/src/stores/mapStore.ts index eaa5f80fa7..dddc020a37 100644 --- a/frontend/openchat-client/src/stores/mapStore.ts +++ b/frontend/openchat-client/src/stores/mapStore.ts @@ -1,10 +1,15 @@ import type { Writable } from "svelte/store"; -import { get } from "svelte/store"; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function createMapStore(store: Writable>) { + let storeValue = new Map(); + store.subscribe((v) => (storeValue = v)); + return { subscribe: store.subscribe, + get: storeValue.get, + has: storeValue.has, + size: () => storeValue.size, set: store.set, insert: (key: K, value: V) => { store.update((map) => { @@ -12,8 +17,11 @@ export function createMapStore(store: Writable>) { return map; }); }, + update: (updater: (value: Map) => Map) => { + store.update(updater); + }, delete: (key: K): boolean => { - if (get(store).has(key)) { + if (storeValue.has(key)) { store.update((map) => { map.delete(key); return map; diff --git a/frontend/openchat-client/src/stores/recentlySentMessages.ts b/frontend/openchat-client/src/stores/recentlySentMessages.ts new file mode 100644 index 0000000000..57895f4e08 --- /dev/null +++ b/frontend/openchat-client/src/stores/recentlySentMessages.ts @@ -0,0 +1,25 @@ +import { createMapStore } from "./mapStore"; +import { writable } from "svelte/store"; + +// Key: MessageId, Value: Timestamp +export const recentlySentMessagesStore = createMapStore( + writable(new Map()), +); + +function pruneOldMessages(): void { + if (recentlySentMessagesStore.size() > 0) { + const oneMinuteAgo = BigInt(Date.now() - 60000); + recentlySentMessagesStore.update((map) => { + const newMap = new Map(); + for (const [key, value] of map.entries()) { + if (value > oneMinuteAgo) { + newMap.set(key, value); + } + } + return newMap; + }); + } +} + +// Prune old messages every 31 seconds +window.setInterval(pruneOldMessages, 31000); diff --git a/frontend/openchat-client/src/stores/unconfirmed.ts b/frontend/openchat-client/src/stores/unconfirmed.ts index f7f446d672..dabde8db92 100644 --- a/frontend/openchat-client/src/stores/unconfirmed.ts +++ b/frontend/openchat-client/src/stores/unconfirmed.ts @@ -6,6 +6,7 @@ import { type MessageContext, MessageContextMap, } from "openchat-shared"; +import { recentlySentMessagesStore } from "./recentlySentMessages"; export type UnconfirmedState = { messages: EventWrapper[]; @@ -81,6 +82,7 @@ function createUnconfirmedStore() { } return state; }); + recentlySentMessagesStore.insert(message.event.messageId, message.timestamp); }, contains: (key: MessageContext, messageId: bigint): boolean => { return storeValue.get(key)?.messageIds.has(messageId) ?? false; diff --git a/frontend/openchat-client/src/utils/chat.ts b/frontend/openchat-client/src/utils/chat.ts index 5ef20232a6..207a763c07 100644 --- a/frontend/openchat-client/src/utils/chat.ts +++ b/frontend/openchat-client/src/utils/chat.ts @@ -896,6 +896,28 @@ function updateReplyContexts( } } +function createMessageSortFunction( + unconfirmed: Set, + recentlySent: Map, +): (a: EventWrapper, b: EventWrapper) => number { + return (a: EventWrapper, b: EventWrapper): number => { + // If either message is still unconfirmed, and both were sent recently, use both of their local timestamps, + // otherwise we will be comparing the local timestamp of one with the server timestamp of the other + if (a.event.kind === "message" && b.event.kind === "message") { + if (unconfirmed.has(a.event.messageId) || unconfirmed.has(b.event.messageId)) { + const aTimestampOverride = recentlySent.get(a.event.messageId); + const bTimestampOverride = recentlySent.get(b.event.messageId); + + if (aTimestampOverride && bTimestampOverride) { + return aTimestampOverride > bTimestampOverride ? 1 : -1; + } + } + } + + return sortByTimestampThenEventIndex(a, b); + }; +} + function sortByTimestampThenEventIndex( a: EventWrapper, b: EventWrapper, @@ -1389,6 +1411,7 @@ export function mergeEventsAndLocalUpdates( blockedUsers: Set, currentUserId: string, messageFilters: MessageFilter[], + recentlySentMessages: Map, ): EventWrapper[] { const eventIndexes = new DRange(); eventIndexes.add(expiredEventRanges); @@ -1466,7 +1489,7 @@ export function mergeEventsAndLocalUpdates( if (unconfirmed.length > 0) { unconfirmed.sort(sortByTimestampThenEventIndex); - let anyAdded = false; + let unconfirmedAdded = new Set(); for (const message of unconfirmed) { // Only include unconfirmed events that are either contiguous with the loaded confirmed events, or are the // first events in a new chat @@ -1478,11 +1501,12 @@ export function mergeEventsAndLocalUpdates( .some((s) => s.low - 1 <= message.index && message.index <= s.high + 1)) ) { merged.push(processEvent(message)); - anyAdded = true; + unconfirmedAdded.add(message.event.messageId); } } - if (anyAdded) { - merged.sort(sortByTimestampThenEventIndex); + if (unconfirmedAdded.size > 0) { + const sortFn = createMessageSortFunction(unconfirmedAdded, recentlySentMessages); + merged.sort(sortFn); } }