diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 7f4bee9676..fc8cf67b26 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -350,17 +350,176 @@ class Channel extends Base { let msgs = channel.msgs.getModelsArray().filter(msgFilter); if (searchOptions && searchOptions.limit > 0) { - while (msgs.length < searchOptions.limit) { - const loadedMessages = await window - .require('WAWebChatLoadMessages') - .loadEarlierMsgs(channel); - if (!loadedMessages || !loadedMessages.length) break; - msgs = [...loadedMessages.filter(msgFilter), ...msgs]; - } - - if (msgs.length > searchOptions.limit) { + const msgFindLocal = window.require( + 'WAWebDBMessageFindLocal', + ); + const WAWebMsgKey = window.require('WAWebMsgKey'); + const MsgStore = window.require('WAWebCollections').Msg; + + // msgFindByDirection is the newer API; fall back to msgFindBefore on older WA Web versions + const findBefore = async (anchorKey, count) => { + if ( + typeof msgFindLocal.msgFindByDirection === + 'function' + ) { + return await msgFindLocal.msgFindByDirection({ + anchor: anchorKey, + count, + direction: 'before', + }); + } + return await msgFindLocal.msgFindBefore({ + anchor: anchorKey, + count, + }); + }; + + const toMsgKey = (id) => { + if (!id) return null; + if (id instanceof WAWebMsgKey) return id; + const s = + typeof id === 'string' + ? id + : id._serialized || id?.toString?.(); + return s ? WAWebMsgKey.fromString(s) : null; + }; + + const toMsgModels = (rawMessages) => { + const out = []; + for (const m of rawMessages) { + if (m && typeof m.serialize === 'function') { + out.push(m); + continue; + } + const serialized = + m?.id?._serialized || + (typeof m === 'string' ? m : null); + let model = + (serialized && MsgStore.get(serialized)) || + (m?.id && + MsgStore.get(m.id._serialized || m.id)) || + null; + if (!model && m && MsgStore.modelClass) { + try { + model = new MsgStore.modelClass(m); + } catch (e) { + model = null; + } + } + if (model) out.push(model); + } + return out; + }; + + const dedupeByMsgId = (arr) => { + const seen = new Set(); + return arr.filter((m) => { + const key = m.id?._serialized; + if (!key || seen.has(key)) return false; + seen.add(key); + return true; + }); + }; + + const limit = searchOptions.limit; + const finite = Number.isFinite(limit); + const fromMeFilter = + searchOptions && searchOptions.fromMe !== undefined; + + if (!fromMeFilter && finite) { + const anchorSerialized = + channel.lastReceivedKey?.toString(); + if (!anchorSerialized) { + msgs.sort((a, b) => (a.t > b.t ? 1 : -1)); + msgs = msgs.slice(-Math.min(limit, msgs.length)); + } else { + const fetchCount = Math.max(0, limit - 1); + const anchorKey = toMsgKey(anchorSerialized); + const result = await findBefore( + anchorKey, + fetchCount, + ); + const rawMessages = Array.isArray(result) + ? result + : result?.messages || []; + if ( + result?.status === 404 && + (!rawMessages || !rawMessages.length) + ) { + // anchor not in local DB — fall back to in-memory msgs (same as the !anchorSerialized branch above) + msgs.sort((a, b) => (a.t > b.t ? 1 : -1)); + msgs = msgs.slice( + -Math.min(limit, msgs.length), + ); + } else { + let loaded = toMsgModels(rawMessages); + const anchorMsg = + MsgStore.get(anchorSerialized); + let merged = [ + ...loaded, + ...(anchorMsg ? [anchorMsg] : []), + // include in-memory msgs so recent (not-yet-persisted) messages aren't dropped + ...msgs, + ]; + merged = merged.filter( + (m) => + !m.isNotification && + m.type !== 'newsletter_notification', + ); + merged.sort((a, b) => (a.t > b.t ? 1 : -1)); + merged = dedupeByMsgId(merged); + msgs = merged.filter(msgFilter); + if (msgs.length > limit) { + msgs = msgs.slice(-limit); + } + } + } + } else { msgs.sort((a, b) => (a.t > b.t ? 1 : -1)); - msgs = msgs.splice(msgs.length - searchOptions.limit); + const batchCap = finite ? limit : 100; + while (msgs.length < limit || !finite) { + const anchor = + msgs[0]?.id || + channel.msgs.getModelsArray()[0]?.id || + channel.lastReceivedKey; + if (!anchor) break; + + const anchorKey = toMsgKey(anchor); + if (!anchorKey) break; + + const need = finite + ? Math.min(batchCap, limit - msgs.length) + : batchCap; + if (need <= 0) break; + + const result = await findBefore(anchorKey, need); + const rawMessages = Array.isArray(result) + ? result + : result?.messages || []; + if (result?.status === 404 || !rawMessages.length) { + break; + } + + const loadedMessages = toMsgModels(rawMessages); + if (!loadedMessages.length) break; + + const prevLen = msgs.length; + msgs = dedupeByMsgId([ + ...loadedMessages.filter(msgFilter), + ...msgs, + ]); + msgs.sort((a, b) => (a.t > b.t ? 1 : -1)); + + if (msgs.length === prevLen) break; + + if (!finite && loadedMessages.length < need) { + break; + } + } + + if (finite && msgs.length > limit) { + msgs = msgs.slice(-limit); + } } } diff --git a/src/structures/Chat.js b/src/structures/Chat.js index 8cea7c134e..48a885530d 100644 --- a/src/structures/Chat.js +++ b/src/structures/Chat.js @@ -223,17 +223,174 @@ class Chat extends Base { let msgs = chat.msgs.getModelsArray().filter(msgFilter); if (searchOptions && searchOptions.limit > 0) { - while (msgs.length < searchOptions.limit) { - const loadedMessages = await window - .require('WAWebChatLoadMessages') - .loadEarlierMsgs(chat, chat.msgs); - if (!loadedMessages || !loadedMessages.length) break; - msgs = [...loadedMessages.filter(msgFilter), ...msgs]; - } - - if (msgs.length > searchOptions.limit) { + const msgFindLocal = window.require( + 'WAWebDBMessageFindLocal', + ); + const WAWebMsgKey = window.require('WAWebMsgKey'); + const MsgStore = window.require('WAWebCollections').Msg; + + // msgFindByDirection is the newer API; fall back to msgFindBefore on older WA Web versions + const findBefore = async (anchorKey, count) => { + if ( + typeof msgFindLocal.msgFindByDirection === + 'function' + ) { + return await msgFindLocal.msgFindByDirection({ + anchor: anchorKey, + count, + direction: 'before', + }); + } + return await msgFindLocal.msgFindBefore({ + anchor: anchorKey, + count, + }); + }; + + const toMsgKey = (id) => { + if (!id) return null; + if (id instanceof WAWebMsgKey) return id; + const s = + typeof id === 'string' + ? id + : id._serialized || id?.toString?.(); + return s ? WAWebMsgKey.fromString(s) : null; + }; + + const toMsgModels = (rawMessages) => { + const out = []; + for (const m of rawMessages) { + if (m && typeof m.serialize === 'function') { + out.push(m); + continue; + } + const serialized = + m?.id?._serialized || + (typeof m === 'string' ? m : null); + let model = + (serialized && MsgStore.get(serialized)) || + (m?.id && + MsgStore.get(m.id._serialized || m.id)) || + null; + if (!model && m && MsgStore.modelClass) { + try { + model = new MsgStore.modelClass(m); + } catch (e) { + model = null; + } + } + if (model) out.push(model); + } + return out; + }; + + const dedupeByMsgId = (arr) => { + const seen = new Set(); + return arr.filter((m) => { + const key = m.id?._serialized; + if (!key || seen.has(key)) return false; + seen.add(key); + return true; + }); + }; + + const limit = searchOptions.limit; + const finite = Number.isFinite(limit); + const fromMeFilter = + searchOptions && searchOptions.fromMe !== undefined; + + if (!fromMeFilter && finite) { + const anchorSerialized = + chat.lastReceivedKey?.toString(); + if (!anchorSerialized) { + msgs.sort((a, b) => (a.t > b.t ? 1 : -1)); + msgs = msgs.slice(-Math.min(limit, msgs.length)); + } else { + const fetchCount = Math.max(0, limit - 1); + const anchorKey = toMsgKey(anchorSerialized); + const result = await findBefore( + anchorKey, + fetchCount, + ); + const rawMessages = Array.isArray(result) + ? result + : result?.messages || []; + if ( + result?.status === 404 && + (!rawMessages || !rawMessages.length) + ) { + // anchor not in local DB — fall back to in-memory msgs (same as the !anchorSerialized branch above) + msgs.sort((a, b) => (a.t > b.t ? 1 : -1)); + msgs = msgs.slice( + -Math.min(limit, msgs.length), + ); + } else { + let loaded = toMsgModels(rawMessages); + const anchorMsg = + MsgStore.get(anchorSerialized); + let merged = [ + ...loaded, + ...(anchorMsg ? [anchorMsg] : []), + // include in-memory msgs so recent (not-yet-persisted) messages aren't dropped + ...msgs, + ]; + merged = merged.filter( + (m) => !m.isNotification, + ); + merged.sort((a, b) => (a.t > b.t ? 1 : -1)); + merged = dedupeByMsgId(merged); + msgs = merged.filter(msgFilter); + if (msgs.length > limit) { + msgs = msgs.slice(-limit); + } + } + } + } else { msgs.sort((a, b) => (a.t > b.t ? 1 : -1)); - msgs = msgs.splice(msgs.length - searchOptions.limit); + const batchCap = finite ? limit : 100; + while (msgs.length < limit || !finite) { + const anchor = + msgs[0]?.id || + chat.msgs.getModelsArray()[0]?.id || + chat.lastReceivedKey; + if (!anchor) break; + + const anchorKey = toMsgKey(anchor); + if (!anchorKey) break; + + const need = finite + ? Math.min(batchCap, limit - msgs.length) + : batchCap; + if (need <= 0) break; + + const result = await findBefore(anchorKey, need); + const rawMessages = Array.isArray(result) + ? result + : result?.messages || []; + if (result?.status === 404 || !rawMessages.length) { + break; + } + + const loadedMessages = toMsgModels(rawMessages); + if (!loadedMessages.length) break; + + const prevLen = msgs.length; + msgs = dedupeByMsgId([ + ...loadedMessages.filter(msgFilter), + ...msgs, + ]); + msgs.sort((a, b) => (a.t > b.t ? 1 : -1)); + + if (msgs.length === prevLen) break; + + if (!finite && loadedMessages.length < need) { + break; + } + } + + if (finite && msgs.length > limit) { + msgs = msgs.slice(-limit); + } } }