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 { @@ -218,6 +219,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,30 @@ return threads.threadInfos[threadID]; } +async function fetchThreadsWithLatestMessages( + userID: string, + home: boolean, + fromMessageID: string, +): Promise<$ReadOnlyArray> { + const query = SQL` + SELECT t.id + FROM threads t + LEFT JOIN memberships m ON m.thread = t.id AND m.user = ${userID} + WHERE 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 ids = []; + for (const row of result) { + ids.push(row.id.toString()); + } + return ids; +} + export { fetchServerThreadInfos, fetchThreadInfos, @@ -318,4 +342,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 @@ -27,6 +27,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 +54,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, @@ -500,6 +505,54 @@ ); } +const fetchLatestMessagesInputValidator = tShape({ + home: t.Boolean, + fromMessage: tID, +}); + +const fetchLatestMessagesResponseValidator = + tShape({ + rawMessageInfos: t.list(rawMessageInfoValidator), + truncationStatuses: messageTruncationStatusesValidator, + }); + +async function fetchLatestMessages( + viewer: Viewer, + input: mixed, +): Promise { + const request = await validateInput( + viewer, + fetchLatestMessagesInputValidator, + input, + ); + const result = await fetchThreadsWithLatestMessages( + viewer.userID, + request.home, + request.fromMessage, + ); + + if (result.length === 0) { + return { rawMessageInfos: [], truncationStatuses: {} }; + } + + const threadCursors = {}; + for (const threadID of result) { + threadCursors[threadID] = null; + } + + const response = await fetchMessageInfos( + viewer, + { threadCursors, joinedThreads: true, newerThan: 0 }, + 1, + ); + + return validateOutput( + viewer.platformDetails, + fetchLatestMessagesResponseValidator, + response, + ); +} + export { textMessageCreationResponder, messageFetchResponder, @@ -508,4 +561,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,20 @@ }; }; +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 => + await callServerEndpoint('fetch_latest_messages', request); + export { fetchMessagesBeforeCursorActionTypes, fetchMessagesBeforeCursor, @@ -326,4 +342,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'; @@ -896,6 +897,14 @@ newThreadInfos, action.type, ); + } else if (action.type === fetchLatestMessagesActionTypes.success) { + return mergeNewMessages( + messageStore, + action.payload.rawMessageInfos, + action.payload.truncationStatuses, + newThreadInfos, + action.type, + ); } else if ( action.type === fetchMessagesBeforeCursorActionTypes.success || action.type === fetchMostRecentMessagesActionTypes.success diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js --- a/lib/types/endpoints.js +++ b/lib/types/endpoints.js @@ -95,6 +95,7 @@ UPLOAD_MEDIA_METADATA: 'upload_media_metadata', SEARCH_MESSAGES: 'search_messages', GET_OLM_SESSION_INITIALIZATION_DATA: 'get_olm_session_initialization_data', + 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,13 @@ +messages: $ReadOnlyArray, +endReached: boolean, }; + +export type FetchLatestMessagesRequest = { + +home: boolean, + +fromMessage: string, +}; + +export type FetchLatestMessagesResponse = { + +rawMessageInfos: $ReadOnlyArray, + +truncationStatuses: MessageTruncationStatuses, +}; 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 @@ -61,6 +61,7 @@ ClientDBThreadMessageInfo, FetchPinnedMessagesResult, SearchMessagesResponse, + FetchLatestMessagesResponse, } from './message-types.js'; import type { RawReactionMessageInfo } from './messages/reaction.js'; import type { RawTextMessageInfo } from './messages/text.js'; @@ -1136,6 +1137,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);