diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -37,6 +37,7 @@ editMessageCreationResponder, fetchPinnedMessagesResponder, searchMessagesResponder, + fetchLatestMessages, } from './responders/message-responders.js'; import { updateRelationshipsResponder } from './responders/relationship-responders.js'; import { @@ -219,6 +220,10 @@ responder: searchMessagesResponder, requiredPolicies: baseLegalPolicies, }, + fetch_latest_messages: { + responder: fetchLatestMessages, + requiredPolicies: baseLegalPolicies, + }, search_users: { responder: userSearchResponder, requiredPolicies: baseLegalPolicies, diff --git a/keyserver/src/fetchers/thread-fetchers.js b/keyserver/src/fetchers/thread-fetchers.js --- a/keyserver/src/fetchers/thread-fetchers.js +++ b/keyserver/src/fetchers/thread-fetchers.js @@ -308,6 +308,44 @@ return threads.threadInfos[threadID]; } +async function fetchThreadsWithLatestMessages( + userID: string, + home: boolean, + fromMessageID: string, +): Promise<{ + +allThreads: $ReadOnlyArray, + +threadsExcludingParents: Set, +}> { + const query = SQL` + SELECT m.thread, t.type, t.parent_thread_id + FROM memberships m + LEFT JOIN threads t ON m.thread = t.id + WHERE m.user = ${userID} AND + m.role > 0 AND + m.last_message < ${fromMessageID} AND + JSON_EXTRACT(m.subscription, '$.home') IS ${home} + ORDER BY m.last_message DESC + LIMIT 25 + `; + + const [result] = await dbQuery(query); + + const allThreads = new Set(); + const threadsExcludingParents = new Set(); + for (const row of result) { + allThreads.add(row.thread.toString()); + threadsExcludingParents.add(row.thread.toString()); + if (row.type === threadTypes.SIDEBAR) { + allThreads.add(row.parent_thread_id); + } + } + + return { + allThreads: [...allThreads], + threadsExcludingParents, + }; +} + export { fetchServerThreadInfos, fetchThreadInfos, @@ -318,4 +356,5 @@ personalThreadQuery, fetchPersonalThreadID, serverThreadInfoFromMessageInfo, + fetchThreadsWithLatestMessages, }; diff --git a/keyserver/src/responders/message-responders.js b/keyserver/src/responders/message-responders.js --- a/keyserver/src/responders/message-responders.js +++ b/keyserver/src/responders/message-responders.js @@ -1,6 +1,7 @@ // @flow import invariant from 'invariant'; +import _min from 'lodash/fp/min.js'; import t, { type TInterface } from 'tcomb'; import { onlyOneEmojiRegex } from 'lib/shared/emojis.js'; @@ -27,6 +28,8 @@ rawMessageInfoValidator, type SearchMessagesResponse, type SearchMessagesRequest, + type FetchLatestMessagesRequest, + type FetchLatestMessagesResponse, } from 'lib/types/message-types.js'; import type { EditMessageData } from 'lib/types/messages/edit.js'; import type { ReactionMessageData } from 'lib/types/messages/reaction.js'; @@ -52,7 +55,10 @@ fetchPinnedMessageInfos, searchMessagesInSingleChat, } from '../fetchers/message-fetchers.js'; -import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js'; +import { + fetchServerThreadInfos, + fetchThreadsWithLatestMessages, +} from '../fetchers/thread-fetchers.js'; import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js'; import { fetchImages, @@ -514,6 +520,58 @@ ); } +const fetchLatestMessagesInputValidator = tShape({ + home: t.Boolean, + fromMessage: tID, +}); + +const fetchLatestMessagesResponseValidator = + tShape({ + rawMessageInfos: t.list(rawMessageInfoValidator), + truncationStatuses: messageTruncationStatusesValidator, + oldestMessage: t.maybe(tID), + }); + +async function fetchLatestMessages( + viewer: Viewer, + input: mixed, +): Promise { + const request = await validateInput( + viewer, + fetchLatestMessagesInputValidator, + input, + ); + const { allThreads, threadsExcludingParents } = + await fetchThreadsWithLatestMessages( + viewer.userID, + request.home, + request.fromMessage, + ); + + if (allThreads.length === 0) { + return { rawMessageInfos: [], truncationStatuses: {}, oldestMessage: null }; + } + + const threadCursors = {}; + for (const threadID of allThreads) { + threadCursors[threadID] = null; + } + + const response = await fetchMessageInfos(viewer, { threadCursors }, 1); + + const oldestMessage = _min( + response.rawMessageInfos + .filter(message => threadsExcludingParents.has(message.threadID)) + .map(message => Number(message.id)), + ).toString(); + + return validateOutput( + viewer.platformDetails, + fetchLatestMessagesResponseValidator, + { ...response, oldestMessage }, + ); +} + export { textMessageCreationResponder, messageFetchResponder, @@ -522,4 +580,5 @@ editMessageCreationResponder, fetchPinnedMessagesResponder, searchMessagesResponder, + fetchLatestMessages, }; diff --git a/lib/actions/message-actions.js b/lib/actions/message-actions.js --- a/lib/actions/message-actions.js +++ b/lib/actions/message-actions.js @@ -13,6 +13,8 @@ FetchPinnedMessagesResult, SearchMessagesRequest, SearchMessagesResponse, + FetchLatestMessagesRequest, + FetchLatestMessagesResponse, } from '../types/message-types.js'; import type { MediaMessageServerDBContent } from '../types/messages/media.js'; import type { @@ -302,6 +304,26 @@ }; }; +const fetchLatestMessagesActionTypes = Object.freeze({ + started: 'FETCH_LATEST_MESSAGES_STARTED', + success: 'FETCH_LATEST_MESSAGES_SUCCESS', + failed: 'FETCH_LATEST_MESSAGES_FAILED', +}); +const fetchLatestMessages = + ( + callServerEndpoint: CallServerEndpoint, + ): (( + request: FetchLatestMessagesRequest, + ) => Promise) => + async request => { + const response = await callServerEndpoint('fetch_latest_messages', request); + return { + rawMessageInfos: response.rawMessageInfos, + truncationStatuses: response.truncationStatuses, + oldestMessage: response.oldestMessage, + }; + }; + export { fetchMessagesBeforeCursorActionTypes, fetchMessagesBeforeCursor, @@ -326,4 +348,6 @@ sendEditMessage, fetchPinnedMessages, fetchPinnedMessageActionTypes, + fetchLatestMessagesActionTypes, + fetchLatestMessages, }; diff --git a/lib/reducers/message-reducer.js b/lib/reducers/message-reducer.js --- a/lib/reducers/message-reducer.js +++ b/lib/reducers/message-reducer.js @@ -33,6 +33,7 @@ messageStorePruneActionType, createLocalMessageActionType, fetchSingleMostRecentMessagesFromThreadsActionTypes, + fetchLatestMessagesActionTypes, } from '../actions/message-actions.js'; import { sendMessageReportActionTypes } from '../actions/message-report-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; @@ -887,7 +888,9 @@ action.type, ); } else if ( - action.type === fetchSingleMostRecentMessagesFromThreadsActionTypes.success + action.type === + fetchSingleMostRecentMessagesFromThreadsActionTypes.success || + action.type === fetchLatestMessagesActionTypes.success ) { return mergeNewMessages( messageStore, diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js --- a/lib/types/endpoints.js +++ b/lib/types/endpoints.js @@ -96,6 +96,7 @@ SEARCH_MESSAGES: 'search_messages', GET_OLM_SESSION_INITIALIZATION_DATA: 'get_olm_session_initialization_data', VERSION: 'version', + FETCH_LATEST_MESSAGES: 'fetch_latest_messages', }); type SocketPreferredEndpoint = $Values; diff --git a/lib/types/message-types.js b/lib/types/message-types.js --- a/lib/types/message-types.js +++ b/lib/types/message-types.js @@ -728,3 +728,14 @@ +messages: $ReadOnlyArray, +endReached: boolean, }; + +export type FetchLatestMessagesRequest = { + +home: boolean, + +fromMessage: string, +}; + +export type FetchLatestMessagesResponse = { + +rawMessageInfos: $ReadOnlyArray, + +truncationStatuses: MessageTruncationStatuses, + +oldestMessage: ?string, +}; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -62,6 +62,7 @@ ClientDBThreadMessageInfo, FetchPinnedMessagesResult, SearchMessagesResponse, + FetchLatestMessagesResponse, } from './message-types.js'; import type { RawReactionMessageInfo } from './messages/reaction.js'; import type { RawTextMessageInfo } from './messages/text.js'; @@ -1159,6 +1160,22 @@ +error: true, +payload: Error, +loadingInfo: LoadingInfo, + } + | { + +type: 'FETCH_LATEST_MESSAGES_STARTED', + +payload: void, + +loadingInfo?: LoadingInfo, + } + | { + +type: 'FETCH_LATEST_MESSAGES_SUCCESS', + +payload: FetchLatestMessagesResponse, + +loadingInfo: LoadingInfo, + } + | { + +type: 'FETCH_LATEST_MESSAGES_FAILED', + +error: true, + +payload: Error, + +loadingInfo: LoadingInfo, }; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string);