diff --git a/lib/actions/activity-actions.js b/lib/actions/activity-actions.js index a5349e317..978ba2a2b 100644 --- a/lib/actions/activity-actions.js +++ b/lib/actions/activity-actions.js @@ -1,103 +1,103 @@ // @flow import type { ActivityUpdate, ActivityUpdateSuccessPayload, SetThreadUnreadStatusPayload, SetThreadUnreadStatusRequest, } from '../types/activity-types.js'; import { extractKeyserverIDFromID } from '../utils/action-utils.js'; import type { CallKeyserverEndpoint } from '../utils/keyserver-call'; import { useKeyserverCall } from '../utils/keyserver-call.js'; export type UpdateActivityInput = { +activityUpdates: $ReadOnlyArray, }; const updateActivityActionTypes = Object.freeze({ started: 'UPDATE_ACTIVITY_STARTED', success: 'UPDATE_ACTIVITY_SUCCESS', failed: 'UPDATE_ACTIVITY_FAILED', }); const updateActivity = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: UpdateActivityInput) => Promise) => async input => { const { activityUpdates } = input; - const requests = {}; + const requests: { [string]: { +updates: ActivityUpdate[] } } = {}; for (const update of activityUpdates) { const keyserverID = extractKeyserverIDFromID(update.threadID); if (!requests[keyserverID]) { requests[keyserverID] = { updates: [] }; } requests[keyserverID].updates.push(update); } const responses = await callKeyserverEndpoint('update_activity', requests); - let unfocusedToUnread = []; + let unfocusedToUnread: $ReadOnlyArray = []; for (const keyserverID in responses) { unfocusedToUnread = unfocusedToUnread.concat( responses[keyserverID].unfocusedToUnread, ); } const sortedActivityUpdates: { [keyserverID: string]: $ReadOnlyArray, } = {}; for (const keyserverID in requests) { sortedActivityUpdates[keyserverID] = requests[keyserverID].updates; } return { activityUpdates: sortedActivityUpdates, result: { unfocusedToUnread, }, }; }; function useUpdateActivity(): ( input: UpdateActivityInput, ) => Promise { return useKeyserverCall(updateActivity); } const setThreadUnreadStatusActionTypes = Object.freeze({ started: 'SET_THREAD_UNREAD_STATUS_STARTED', success: 'SET_THREAD_UNREAD_STATUS_SUCCESS', failed: 'SET_THREAD_UNREAD_STATUS_FAILED', }); const setThreadUnreadStatus = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: SetThreadUnreadStatusRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'set_thread_unread_status', requests, ); return { resetToUnread: responses[keyserverID].resetToUnread, threadID: input.threadID, }; }; function useSetThreadUnreadStatus(): ( request: SetThreadUnreadStatusRequest, ) => Promise { return useKeyserverCall(setThreadUnreadStatus); } export { updateActivityActionTypes, useUpdateActivity, setThreadUnreadStatusActionTypes, useSetThreadUnreadStatus, }; diff --git a/lib/actions/device-actions.js b/lib/actions/device-actions.js index 297cc76bf..e7da3f016 100644 --- a/lib/actions/device-actions.js +++ b/lib/actions/device-actions.js @@ -1,123 +1,126 @@ // @flow -import type { GetVersionActionPayload } from '../types/device-types'; +import type { + GetVersionActionPayload, + DeviceTokenUpdateRequest, +} from '../types/device-types'; import { getConfig } from '../utils/config.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; import type { CallKeyserverEndpoint, KeyserverCallParamOverride, } from '../utils/keyserver-call.js'; export type DeviceTokens = { +[keyserverID: string]: ?string, }; export type SetDeviceTokenActionPayload = { +deviceTokens: DeviceTokens, }; const setDeviceTokenActionTypes = Object.freeze({ started: 'SET_DEVICE_TOKEN_STARTED', success: 'SET_DEVICE_TOKEN_SUCCESS', failed: 'SET_DEVICE_TOKEN_FAILED', }); const setDeviceToken = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: DeviceTokens) => Promise) => async input => { - const requests = {}; + const requests: { [string]: DeviceTokenUpdateRequest } = {}; for (const keyserverID in input) { requests[keyserverID] = { deviceToken: input[keyserverID], platformDetails: getConfig().platformDetails, }; } await callKeyserverEndpoint('update_device_token', requests); return { deviceTokens: input }; }; function useSetDeviceToken(): ( input: DeviceTokens, ) => Promise { return useKeyserverCall(setDeviceToken); } const setDeviceTokenFanout = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: ?string) => Promise) => async input => { - const requests = {}; - const deviceTokens = {}; + const requests: { [string]: DeviceTokenUpdateRequest } = {}; + const deviceTokens: { [string]: ?string } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = { deviceToken: input, platformDetails: getConfig().platformDetails, }; deviceTokens[keyserverID] = input; } await callKeyserverEndpoint('update_device_token', requests); return { deviceTokens }; }; function useSetDeviceTokenFanout(): ( input: ?string, ) => Promise { return useKeyserverCall(setDeviceTokenFanout); } const getVersionActionTypes = Object.freeze({ started: 'GET_VERSION_STARTED', success: 'GET_VERSION_SUCCESS', failed: 'GET_VERSION_FAILED', }); // useGetVersion should be passed paramOverride containing keyserverInfos // with entries for the keyservers we want to connect to. // They should contain the urlPrefix of the keyserver. // The key should also be the urlPrefix, // since the id of the keyserver is not yet known. const getVersion = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): (() => Promise) => async () => { - const requests = {}; + const requests: { [string]: {} } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = {}; } const responses = await callKeyserverEndpoint('version', requests); const result = { versionResponses: {} }; for (const keyserverID in responses) { const { codeVersion, ownerUsername, ownerID } = responses[keyserverID]; result.versionResponses[ownerID] = { codeVersion, ownerUsername, ownerID, }; } return result; }; function useGetVersion( paramOverride?: ?KeyserverCallParamOverride, ): () => Promise { return useKeyserverCall(getVersion, paramOverride); } const updateLastCommunicatedPlatformDetailsActionType = 'UPDATE_LAST_COMMUNICATED_PLATFORM_DETAILS'; export { setDeviceTokenActionTypes, useSetDeviceToken, useSetDeviceTokenFanout, getVersionActionTypes, useGetVersion, updateLastCommunicatedPlatformDetailsActionType, }; diff --git a/lib/actions/entry-actions.js b/lib/actions/entry-actions.js index 9713a46ce..a52150c9e 100644 --- a/lib/actions/entry-actions.js +++ b/lib/actions/entry-actions.js @@ -1,344 +1,344 @@ // @flow import { localIDPrefix } from '../shared/message-utils.js'; import type { RawEntryInfo, CalendarQuery, SaveEntryInfo, SaveEntryResult, CreateEntryInfo, CreateEntryPayload, DeleteEntryInfo, DeleteEntryResult, RestoreEntryInfo, RestoreEntryResult, FetchEntryInfosResult, CalendarQueryUpdateResult, } from '../types/entry-types.js'; import type { HistoryRevisionInfo } from '../types/history-types.js'; import { extractKeyserverIDFromID, sortCalendarQueryPerKeyserver, } from '../utils/action-utils.js'; import { dateFromString } from '../utils/date-utils.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; import type { CallKeyserverEndpoint } from '../utils/keyserver-call.js'; const fetchEntriesActionTypes = Object.freeze({ started: 'FETCH_ENTRIES_STARTED', success: 'FETCH_ENTRIES_SUCCESS', failed: 'FETCH_ENTRIES_FAILED', }); const fetchEntries = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((calendarQuery: CalendarQuery) => Promise) => async calendarQuery => { const calendarQueries = sortCalendarQueryPerKeyserver( calendarQuery, allKeyserverIDs, ); - const requests = {}; + const requests: { [string]: CalendarQuery } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = calendarQueries[keyserverID]; } const responses = await callKeyserverEndpoint('fetch_entries', requests); - let rawEntryInfos = []; + let rawEntryInfos: $ReadOnlyArray = []; for (const keyserverID in responses) { rawEntryInfos = rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } return { rawEntryInfos, }; }; function useFetchEntries(): ( calendarQuery: CalendarQuery, ) => Promise { return useKeyserverCall(fetchEntries); } export type UpdateCalendarQueryInput = { +calendarQuery: CalendarQuery, +reduxAlreadyUpdated?: boolean, }; const updateCalendarQueryActionTypes = Object.freeze({ started: 'UPDATE_CALENDAR_QUERY_STARTED', success: 'UPDATE_CALENDAR_QUERY_SUCCESS', failed: 'UPDATE_CALENDAR_QUERY_FAILED', }); const updateCalendarQuery = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): (( input: UpdateCalendarQueryInput, ) => Promise) => async input => { const { calendarQuery, reduxAlreadyUpdated = false } = input; const calendarQueries = sortCalendarQueryPerKeyserver( calendarQuery, allKeyserverIDs, ); - const requests = {}; + const requests: { [string]: CalendarQuery } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = calendarQueries[keyserverID]; } const responses = await callKeyserverEndpoint( 'update_calendar_query', requests, ); - let rawEntryInfos = []; - let deletedEntryIDs = []; + let rawEntryInfos: $ReadOnlyArray = []; + let deletedEntryIDs: $ReadOnlyArray = []; for (const keyserverID in responses) { rawEntryInfos = rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); deletedEntryIDs = deletedEntryIDs.concat( responses[keyserverID].deletedEntryIDs, ); } return { rawEntryInfos, deletedEntryIDs, calendarQuery, calendarQueryAlreadyUpdated: reduxAlreadyUpdated, }; }; function useUpdateCalendarQuery(): ( input: UpdateCalendarQueryInput, ) => Promise { return useKeyserverCall(updateCalendarQuery); } const createLocalEntryActionType = 'CREATE_LOCAL_ENTRY'; function createLocalEntry( threadID: string, localID: number, dateString: string, creatorID: string, ): RawEntryInfo { const date = dateFromString(dateString); const newEntryInfo: RawEntryInfo = { localID: `${localIDPrefix}${localID}`, threadID, text: '', year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), creationTime: Date.now(), creatorID, deleted: false, }; return newEntryInfo; } const createEntryActionTypes = Object.freeze({ started: 'CREATE_ENTRY_STARTED', success: 'CREATE_ENTRY_SUCCESS', failed: 'CREATE_ENTRY_FAILED', }); const createEntry = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: CreateEntryInfo) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const calendarQueries = sortCalendarQueryPerKeyserver(input.calendarQuery, [ keyserverID, ]); const requests = { [keyserverID]: { ...input, calendarQuery: calendarQueries[keyserverID], }, }; const response = await callKeyserverEndpoint('create_entry', requests); return { entryID: response[keyserverID].entryID, newMessageInfos: response[keyserverID].newMessageInfos, threadID: response[keyserverID].threadID, localID: response[keyserverID].localID, updatesResult: response[keyserverID].updatesResult, }; }; function useCreateEntry(): ( request: CreateEntryInfo, ) => Promise { return useKeyserverCall(createEntry); } const saveEntryActionTypes = Object.freeze({ started: 'SAVE_ENTRY_STARTED', success: 'SAVE_ENTRY_SUCCESS', failed: 'SAVE_ENTRY_FAILED', }); const concurrentModificationResetActionType = 'CONCURRENT_MODIFICATION_RESET'; const saveEntry = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SaveEntryInfo) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.entryID); const calendarQueries = sortCalendarQueryPerKeyserver(input.calendarQuery, [ keyserverID, ]); const requests = { [keyserverID]: { ...input, calendarQuery: calendarQueries[keyserverID], }, }; const response = await callKeyserverEndpoint('update_entry', requests); return { entryID: response[keyserverID].entryID, newMessageInfos: response[keyserverID].newMessageInfos, updatesResult: response[keyserverID].updatesResult, }; }; function useSaveEntry(): (input: SaveEntryInfo) => Promise { return useKeyserverCall(saveEntry); } const deleteEntryActionTypes = Object.freeze({ started: 'DELETE_ENTRY_STARTED', success: 'DELETE_ENTRY_SUCCESS', failed: 'DELETE_ENTRY_FAILED', }); const deleteEntry = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((info: DeleteEntryInfo) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.entryID); const calendarQueries = sortCalendarQueryPerKeyserver(input.calendarQuery, [ keyserverID, ]); const requests = { [keyserverID]: { ...input, calendarQuery: calendarQueries[keyserverID], timestamp: Date.now(), }, }; const response = await callKeyserverEndpoint('delete_entry', requests); return { newMessageInfos: response[keyserverID].newMessageInfos, threadID: response[keyserverID].threadID, updatesResult: response[keyserverID].updatesResult, }; }; function useDeleteEntry(): ( info: DeleteEntryInfo, ) => Promise { return useKeyserverCall(deleteEntry); } export type FetchRevisionsForEntryInput = { +entryID: string, }; const fetchRevisionsForEntryActionTypes = Object.freeze({ started: 'FETCH_REVISIONS_FOR_ENTRY_STARTED', success: 'FETCH_REVISIONS_FOR_ENTRY_SUCCESS', failed: 'FETCH_REVISIONS_FOR_ENTRY_FAILED', }); const fetchRevisionsForEntry = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: FetchRevisionsForEntryInput, ) => Promise<$ReadOnlyArray>) => async input => { const keyserverID = extractKeyserverIDFromID(input.entryID); const requests = { [keyserverID]: { id: input.entryID, }, }; const response = await callKeyserverEndpoint( 'fetch_entry_revisions', requests, ); return response[keyserverID].result; }; function useFetchRevisionsForEntry(): ( input: FetchRevisionsForEntryInput, ) => Promise<$ReadOnlyArray> { return useKeyserverCall(fetchRevisionsForEntry); } const restoreEntryActionTypes = Object.freeze({ started: 'RESTORE_ENTRY_STARTED', success: 'RESTORE_ENTRY_SUCCESS', failed: 'RESTORE_ENTRY_FAILED', }); const restoreEntry = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: RestoreEntryInfo) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.entryID); const calendarQueries = sortCalendarQueryPerKeyserver(input.calendarQuery, [ keyserverID, ]); const requests = { [keyserverID]: { ...input, calendarQuery: calendarQueries[keyserverID], timestamp: Date.now(), }, }; const response = await callKeyserverEndpoint('restore_entry', requests); return { newMessageInfos: response[keyserverID].newMessageInfos, updatesResult: response[keyserverID].updatesResult, }; }; function useRestoreEntry(): ( input: RestoreEntryInfo, ) => Promise { return useKeyserverCall(restoreEntry); } export { fetchEntriesActionTypes, useFetchEntries, updateCalendarQueryActionTypes, useUpdateCalendarQuery, createLocalEntryActionType, createLocalEntry, createEntryActionTypes, useCreateEntry, saveEntryActionTypes, concurrentModificationResetActionType, useSaveEntry, deleteEntryActionTypes, useDeleteEntry, fetchRevisionsForEntryActionTypes, useFetchRevisionsForEntry, restoreEntryActionTypes, useRestoreEntry, }; diff --git a/lib/actions/link-actions.js b/lib/actions/link-actions.js index a5dd9d937..913167a50 100644 --- a/lib/actions/link-actions.js +++ b/lib/actions/link-actions.js @@ -1,147 +1,147 @@ // @flow import type { FetchInviteLinksResponse, InviteLinkVerificationRequest, InviteLinkVerificationResponse, CreateOrUpdatePublicLinkRequest, InviteLink, DisableInviteLinkRequest, DisableInviteLinkPayload, } from '../types/link-types.js'; import { extractKeyserverIDFromID } from '../utils/action-utils.js'; import type { CallServerEndpoint } from '../utils/call-server-endpoint.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; import type { CallKeyserverEndpoint } from '../utils/keyserver-call.js'; const verifyInviteLinkActionTypes = Object.freeze({ started: 'VERIFY_INVITE_LINK_STARTED', success: 'VERIFY_INVITE_LINK_SUCCESS', failed: 'VERIFY_INVITE_LINK_FAILED', }); const verifyInviteLink = ( callServerEndpoint: CallServerEndpoint, ): (( request: InviteLinkVerificationRequest, ) => Promise) => async request => { const response = await callServerEndpoint('verify_invite_link', request); if (response.status === 'valid' || response.status === 'already_joined') { return { status: response.status, community: response.community, }; } return { status: response.status, }; }; const fetchPrimaryInviteLinkActionTypes = Object.freeze({ started: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', success: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', failed: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', }); const fetchPrimaryInviteLinks = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): (() => Promise) => async () => { - const requests = {}; + const requests: { [string]: void } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = undefined; } const responses = await callKeyserverEndpoint( 'fetch_primary_invite_links', requests, ); - let links = []; + let links: $ReadOnlyArray = []; for (const keyserverID in responses) { links = links.concat(responses[keyserverID].links); } return { links, }; }; function useFetchPrimaryInviteLinks(): () => Promise { return useKeyserverCall(fetchPrimaryInviteLinks); } const createOrUpdatePublicLinkActionTypes = Object.freeze({ started: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', success: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', failed: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', }); const createOrUpdatePublicLink = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: CreateOrUpdatePublicLinkRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.communityID); const requests = { [keyserverID]: { name: input.name, communityID: input.communityID, }, }; const responses = await callKeyserverEndpoint( 'create_or_update_public_link', requests, ); const response = responses[keyserverID]; return { name: response.name, primary: response.primary, role: response.role, communityID: response.communityID, expirationTime: response.expirationTime, limitOfUses: response.limitOfUses, numberOfUses: response.numberOfUses, }; }; function useCreateOrUpdatePublicLink(): ( input: CreateOrUpdatePublicLinkRequest, ) => Promise { return useKeyserverCall(createOrUpdatePublicLink); } const disableInviteLinkLinkActionTypes = Object.freeze({ started: 'DISABLE_INVITE_LINK_STARTED', success: 'DISABLE_INVITE_LINK_SUCCESS', failed: 'DISABLE_INVITE_LINK_FAILED', }); const disableInviteLink = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: DisableInviteLinkRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.communityID); const requests = { [keyserverID]: input }; await callKeyserverEndpoint('disable_invite_link', requests); return input; }; function useDisableInviteLink(): ( input: DisableInviteLinkRequest, ) => Promise { return useKeyserverCall(disableInviteLink); } export { verifyInviteLinkActionTypes, verifyInviteLink, fetchPrimaryInviteLinkActionTypes, useFetchPrimaryInviteLinks, createOrUpdatePublicLinkActionTypes, useCreateOrUpdatePublicLink, disableInviteLinkLinkActionTypes, useDisableInviteLink, }; diff --git a/lib/actions/message-actions.js b/lib/actions/message-actions.js index 0e3515cd9..ac7f0ccfe 100644 --- a/lib/actions/message-actions.js +++ b/lib/actions/message-actions.js @@ -1,519 +1,522 @@ // @flow import invariant from 'invariant'; import type { FetchMessageInfosPayload, SendMessageResult, SendEditMessageResult, SendReactionMessageRequest, SimpleMessagesPayload, SendEditMessageRequest, FetchPinnedMessagesRequest, FetchPinnedMessagesResult, SearchMessagesRequest, SearchMessagesResponse, + FetchMessageInfosRequest, + RawMessageInfo, + MessageTruncationStatuses, } from '../types/message-types.js'; import type { MediaMessageServerDBContent } from '../types/messages/media.js'; import type { ToggleMessagePinRequest, ToggleMessagePinResult, } from '../types/thread-types.js'; import { extractKeyserverIDFromID, sortThreadIDsPerKeyserver, } from '../utils/action-utils.js'; import type { CallServerEndpointResultInfo } from '../utils/call-server-endpoint.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; import type { CallKeyserverEndpoint } from '../utils/keyserver-call.js'; const fetchMessagesBeforeCursorActionTypes = Object.freeze({ started: 'FETCH_MESSAGES_BEFORE_CURSOR_STARTED', success: 'FETCH_MESSAGES_BEFORE_CURSOR_SUCCESS', failed: 'FETCH_MESSAGES_BEFORE_CURSOR_FAILED', }); export type FetchMessagesBeforeCursorInput = { +threadID: string, +beforeMessageID: string, }; const fetchMessagesBeforeCursor = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: FetchMessagesBeforeCursorInput, ) => Promise) => async input => { const { threadID, beforeMessageID } = input; const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: { cursors: { [threadID]: beforeMessageID, }, }, }; const responses = await callKeyserverEndpoint('fetch_messages', requests); return { threadID, rawMessageInfos: responses[keyserverID].rawMessageInfos, truncationStatus: responses[keyserverID].truncationStatuses[threadID], }; }; function useFetchMessagesBeforeCursor(): ( input: FetchMessagesBeforeCursorInput, ) => Promise { return useKeyserverCall(fetchMessagesBeforeCursor); } export type FetchMostRecentMessagesInput = { +threadID: string, }; const fetchMostRecentMessagesActionTypes = Object.freeze({ started: 'FETCH_MOST_RECENT_MESSAGES_STARTED', success: 'FETCH_MOST_RECENT_MESSAGES_SUCCESS', failed: 'FETCH_MOST_RECENT_MESSAGES_FAILED', }); const fetchMostRecentMessages = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: FetchMostRecentMessagesInput, ) => Promise) => async input => { const { threadID } = input; const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: { cursors: { [threadID]: null, }, }, }; const responses = await callKeyserverEndpoint('fetch_messages', requests); return { threadID, rawMessageInfos: responses[keyserverID].rawMessageInfos, truncationStatus: responses[keyserverID].truncationStatuses[threadID], }; }; function useFetchMostRecentMessages(): ( input: FetchMostRecentMessagesInput, ) => Promise { return useKeyserverCall(fetchMostRecentMessages); } const fetchSingleMostRecentMessagesFromThreadsActionTypes = Object.freeze({ started: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_STARTED', success: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_SUCCESS', failed: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_FAILED', }); const fetchSingleMostRecentMessagesFromThreads = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((threadIDs: $ReadOnlyArray) => Promise) => async threadIDs => { const sortedThreadIDs = sortThreadIDsPerKeyserver(threadIDs); - const requests = {}; + const requests: { [string]: FetchMessageInfosRequest } = {}; for (const keyserverID in sortedThreadIDs) { const cursors = Object.fromEntries( sortedThreadIDs[keyserverID].map(threadID => [threadID, null]), ); requests[keyserverID] = { cursors, numberPerThread: 1, }; } const responses = await callKeyserverEndpoint('fetch_messages', requests); - let rawMessageInfos = []; - let truncationStatuses = {}; + let rawMessageInfos: $ReadOnlyArray = []; + let truncationStatuses: MessageTruncationStatuses = {}; for (const keyserverID in responses) { rawMessageInfos = rawMessageInfos.concat( responses[keyserverID].rawMessageInfos, ); truncationStatuses = { ...truncationStatuses, ...responses[keyserverID].truncationStatuses, }; } return { rawMessageInfos, truncationStatuses, }; }; function useFetchSingleMostRecentMessagesFromThreads(): ( threadIDs: $ReadOnlyArray, ) => Promise { return useKeyserverCall(fetchSingleMostRecentMessagesFromThreads); } export type SendTextMessageInput = { +threadID: string, +localID: string, +text: string, +sidebarCreation?: boolean, }; const sendTextMessageActionTypes = Object.freeze({ started: 'SEND_TEXT_MESSAGE_STARTED', success: 'SEND_TEXT_MESSAGE_SUCCESS', failed: 'SEND_TEXT_MESSAGE_FAILED', }); const sendTextMessage = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SendTextMessageInput) => Promise) => async input => { let resultInfo; const getResultInfo = (passedResultInfo: CallServerEndpointResultInfo) => { resultInfo = passedResultInfo; }; const { threadID, localID, text, sidebarCreation } = input; let payload = { threadID, localID, text }; if (sidebarCreation) { payload = { ...payload, sidebarCreation }; } const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: payload }; const responses = await callKeyserverEndpoint( 'create_text_message', requests, { getResultInfo, }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callKeyserverEndpoint resolves', ); return { id: responses[keyserverID].newMessageInfo.id, time: responses[keyserverID].newMessageInfo.time, interface: resultInterface, }; }; function useSendTextMessage(): ( input: SendTextMessageInput, ) => Promise { return useKeyserverCall(sendTextMessage); } const createLocalMessageActionType = 'CREATE_LOCAL_MESSAGE'; export type SendMultimediaMessageInput = { +threadID: string, +localID: string, +mediaMessageContents: $ReadOnlyArray, +sidebarCreation?: boolean, }; const sendMultimediaMessageActionTypes = Object.freeze({ started: 'SEND_MULTIMEDIA_MESSAGE_STARTED', success: 'SEND_MULTIMEDIA_MESSAGE_SUCCESS', failed: 'SEND_MULTIMEDIA_MESSAGE_FAILED', }); const sendMultimediaMessage = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SendMultimediaMessageInput) => Promise) => async input => { let resultInfo; const getResultInfo = (passedResultInfo: CallServerEndpointResultInfo) => { resultInfo = passedResultInfo; }; const { threadID, localID, mediaMessageContents, sidebarCreation } = input; let payload = { threadID, localID, mediaMessageContents }; if (sidebarCreation) { payload = { ...payload, sidebarCreation }; } const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: payload }; const responses = await callKeyserverEndpoint( 'create_multimedia_message', requests, { getResultInfo }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callKeyserverEndpoint resolves', ); return { id: responses[keyserverID].newMessageInfo.id, time: responses[keyserverID].newMessageInfo.time, interface: resultInterface, }; }; function useSendMultimediaMessage(): ( input: SendMultimediaMessageInput, ) => Promise { return useKeyserverCall(sendMultimediaMessage); } export type LegacySendMultimediaMessageInput = { +threadID: string, +localID: string, +mediaIDs: $ReadOnlyArray, +sidebarCreation?: boolean, }; const legacySendMultimediaMessage = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: LegacySendMultimediaMessageInput, ) => Promise) => async input => { let resultInfo; const getResultInfo = (passedResultInfo: CallServerEndpointResultInfo) => { resultInfo = passedResultInfo; }; const { threadID, localID, mediaIDs, sidebarCreation } = input; let payload = { threadID, localID, mediaIDs }; if (sidebarCreation) { payload = { ...payload, sidebarCreation }; } const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: payload }; const responses = await callKeyserverEndpoint( 'create_multimedia_message', requests, { getResultInfo }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callKeyserverEndpoint resolves', ); return { id: responses[keyserverID].newMessageInfo.id, time: responses[keyserverID].newMessageInfo.time, interface: resultInterface, }; }; function useLegacySendMultimediaMessage(): ( input: LegacySendMultimediaMessageInput, ) => Promise { return useKeyserverCall(legacySendMultimediaMessage); } const sendReactionMessageActionTypes = Object.freeze({ started: 'SEND_REACTION_MESSAGE_STARTED', success: 'SEND_REACTION_MESSAGE_SUCCESS', failed: 'SEND_REACTION_MESSAGE_FAILED', }); const sendReactionMessage = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SendReactionMessageRequest) => Promise) => async input => { let resultInfo; const getResultInfo = (passedResultInfo: CallServerEndpointResultInfo) => { resultInfo = passedResultInfo; }; const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: { threadID: input.threadID, localID: input.localID, targetMessageID: input.targetMessageID, reaction: input.reaction, action: input.action, }, }; const responses = await callKeyserverEndpoint( 'create_reaction_message', requests, { getResultInfo }, ); const resultInterface = resultInfo?.interface; invariant( resultInterface, 'getResultInfo not called before callKeyserverEndpoint resolves', ); return { id: responses[keyserverID].newMessageInfo.id, time: responses[keyserverID].newMessageInfo.time, interface: resultInterface, }; }; function useSendReactionMessage(): ( input: SendReactionMessageRequest, ) => Promise { return useKeyserverCall(sendReactionMessage); } const sendEditMessageActionTypes = Object.freeze({ started: 'SEND_EDIT_MESSAGE_STARTED', success: 'SEND_EDIT_MESSAGE_SUCCESS', failed: 'SEND_EDIT_MESSAGE_FAILED', }); const sendEditMessage = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SendEditMessageRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.targetMessageID); const requests = { [keyserverID]: { targetMessageID: input.targetMessageID, text: input.text, }, }; const responses = await callKeyserverEndpoint('edit_message', requests); return { newMessageInfos: responses[keyserverID].newMessageInfos, }; }; function useSendEditMessage(): ( input: SendEditMessageRequest, ) => Promise { return useKeyserverCall(sendEditMessage); } const saveMessagesActionType = 'SAVE_MESSAGES'; const processMessagesActionType = 'PROCESS_MESSAGES'; const messageStorePruneActionType = 'MESSAGE_STORE_PRUNE'; const fetchPinnedMessageActionTypes = Object.freeze({ started: 'FETCH_PINNED_MESSAGES_STARTED', success: 'FETCH_PINNED_MESSAGES_SUCCESS', failed: 'FETCH_PINNED_MESSAGES_FAILED', }); const fetchPinnedMessages = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: FetchPinnedMessagesRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'fetch_pinned_messages', requests, ); return { pinnedMessages: responses[keyserverID].pinnedMessages }; }; function useFetchPinnedMessages(): ( input: FetchPinnedMessagesRequest, ) => Promise { return useKeyserverCall(fetchPinnedMessages); } const searchMessagesActionTypes = Object.freeze({ started: 'SEARCH_MESSAGES_STARTED', success: 'SEARCH_MESSAGES_SUCCESS', failed: 'SEARCH_MESSAGES_FAILED', }); const searchMessages = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: SearchMessagesRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint('search_messages', requests); return { messages: responses[keyserverID].messages, endReached: responses[keyserverID].endReached, }; }; function useSearchMessages(): ( input: SearchMessagesRequest, ) => Promise { return useKeyserverCall(searchMessages); } const toggleMessagePinActionTypes = Object.freeze({ started: 'TOGGLE_MESSAGE_PIN_STARTED', success: 'TOGGLE_MESSAGE_PIN_SUCCESS', failed: 'TOGGLE_MESSAGE_PIN_FAILED', }); const toggleMessagePin = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: ToggleMessagePinRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.messageID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'toggle_message_pin', requests, ); const response = responses[keyserverID]; return { newMessageInfos: response.newMessageInfos, threadID: response.threadID, }; }; function useToggleMessagePin(): ( input: ToggleMessagePinRequest, ) => Promise { return useKeyserverCall(toggleMessagePin); } export { fetchMessagesBeforeCursorActionTypes, useFetchMessagesBeforeCursor, fetchMostRecentMessagesActionTypes, useFetchMostRecentMessages, fetchSingleMostRecentMessagesFromThreadsActionTypes, useFetchSingleMostRecentMessagesFromThreads, sendTextMessageActionTypes, useSendTextMessage, createLocalMessageActionType, sendMultimediaMessageActionTypes, useSendMultimediaMessage, useLegacySendMultimediaMessage, searchMessagesActionTypes, useSearchMessages, sendReactionMessageActionTypes, useSendReactionMessage, saveMessagesActionType, processMessagesActionType, messageStorePruneActionType, sendEditMessageActionTypes, useSendEditMessage, useFetchPinnedMessages, fetchPinnedMessageActionTypes, toggleMessagePinActionTypes, useToggleMessagePin, }; diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js index 527df3594..2e1da9479 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,510 +1,529 @@ // @flow import threadWatcher from '../shared/thread-watcher.js'; import type { LogOutResult, LogInInfo, LogInResult, RegisterResult, RegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameResponse, LogInResponse, + LogInRequest, } from '../types/account-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from '../types/avatar-types.js'; +import type { RawEntryInfo, CalendarQuery } from '../types/entry-types.js'; +import type { + RawMessageInfo, + MessageTruncationStatuses, +} from '../types/message-types.js'; import type { GetSessionPublicKeysArgs, GetOlmSessionInitializationDataResponse, } from '../types/request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from '../types/search-types.js'; import type { SessionPublicKeys, PreRequestUserState, } from '../types/session-types.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; import type { RawThreadInfos } from '../types/thread-types'; import type { UserInfo, PasswordUpdate, LoggedOutUserInfo, } from '../types/user-types.js'; import { extractKeyserverIDFromID, sortThreadIDsPerKeyserver, sortCalendarQueryPerKeyserver, } from '../utils/action-utils.js'; import type { CallServerEndpoint, CallServerEndpointOptions, } from '../utils/call-server-endpoint.js'; import { getConfig } from '../utils/config.js'; import type { CallKeyserverEndpoint } from '../utils/keyserver-call'; import { useKeyserverCall } from '../utils/keyserver-call.js'; import sleep from '../utils/sleep.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; const loggedOutUserInfo: LoggedOutUserInfo = { anonymous: true, }; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const logOut = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: PreRequestUserState) => Promise) => async preRequestUserState => { - const requests = {}; + const requests: { [string]: {} } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = {}; } let response = null; try { response = await Promise.race([ callKeyserverEndpoint('log_out', requests), (async () => { await sleep(500); throw new Error('log_out took more than 500ms'); })(), ]); } catch {} const currentUserInfo = response ? loggedOutUserInfo : null; return { currentUserInfo, preRequestUserState }; }; function useLogOut(): (input: PreRequestUserState) => Promise { return useKeyserverCall(logOut); } const claimUsernameActionTypes = Object.freeze({ started: 'CLAIM_USERNAME_STARTED', success: 'CLAIM_USERNAME_SUCCESS', failed: 'CLAIM_USERNAME_FAILED', }); const claimUsernameCallServerEndpointOptions = { timeout: 500 }; const claimUsername = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (() => Promise) => async () => { const requests = { [ashoatKeyserverID]: {} }; const responses = await callKeyserverEndpoint('claim_username', requests, { ...claimUsernameCallServerEndpointOptions, }); const response = responses[ashoatKeyserverID]; return { message: response.message, signature: response.signature, }; }; function useClaimUsername(): () => Promise { return useKeyserverCall(claimUsername); } const deleteAccountActionTypes = Object.freeze({ started: 'DELETE_ACCOUNT_STARTED', success: 'DELETE_ACCOUNT_SUCCESS', failed: 'DELETE_ACCOUNT_FAILED', }); const deleteAccount = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: PreRequestUserState) => Promise) => async preRequestUserState => { - const requests = {}; + const requests: { [string]: {} } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = {}; } await callKeyserverEndpoint('delete_account', requests); return { currentUserInfo: loggedOutUserInfo, preRequestUserState }; }; function useDeleteAccount(): ( input: PreRequestUserState, ) => Promise { return useKeyserverCall(deleteAccount); } const registerActionTypes = Object.freeze({ started: 'REGISTER_STARTED', success: 'REGISTER_SUCCESS', failed: 'REGISTER_FAILED', }); const registerCallServerEndpointOptions = { timeout: 60000 }; const register = ( callServerEndpoint: CallServerEndpoint, ): (( registerInfo: RegisterInfo, options?: CallServerEndpointOptions, ) => Promise) => async (registerInfo, options) => { const deviceTokenUpdateRequest = registerInfo.deviceTokenUpdateRequest[ashoatKeyserverID]; const response = await callServerEndpoint( 'create_account', { ...registerInfo, deviceTokenUpdateRequest, platformDetails: getConfig().platformDetails, }, { ...registerCallServerEndpointOptions, ...options, }, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; -function mergeUserInfos(...userInfoArrays: UserInfo[][]): UserInfo[] { +function mergeUserInfos( + ...userInfoArrays: Array<$ReadOnlyArray> +): UserInfo[] { const merged: { [string]: UserInfo } = {}; for (const userInfoArray of userInfoArrays) { for (const userInfo of userInfoArray) { merged[userInfo.id] = userInfo; } } const flattened = []; for (const id in merged) { flattened.push(merged[id]); } return flattened; } +type WritableGenericMessagesResult = { + messageInfos: RawMessageInfo[], + truncationStatus: MessageTruncationStatuses, + watchedIDsAtRequestTime: string[], + currentAsOf: { [keyserverID: string]: number }, +}; +type WritableCalendarResult = { + rawEntryInfos: RawEntryInfo[], + calendarQuery: CalendarQuery, +}; + const logInActionTypes = Object.freeze({ started: 'LOG_IN_STARTED', success: 'LOG_IN_SUCCESS', failed: 'LOG_IN_FAILED', }); const logInCallServerEndpointOptions = { timeout: 60000 }; const logIn = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: LogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { logInActionSource, calendarQuery, keyserverIDs: inputKeyserverIDs, ...restLogInInfo } = logInInfo; // Eventually the list of keyservers will be fetched from the // identity service const keyserverIDs = inputKeyserverIDs ?? [ashoatKeyserverID]; const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); - const requests = {}; + const requests: { [string]: LogInRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: logInInfo.deviceTokenUpdateRequest[keyserverID], source: logInActionSource, watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, }; } const responses: { +[string]: LogInResponse } = await callKeyserverEndpoint( 'log_in', requests, logInCallServerEndpointOptions, ); const userInfosArrays = []; let threadInfos: RawThreadInfos = {}; - const calendarResult = { + const calendarResult: WritableCalendarResult = { calendarQuery: logInInfo.calendarQuery, rawEntryInfos: [], }; - const messagesResult = { + const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; userInfosArrays.push(responses[keyserverID].userInfos); userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); } const userInfos = mergeUserInfos(...userInfosArrays); return { threadInfos, currentUserInfo: responses[ashoatKeyserverID].currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, logInActionSource: logInInfo.logInActionSource, notAcknowledgedPolicies: responses[ashoatKeyserverID].notAcknowledgedPolicies, }; }; function useLogIn(): (input: LogInInfo) => Promise { return useKeyserverCall(logIn); } const changeUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_USER_PASSWORD_STARTED', success: 'CHANGE_USER_PASSWORD_SUCCESS', failed: 'CHANGE_USER_PASSWORD_FAILED', }); const changeUserPassword = ( callServerEndpoint: CallServerEndpoint, ): ((passwordUpdate: PasswordUpdate) => Promise) => async passwordUpdate => { await callServerEndpoint('update_account', passwordUpdate); }; const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( callServerEndpoint: CallServerEndpoint, ): ((usernamePrefix: string) => Promise) => async usernamePrefix => { const response = await callServerEndpoint('search_users', { prefix: usernamePrefix, }); return { userInfos: response.userInfos, }; }; const exactSearchUserActionTypes = Object.freeze({ started: 'EXACT_SEARCH_USER_STARTED', success: 'EXACT_SEARCH_USER_SUCCESS', failed: 'EXACT_SEARCH_USER_FAILED', }); const exactSearchUser = ( callServerEndpoint: CallServerEndpoint, ): ((username: string) => Promise) => async username => { const response = await callServerEndpoint('exact_search_user', { username, }); return { userInfo: response.userInfo, }; }; const updateSubscriptionActionTypes = Object.freeze({ started: 'UPDATE_SUBSCRIPTION_STARTED', success: 'UPDATE_SUBSCRIPTION_SUCCESS', failed: 'UPDATE_SUBSCRIPTION_FAILED', }); const updateSubscription = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: SubscriptionUpdateRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'update_user_subscription', requests, ); const response = responses[keyserverID]; return { threadID: input.threadID, subscription: response.threadSubscription, }; }; function useUpdateSubscription(): ( input: SubscriptionUpdateRequest, ) => Promise { return useKeyserverCall(updateSubscription); } const setUserSettingsActionTypes = Object.freeze({ started: 'SET_USER_SETTINGS_STARTED', success: 'SET_USER_SETTINGS_SUCCESS', failed: 'SET_USER_SETTINGS_FAILED', }); const setUserSettings = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: UpdateUserSettingsRequest) => Promise) => async input => { - const requests = {}; + const requests: { [string]: UpdateUserSettingsRequest } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = input; } await callKeyserverEndpoint('update_user_settings', requests); }; function useSetUserSettings(): ( input: UpdateUserSettingsRequest, ) => Promise { return useKeyserverCall(setUserSettings); } const getSessionPublicKeys = ( callServerEndpoint: CallServerEndpoint, ): ((data: GetSessionPublicKeysArgs) => Promise) => async data => { return await callServerEndpoint('get_session_public_keys', data); }; const getOlmSessionInitializationDataActionTypes = Object.freeze({ started: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', success: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', failed: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', }); const getOlmSessionInitializationData = ( callServerEndpoint: CallServerEndpoint, ): (( options?: ?CallServerEndpointOptions, ) => Promise) => async options => { return await callServerEndpoint( 'get_olm_session_initialization_data', {}, options, ); }; const policyAcknowledgmentActionTypes = Object.freeze({ started: 'POLICY_ACKNOWLEDGMENT_STARTED', success: 'POLICY_ACKNOWLEDGMENT_SUCCESS', failed: 'POLICY_ACKNOWLEDGMENT_FAILED', }); const policyAcknowledgment = ( callServerEndpoint: CallServerEndpoint, ): ((policyRequest: PolicyAcknowledgmentRequest) => Promise) => async policyRequest => { await callServerEndpoint('policy_acknowledgment', policyRequest); }; const updateUserAvatarActionTypes = Object.freeze({ started: 'UPDATE_USER_AVATAR_STARTED', success: 'UPDATE_USER_AVATAR_SUCCESS', failed: 'UPDATE_USER_AVATAR_FAILED', }); const updateUserAvatar = ( callServerEndpoint: CallServerEndpoint, ): (( avatarDBContent: UpdateUserAvatarRequest, ) => Promise) => async avatarDBContent => { const { updates }: UpdateUserAvatarResponse = await callServerEndpoint( 'update_user_avatar', avatarDBContent, ); return { updates }; }; const resetUserStateActionType = 'RESET_USER_STATE'; const setAccessTokenActionType = 'SET_ACCESS_TOKEN'; export { changeUserPasswordActionTypes, changeUserPassword, claimUsernameActionTypes, useClaimUsername, useDeleteAccount, deleteAccountActionTypes, getSessionPublicKeys, getOlmSessionInitializationDataActionTypes, getOlmSessionInitializationData, mergeUserInfos, logIn as logInRawAction, useLogIn, logInActionTypes, useLogOut, logOutActionTypes, register, registerActionTypes, searchUsers, searchUsersActionTypes, exactSearchUser, exactSearchUserActionTypes, useSetUserSettings, setUserSettingsActionTypes, useUpdateSubscription, updateSubscriptionActionTypes, policyAcknowledgment, policyAcknowledgmentActionTypes, updateUserAvatarActionTypes, updateUserAvatar, resetUserStateActionType, setAccessTokenActionType, }; diff --git a/lib/selectors/account-selectors.js b/lib/selectors/account-selectors.js index 0040963aa..6557ab015 100644 --- a/lib/selectors/account-selectors.js +++ b/lib/selectors/account-selectors.js @@ -1,96 +1,99 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import { cookieSelector, sessionIDSelector, deviceTokensSelector, } from './keyserver-selectors.js'; import { currentCalendarQuery } from './nav-selectors.js'; -import type { LogInExtraInfo } from '../types/account-types.js'; +import type { + LogInExtraInfo, + DeviceTokenUpdateRequest, +} from '../types/account-types.js'; import type { CalendarQuery } from '../types/entry-types.js'; import type { KeyserverInfos } from '../types/keyserver-types.js'; import type { AppState } from '../types/redux-types.js'; import type { PreRequestUserState, PreRequestUserKeyserverSessionInfo, } from '../types/session-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; const logInExtraInfoSelector: ( state: AppState, ) => (calendarActive: boolean) => LogInExtraInfo = createSelector( deviceTokensSelector, currentCalendarQuery, ( deviceTokens: { +[keyserverID: string]: ?string }, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => { - const deviceTokenUpdateRequest = {}; + const deviceTokenUpdateRequest: { [string]: DeviceTokenUpdateRequest } = {}; for (const keyserverID in deviceTokens) { if (deviceTokens[keyserverID]) { deviceTokenUpdateRequest[keyserverID] = { deviceToken: deviceTokens[keyserverID], }; } } // Return a function since we depend on the time of evaluation return (calendarActive: boolean): LogInExtraInfo => ({ calendarQuery: calendarQuery(calendarActive), deviceTokenUpdateRequest, }); }, ); const basePreRequestUserStateForSingleKeyserverSelector: ( keyserverID: string, ) => (state: AppState) => PreRequestUserState = keyserverID => createSelector( (state: AppState) => state.currentUserInfo, cookieSelector(keyserverID), sessionIDSelector(keyserverID), ( currentUserInfo: ?CurrentUserInfo, cookie: ?string, sessionID: ?string, ) => ({ currentUserInfo, cookiesAndSessions: { [keyserverID]: { cookie, sessionID } }, }), ); const preRequestUserStateForSingleKeyserverSelector: ( keyserverID: string, ) => (state: AppState) => PreRequestUserState = _memoize( basePreRequestUserStateForSingleKeyserverSelector, ); const preRequestUserStateSelector: (state: AppState) => PreRequestUserState = createSelector( (state: AppState) => state.currentUserInfo, (state: AppState) => state.keyserverStore.keyserverInfos, (currentUserInfo: ?CurrentUserInfo, keyserverInfos: KeyserverInfos) => { const cookiesAndSessions: { [string]: PreRequestUserKeyserverSessionInfo, } = {}; for (const keyserverID in keyserverInfos) { cookiesAndSessions[keyserverID] = { cookie: keyserverInfos[keyserverID].cookie, sessionID: keyserverInfos[keyserverID].sessionID, }; } return { currentUserInfo, cookiesAndSessions, }; }, ); export { logInExtraInfoSelector, preRequestUserStateForSingleKeyserverSelector, preRequestUserStateSelector, }; diff --git a/lib/selectors/keyserver-selectors.js b/lib/selectors/keyserver-selectors.js index d193e666d..374feda02 100644 --- a/lib/selectors/keyserver-selectors.js +++ b/lib/selectors/keyserver-selectors.js @@ -1,156 +1,156 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import type { PlatformDetails } from '../types/device-types'; import type { KeyserverInfo, KeyserverInfos, SelectedKeyserverInfo, } from '../types/keyserver-types'; import type { AppState } from '../types/redux-types.js'; import type { ConnectionInfo } from '../types/socket-types.js'; import type { UserInfos } from '../types/user-types.js'; const baseCookieSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.cookie; const cookieSelector: (keyserverID: string) => (state: AppState) => ?string = _memoize(baseCookieSelector); const cookiesSelector: (state: AppState) => { +[keyserverID: string]: ?string, } = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (infos: { +[key: string]: KeyserverInfo }) => { const cookies: { [string]: ?string } = {}; for (const keyserverID in infos) { cookies[keyserverID] = infos[keyserverID].cookie; } return cookies; }, ); const baseSessionIDSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.sessionID; const sessionIDSelector: (keyserverID: string) => (state: AppState) => ?string = _memoize(baseSessionIDSelector); const baseUpdatesCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.updatesCurrentAsOf ?? 0; const updatesCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = _memoize(baseUpdatesCurrentAsOfSelector); const baseCurrentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = keyserverID => (state: AppState) => state.messageStore.currentAsOf[keyserverID] ?? 0; const currentAsOfSelector: ( keyserverID: string, ) => (state: AppState) => number = _memoize(baseCurrentAsOfSelector); const baseUrlPrefixSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.urlPrefix; const urlPrefixSelector: (keyserverID: string) => (state: AppState) => ?string = _memoize(baseUrlPrefixSelector); const baseConnectionSelector: ( keyserverID: string, ) => (state: AppState) => ?ConnectionInfo = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.connection; const connectionSelector: ( keyserverID: string, ) => (state: AppState) => ?ConnectionInfo = _memoize(baseConnectionSelector); const baseLastCommunicatedPlatformDetailsSelector: ( keyserverID: string, ) => (state: AppState) => ?PlatformDetails = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID] ?.lastCommunicatedPlatformDetails; const lastCommunicatedPlatformDetailsSelector: ( keyserverID: string, ) => (state: AppState) => ?PlatformDetails = _memoize( baseLastCommunicatedPlatformDetailsSelector, ); const selectedKeyserversSelector: ( state: AppState, ) => $ReadOnlyArray = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (state: AppState) => state.userStore.userInfos, (keyserverInfos: KeyserverInfos, userInfos: UserInfos) => { const result = []; for (const key in keyserverInfos) { const keyserverInfo = keyserverInfos[key]; const keyserverAdminUsername = userInfos[key]?.username; if (!keyserverAdminUsername) { continue; } const keyserverAdminUserInfo = { id: userInfos[key].id, username: keyserverAdminUsername, }; result.push({ keyserverAdminUserInfo, keyserverInfo, }); } return result; }, ); const deviceTokensSelector: (state: AppState) => { +[keyserverID: string]: ?string, } = createSelector( (state: AppState) => state.keyserverStore.keyserverInfos, (infos: { +[key: string]: KeyserverInfo }) => { - const deviceTokens = {}; + const deviceTokens: { [string]: ?string } = {}; for (const keyserverID in infos) { deviceTokens[keyserverID] = infos[keyserverID].deviceToken; } return deviceTokens; }, ); const baseDeviceTokenSelector: ( keyserverID: string, ) => (state: AppState) => ?string = keyserverID => (state: AppState) => state.keyserverStore.keyserverInfos[keyserverID]?.deviceToken; const deviceTokenSelector: ( keyserverID: string, ) => (state: AppState) => ?string = _memoize(baseDeviceTokenSelector); export { cookieSelector, cookiesSelector, sessionIDSelector, updatesCurrentAsOfSelector, currentAsOfSelector, urlPrefixSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, deviceTokensSelector, deviceTokenSelector, selectedKeyserversSelector, }; diff --git a/lib/types/account-types.js b/lib/types/account-types.js index ca69406b2..7d45f7eef 100644 --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -1,210 +1,210 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import type { PlatformDetails } from './device-types.js'; import type { CalendarQuery, CalendarResult, RawEntryInfo, } from './entry-types.js'; import { type RawMessageInfo, type MessageTruncationStatuses, type GenericMessagesResult, } from './message-types.js'; import type { PreRequestUserState } from './session-types.js'; import { type RawThreadInfos } from './thread-types.js'; import { type UserInfo, type LoggedOutUserInfo, type LoggedInUserInfo, } from './user-types.js'; import type { PolicyType } from '../facts/policies.js'; import { values } from '../utils/objects.js'; import { tShape } from '../utils/validation-utils.js'; export type ResetPasswordRequest = { +usernameOrEmail: string, }; export type LogOutResult = { +currentUserInfo: ?LoggedOutUserInfo, +preRequestUserState: PreRequestUserState, }; export type LogOutResponse = { +currentUserInfo: LoggedOutUserInfo, }; export type RegisterInfo = { ...LogInExtraInfo, +username: string, +password: string, }; -type DeviceTokenUpdateRequest = { +export type DeviceTokenUpdateRequest = { +deviceToken: string, }; type DeviceTokenUpdateInput = { +[keyserverID: string]: DeviceTokenUpdateRequest, }; export type RegisterRequest = { +username: string, +email?: empty, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +primaryIdentityPublicKey?: empty, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; export type RegisterResponse = { id: string, rawMessageInfos: $ReadOnlyArray, currentUserInfo: LoggedInUserInfo, cookieChange: { threadInfos: RawThreadInfos, userInfos: $ReadOnlyArray, }, }; export type RegisterResult = { +currentUserInfo: LoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +threadInfos: RawThreadInfos, +userInfos: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export const logInActionSources = Object.freeze({ cookieInvalidationResolutionAttempt: 'COOKIE_INVALIDATION_RESOLUTION_ATTEMPT', appStartCookieLoggedInButInvalidRedux: 'APP_START_COOKIE_LOGGED_IN_BUT_INVALID_REDUX', appStartReduxLoggedInButInvalidCookie: 'APP_START_REDUX_LOGGED_IN_BUT_INVALID_COOKIE', socketAuthErrorResolutionAttempt: 'SOCKET_AUTH_ERROR_RESOLUTION_ATTEMPT', sqliteOpFailure: 'SQLITE_OP_FAILURE', sqliteLoadFailure: 'SQLITE_LOAD_FAILURE', logInFromWebForm: 'LOG_IN_FROM_WEB_FORM', logInFromNativeForm: 'LOG_IN_FROM_NATIVE_FORM', logInFromNativeSIWE: 'LOG_IN_FROM_NATIVE_SIWE', corruptedDatabaseDeletion: 'CORRUPTED_DATABASE_DELETION', refetchUserDataAfterAcknowledgment: 'REFETCH_USER_DATA_AFTER_ACKNOWLEDGMENT', }); export type LogInActionSource = $Values; export type LogInStartingPayload = { +calendarQuery: CalendarQuery, +logInActionSource?: LogInActionSource, }; export type LogInExtraInfo = { +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest: DeviceTokenUpdateInput, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; export type LogInInfo = { ...LogInExtraInfo, +username: string, +password: string, +logInActionSource: LogInActionSource, +keyserverIDs?: $ReadOnlyArray, }; export type LogInRequest = { +usernameOrEmail?: ?string, +username?: ?string, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +watchedIDs: $ReadOnlyArray, +source?: LogInActionSource, +primaryIdentityPublicKey?: empty, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; export type LogInResponse = { +currentUserInfo: LoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: $ReadOnlyArray, +rawEntryInfos?: ?$ReadOnlyArray, +serverTime: number, +cookieChange: { +threadInfos: RawThreadInfos, +userInfos: $ReadOnlyArray, }, +notAcknowledgedPolicies?: $ReadOnlyArray, }; export type LogInResult = { +threadInfos: RawThreadInfos, +currentUserInfo: LoggedInUserInfo, +messagesResult: GenericMessagesResult, +userInfos: $ReadOnlyArray, +calendarResult: CalendarResult, +updatesCurrentAsOf: { +[keyserverID: string]: number }, +logInActionSource: LogInActionSource, +notAcknowledgedPolicies?: $ReadOnlyArray, }; export type UpdatePasswordRequest = { code: string, password: string, calendarQuery?: ?CalendarQuery, deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, platformDetails: PlatformDetails, watchedIDs: $ReadOnlyArray, }; export type PolicyAcknowledgmentRequest = { +policy: PolicyType, }; export type EmailSubscriptionRequest = { +email: string, }; export type UpdateUserSettingsRequest = { +name: 'default_user_notifications', +data: NotificationTypes, }; export const userSettingsTypes = Object.freeze({ DEFAULT_NOTIFICATIONS: 'default_user_notifications', }); export const notificationTypes = Object.freeze({ FOCUSED: 'focused', BADGE_ONLY: 'badge_only', BACKGROUND: 'background', }); export type NotificationTypes = $Values; export const notificationTypeValues: $ReadOnlyArray = values(notificationTypes); export type DefaultNotificationPayload = { +default_user_notifications: ?NotificationTypes, }; export const defaultNotificationPayloadValidator: TInterface = tShape({ default_user_notifications: t.maybe(t.enums.of(notificationTypeValues)), }); export type ClaimUsernameResponse = { +message: string, +signature: string, }; diff --git a/lib/types/filter-types.js b/lib/types/filter-types.js index a56f8a608..143412d78 100644 --- a/lib/types/filter-types.js +++ b/lib/types/filter-types.js @@ -1,47 +1,47 @@ // @flow import t, { type TUnion } from 'tcomb'; import type { MinimallyEncodedResolvedThreadInfo } from './minimally-encoded-thread-permissions-types.js'; import type { ResolvedThreadInfo } from './thread-types.js'; import { tID, tShape, tString } from '../utils/validation-utils.js'; export const calendarThreadFilterTypes = Object.freeze({ THREAD_LIST: 'threads', NOT_DELETED: 'not_deleted', }); export type CalendarThreadFilterType = $Values< typeof calendarThreadFilterTypes, >; export type CalendarThreadFilter = { +type: 'threads', +threadIDs: $ReadOnlyArray, }; -type NotDeletedFilter = { +type: 'not_deleted' }; +export type NotDeletedFilter = { +type: 'not_deleted' }; export type CalendarFilter = NotDeletedFilter | CalendarThreadFilter; export const calendarFilterValidator: TUnion = t.union([ tShape({ type: tString('threads'), threadIDs: t.list(tID), }), tShape({ type: tString('not_deleted') }), ]); export const defaultCalendarFilters: $ReadOnlyArray = [ { type: calendarThreadFilterTypes.NOT_DELETED }, ]; export const updateCalendarThreadFilter = 'UPDATE_CALENDAR_THREAD_FILTER'; export const clearCalendarThreadFilter = 'CLEAR_CALENDAR_THREAD_FILTER'; export const setCalendarDeletedFilter = 'SET_CALENDAR_DELETED_FILTER'; export type SetCalendarDeletedFilterPayload = { +includeDeleted: boolean, }; export type FilterThreadInfo = { +threadInfo: ResolvedThreadInfo | MinimallyEncodedResolvedThreadInfo, +numVisibleEntries: number, }; diff --git a/lib/utils/action-utils.js b/lib/utils/action-utils.js index 5b7e161c6..882e5e076 100644 --- a/lib/utils/action-utils.js +++ b/lib/utils/action-utils.js @@ -1,591 +1,608 @@ // @flow import invariant from 'invariant'; import _memoize from 'lodash/memoize.js'; import * as React from 'react'; import { createSelector } from 'reselect'; import callServerEndpoint from './call-server-endpoint.js'; import type { CallServerEndpoint, CallServerEndpointOptions, } from './call-server-endpoint.js'; import { getConfig } from './config.js'; import { promiseAll } from './promises.js'; import { useSelector, useDispatch } from './redux-utils.js'; import { ashoatKeyserverID } from './validation-utils.js'; import { serverCallStateSelector } from '../selectors/server-calls.js'; import { logInActionSources, type LogInActionSource, type LogInStartingPayload, type LogInResult, } from '../types/account-types.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { Endpoint, SocketAPIHandler } from '../types/endpoints.js'; import type { CalendarQuery } from '../types/entry-types.js'; +import type { NotDeletedFilter } from '../types/filter-types.js'; import type { LoadingOptions, LoadingInfo } from '../types/loading-types.js'; import type { ActionPayload, Dispatch, PromisedAction, BaseAction, } from '../types/redux-types.js'; import type { ClientSessionChange, PreRequestUserState, } from '../types/session-types.js'; import type { ConnectionStatus } from '../types/socket-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; function extractKeyserverIDFromID(id: string): string { return id.split('|')[0]; } let nextPromiseIndex = 0; export type ActionTypes< STARTED_ACTION_TYPE: string, SUCCESS_ACTION_TYPE: string, FAILED_ACTION_TYPE: string, > = { started: STARTED_ACTION_TYPE, success: SUCCESS_ACTION_TYPE, failed: FAILED_ACTION_TYPE, }; function wrapActionPromise< STARTED_ACTION_TYPE: string, STARTED_PAYLOAD: ActionPayload, SUCCESS_ACTION_TYPE: string, SUCCESS_PAYLOAD: ActionPayload, FAILED_ACTION_TYPE: string, >( actionTypes: ActionTypes< STARTED_ACTION_TYPE, SUCCESS_ACTION_TYPE, FAILED_ACTION_TYPE, >, promise: Promise, loadingOptions: ?LoadingOptions, startingPayload: ?STARTED_PAYLOAD, ): PromisedAction { const loadingInfo: LoadingInfo = { fetchIndex: nextPromiseIndex++, trackMultipleRequests: !!loadingOptions?.trackMultipleRequests, customKeyName: loadingOptions?.customKeyName || null, }; return async (dispatch: Dispatch): Promise => { const startAction = startingPayload ? { type: (actionTypes.started: STARTED_ACTION_TYPE), loadingInfo, payload: (startingPayload: STARTED_PAYLOAD), } : { type: (actionTypes.started: STARTED_ACTION_TYPE), loadingInfo, }; dispatch(startAction); try { const result = await promise; dispatch({ type: (actionTypes.success: SUCCESS_ACTION_TYPE), payload: (result: SUCCESS_PAYLOAD), loadingInfo, }); } catch (e) { console.log(e); dispatch({ type: (actionTypes.failed: FAILED_ACTION_TYPE), error: true, payload: (e: Error), loadingInfo, }); } }; } export type DispatchActionPromise = < STARTED: BaseAction, SUCCESS: BaseAction, FAILED: BaseAction, >( actionTypes: ActionTypes< $PropertyType, $PropertyType, $PropertyType, >, promise: Promise<$PropertyType>, loadingOptions?: LoadingOptions, startingPayload?: $PropertyType, ) => Promise; function useDispatchActionPromise(): DispatchActionPromise { const dispatch = useDispatch(); return React.useMemo(() => createDispatchActionPromise(dispatch), [dispatch]); } function createDispatchActionPromise(dispatch: Dispatch) { const dispatchActionPromise = function < STARTED: BaseAction, SUCCESS: BaseAction, FAILED: BaseAction, >( actionTypes: ActionTypes< $PropertyType, $PropertyType, $PropertyType, >, promise: Promise<$PropertyType>, loadingOptions?: LoadingOptions, startingPayload?: $PropertyType, ): Promise { return dispatch( wrapActionPromise(actionTypes, promise, loadingOptions, startingPayload), ); }; return dispatchActionPromise; } export type DispatchFunctions = { +dispatch: Dispatch, +dispatchActionPromise: DispatchActionPromise, }; let currentlyWaitingForNewCookie = false; let serverEndpointCallsWaitingForNewCookie: (( callServerEndpoint: ?CallServerEndpoint, ) => void)[] = []; export type DispatchRecoveryAttempt = ( actionTypes: ActionTypes<'LOG_IN_STARTED', 'LOG_IN_SUCCESS', 'LOG_IN_FAILED'>, promise: Promise, startingPayload: LogInStartingPayload, ) => Promise; const setNewSessionActionType = 'SET_NEW_SESSION'; function setNewSession( dispatch: Dispatch, sessionChange: ClientSessionChange, preRequestUserState: ?PreRequestUserState, error: ?string, logInActionSource: ?LogInActionSource, keyserverID: string, ) { dispatch({ type: setNewSessionActionType, payload: { sessionChange, preRequestUserState, error, logInActionSource, keyserverID, }, }); } // This function calls resolveInvalidatedCookie, which dispatchs a log in action // using the native credentials. Note that we never actually specify a sessionID // here, on the assumption that only native clients will call this. (Native // clients don't specify a sessionID, indicating to the server that it should // use the cookieID as the sessionID.) async function fetchNewCookieFromNativeCredentials( dispatch: Dispatch, cookie: ?string, urlPrefix: string, logInActionSource: LogInActionSource, keyserverID: string, getInitialNotificationsEncryptedMessage?: () => Promise, ): Promise { const resolveInvalidatedCookie = getConfig().resolveInvalidatedCookie; if (!resolveInvalidatedCookie) { return null; } let newSessionChange = null; let callServerEndpointCallback = null; const boundCallServerEndpoint = async ( endpoint: Endpoint, data: { +[key: string]: mixed }, options?: ?CallServerEndpointOptions, ) => { const innerBoundSetNewSession = ( sessionChange: ClientSessionChange, error: ?string, ) => { newSessionChange = sessionChange; setNewSession( dispatch, sessionChange, null, error, logInActionSource, keyserverID, ); }; try { const result = await callServerEndpoint( cookie, innerBoundSetNewSession, () => new Promise(r => r(null)), () => new Promise(r => r(null)), urlPrefix, null, 'disconnected', null, null, endpoint, data, dispatch, options, false, keyserverID, ); if (callServerEndpointCallback) { callServerEndpointCallback(!!newSessionChange); } return result; } catch (e) { if (callServerEndpointCallback) { callServerEndpointCallback(!!newSessionChange); } throw e; } }; const boundCallKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallServerEndpointOptions, ) => { if (requests[keyserverID]) { const promises = { [keyserverID]: boundCallServerEndpoint( endpoint, requests[keyserverID], options, ), }; return promiseAll(promises); } return Promise.resolve({}); }; const dispatchRecoveryAttempt = ( actionTypes: ActionTypes< 'LOG_IN_STARTED', 'LOG_IN_SUCCESS', 'LOG_IN_FAILED', >, promise: Promise, inputStartingPayload: LogInStartingPayload, ) => { const startingPayload = { ...inputStartingPayload, logInActionSource }; dispatch(wrapActionPromise(actionTypes, promise, null, startingPayload)); return new Promise(r => (callServerEndpointCallback = r)); }; await resolveInvalidatedCookie( boundCallServerEndpoint, boundCallKeyserverEndpoint, dispatchRecoveryAttempt, logInActionSource, keyserverID, getInitialNotificationsEncryptedMessage, ); return newSessionChange; } // Third param is optional and gets called with newCookie if we get a new cookie // Necessary to propagate cookie in cookieInvalidationRecovery below function bindCookieAndUtilsIntoCallServerEndpoint( params: BindServerCallsParams, ): CallServerEndpoint { const { dispatch, cookie, urlPrefix, sessionID, currentUserInfo, connectionStatus, lastCommunicatedPlatformDetails, keyserverID, } = params; const loggedIn = !!(currentUserInfo && !currentUserInfo.anonymous && true); const boundSetNewSession = ( sessionChange: ClientSessionChange, error: ?string, ) => setNewSession( dispatch, sessionChange, { currentUserInfo, cookiesAndSessions: { [keyserverID]: { cookie, sessionID } }, }, error, undefined, keyserverID, ); // This function gets called before callServerEndpoint sends a request, // to make sure that we're not in the middle of trying to recover // an invalidated cookie const waitIfCookieInvalidated = () => { if (!getConfig().resolveInvalidatedCookie) { // If there is no resolveInvalidatedCookie function, just let the caller // callServerEndpoint instance continue return Promise.resolve(null); } if (!currentlyWaitingForNewCookie) { // Our cookie seems to be valid return Promise.resolve(null); } // Wait to run until we get our new cookie return new Promise(r => serverEndpointCallsWaitingForNewCookie.push(r), ); }; // This function is a helper for the next function defined below const attemptToResolveInvalidation = async ( sessionChange: ClientSessionChange, ) => { const newAnonymousCookie = sessionChange.cookie; const newSessionChange = await fetchNewCookieFromNativeCredentials( dispatch, newAnonymousCookie, urlPrefix, logInActionSources.cookieInvalidationResolutionAttempt, keyserverID, ); currentlyWaitingForNewCookie = false; const currentWaitingCalls = serverEndpointCallsWaitingForNewCookie; serverEndpointCallsWaitingForNewCookie = []; const newCallServerEndpoint = newSessionChange ? bindCookieAndUtilsIntoCallServerEndpoint({ ...params, cookie: newSessionChange.cookie, sessionID: newSessionChange.sessionID, currentUserInfo: newSessionChange.currentUserInfo, }) : null; for (const func of currentWaitingCalls) { func(newCallServerEndpoint); } return newCallServerEndpoint; }; // If this function is called, callServerEndpoint got a response invalidating // its cookie, and is wondering if it should just like... give up? // Or if there's a chance at redemption const cookieInvalidationRecovery = (sessionChange: ClientSessionChange) => { if (!getConfig().resolveInvalidatedCookie) { // If there is no resolveInvalidatedCookie function, just let the caller // callServerEndpoint instance continue return Promise.resolve(null); } if (!loggedIn) { // We don't want to attempt any use native credentials of a logged out // user to log-in after a cookieInvalidation while logged out return Promise.resolve(null); } if (currentlyWaitingForNewCookie) { return new Promise(r => serverEndpointCallsWaitingForNewCookie.push(r), ); } currentlyWaitingForNewCookie = true; return attemptToResolveInvalidation(sessionChange); }; return ( endpoint: Endpoint, data: Object, options?: ?CallServerEndpointOptions, ) => callServerEndpoint( cookie, boundSetNewSession, waitIfCookieInvalidated, cookieInvalidationRecovery, urlPrefix, sessionID, connectionStatus, lastCommunicatedPlatformDetails, socketAPIHandler, endpoint, data, dispatch, options, loggedIn, keyserverID, ); } export type ActionFunc = (callServerEndpoint: CallServerEndpoint) => F; export type BindServerCall = (serverCall: ActionFunc) => F; export type BindServerCallsParams = { +dispatch: Dispatch, +cookie: ?string, +urlPrefix: string, +sessionID: ?string, +currentUserInfo: ?CurrentUserInfo, +connectionStatus: ConnectionStatus, +lastCommunicatedPlatformDetails: ?PlatformDetails, +keyserverID: string, }; // All server calls needs to include some information from the Redux state // (namely, the cookie). This information is used deep in the server call, // at the point where callServerEndpoint is called. We don't want to bother // propagating the cookie (and any future config info that callServerEndpoint // needs) through to the server calls so they can pass it to callServerEndpoint. // Instead, we "curry" the cookie onto callServerEndpoint within react-redux's // connect's mapStateToProps function, and then pass that "bound" // callServerEndpoint that no longer needs the cookie as a parameter on to // the server call. const baseCreateBoundServerCallsSelector = ( actionFunc: ActionFunc, ): (BindServerCallsParams => F) => createSelector( (state: BindServerCallsParams) => state.dispatch, (state: BindServerCallsParams) => state.cookie, (state: BindServerCallsParams) => state.urlPrefix, (state: BindServerCallsParams) => state.sessionID, (state: BindServerCallsParams) => state.currentUserInfo, (state: BindServerCallsParams) => state.connectionStatus, (state: BindServerCallsParams) => state.lastCommunicatedPlatformDetails, (state: BindServerCallsParams) => state.keyserverID, ( dispatch: Dispatch, cookie: ?string, urlPrefix: string, sessionID: ?string, currentUserInfo: ?CurrentUserInfo, connectionStatus: ConnectionStatus, lastCommunicatedPlatformDetails: ?PlatformDetails, keyserverID: string, ) => { const boundCallServerEndpoint = bindCookieAndUtilsIntoCallServerEndpoint({ dispatch, cookie, urlPrefix, sessionID, currentUserInfo, connectionStatus, lastCommunicatedPlatformDetails, keyserverID, }); return actionFunc(boundCallServerEndpoint); }, ); type CreateBoundServerCallsSelectorType = ( ActionFunc, ) => BindServerCallsParams => F; const createBoundServerCallsSelector: CreateBoundServerCallsSelectorType = (_memoize(baseCreateBoundServerCallsSelector): any); function useServerCall( serverCall: ActionFunc, paramOverride?: ?$Shape, ): F { const dispatch = useDispatch(); const serverCallState = useSelector( serverCallStateSelector(ashoatKeyserverID), ); return React.useMemo(() => { const { urlPrefix, connectionStatus } = serverCallState; invariant( !!urlPrefix && !!connectionStatus, 'keyserver missing from keyserverStore', ); return createBoundServerCallsSelector(serverCall)({ ...serverCallState, urlPrefix, connectionStatus, dispatch, ...paramOverride, keyserverID: ashoatKeyserverID, }); }, [serverCall, serverCallState, dispatch, paramOverride]); } let socketAPIHandler: ?SocketAPIHandler = null; function registerActiveSocket(passedSocketAPIHandler: ?SocketAPIHandler) { socketAPIHandler = passedSocketAPIHandler; } function sortThreadIDsPerKeyserver(threadIDs: $ReadOnlyArray): { +[keyserverID: string]: $ReadOnlyArray, } { - const results = {}; + const results: { [string]: string[] } = {}; for (const threadID of threadIDs) { const keyserverID = extractKeyserverIDFromID(threadID); invariant(keyserverID, 'keyserver data missing from thread id'); if (results[keyserverID] === undefined) { results[keyserverID] = []; } results[keyserverID].push(threadID); } return results; } +type CalendarThreadFilterWithWritableThreadIDs = { + +type: 'threads', + +threadIDs: string[], +}; +type CalendarFilterWithWritableThreadIDs = + | NotDeletedFilter + | CalendarThreadFilterWithWritableThreadIDs; +type CalendarQueryWithWritableFilters = { + +startDate: string, + +endDate: string, + +filters: CalendarFilterWithWritableThreadIDs[], +}; function sortCalendarQueryPerKeyserver( calendarQuery: CalendarQuery, keyserverIDs: $ReadOnlyArray, ): { +[keyserverID: string]: CalendarQuery, } { const { startDate, endDate, filters } = calendarQuery; - const results = {}; + const results: { [string]: CalendarQueryWithWritableFilters } = {}; for (const keyserverID of keyserverIDs) { results[keyserverID] = { startDate, endDate, filters: [], }; } for (const filter of filters) { if (filter.type === 'not_deleted') { for (const keyserverID in results) { results[keyserverID].filters.push({ type: 'not_deleted' }); } } else if (filter.type === 'threads') { for (const threadID of filter.threadIDs) { const keyserverID = extractKeyserverIDFromID(threadID); if (results[keyserverID] === undefined) { continue; } let threadFilter = results[keyserverID].filters.find( flt => flt.type === 'threads', ); + invariant( + !threadFilter || threadFilter.type === 'threads', + 'should only match CalendarThreadFilter', + ); if (!threadFilter) { threadFilter = { type: 'threads', threadIDs: [] }; results[keyserverID].filters.push(threadFilter); } threadFilter.threadIDs.push(threadID); } } else { console.warn('unhandled filter in sortCalendarQueryPerKeyserver'); } } return results; } export { useDispatchActionPromise, setNewSessionActionType, fetchNewCookieFromNativeCredentials, createBoundServerCallsSelector, registerActiveSocket, useServerCall, bindCookieAndUtilsIntoCallServerEndpoint, extractKeyserverIDFromID, sortThreadIDsPerKeyserver, sortCalendarQueryPerKeyserver, }; diff --git a/lib/utils/keyserver-call.js b/lib/utils/keyserver-call.js index 7de3d27a2..ea63d29b0 100644 --- a/lib/utils/keyserver-call.js +++ b/lib/utils/keyserver-call.js @@ -1,169 +1,173 @@ // @flow import _memoize from 'lodash/memoize.js'; import * as React from 'react'; -import { useDispatch } from 'react-redux'; import { createSelector } from 'reselect'; import { bindCookieAndUtilsIntoCallServerEndpoint } from './action-utils.js'; import type { BindServerCallsParams } from './action-utils.js'; import type { CallServerEndpoint, CallServerEndpointOptions, } from './call-server-endpoint.js'; import { promiseAll } from './promises.js'; -import { useSelector } from './redux-utils.js'; +import { useSelector, useDispatch } from './redux-utils.js'; import type { PlatformDetails } from '../types/device-types.js'; import type { Endpoint } from '../types/endpoints.js'; import type { KeyserverInfo } from '../types/keyserver-types.js'; import type { Dispatch } from '../types/redux-types.js'; import type { ConnectionStatus } from '../types/socket-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; export type CallKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallServerEndpointOptions, ) => Promise<{ +[keyserverID: string]: any }>; export type ActionFunc = ( callServerEndpoint: CallKeyserverEndpoint, // The second argument is only used in actions that call all keyservers, // and the request to all keyservers are exactly the same. // An example of such action is fetchEntries. allKeyserverIDs: $ReadOnlyArray, ) => Args => Promise; // _memoize memoizes the function by caching the result. // The first argument of the memoized function is used as the map cache key. const baseCreateBoundServerCallsSelector = ( // eslint-disable-next-line no-unused-vars keyserverID: string, ): (BindServerCallsParams => CallServerEndpoint) => createSelector( (state: BindServerCallsParams) => state.dispatch, (state: BindServerCallsParams) => state.cookie, (state: BindServerCallsParams) => state.urlPrefix, (state: BindServerCallsParams) => state.sessionID, (state: BindServerCallsParams) => state.currentUserInfo, (state: BindServerCallsParams) => state.connectionStatus, (state: BindServerCallsParams) => state.lastCommunicatedPlatformDetails, ( dispatch: Dispatch, cookie: ?string, urlPrefix: string, sessionID: ?string, currentUserInfo: ?CurrentUserInfo, connectionStatus: ConnectionStatus, lastCommunicatedPlatformDetails: ?PlatformDetails, ) => bindCookieAndUtilsIntoCallServerEndpoint({ dispatch, cookie, urlPrefix, sessionID, currentUserInfo, connectionStatus, lastCommunicatedPlatformDetails, keyserverID, }), ); type CreateBoundServerCallsSelectorType = ( keyserverID: string, ) => BindServerCallsParams => CallServerEndpoint; const createBoundServerCallsSelector: CreateBoundServerCallsSelectorType = (_memoize(baseCreateBoundServerCallsSelector): any); export type KeyserverInfoPartial = $Shape; export type BindKeyserverCallParams = { +dispatch: Dispatch, +currentUserInfo: ?CurrentUserInfo, +keyserverInfos: { +[keyserverID: string]: KeyserverInfoPartial }, }; -const bindCallKeyserverEndpointSelector = createSelector( +const bindCallKeyserverEndpointSelector: BindKeyserverCallParams => < + Args: mixed, + Return, +>( + keyserverCall: ActionFunc, +) => Args => Promise = createSelector( (state: BindKeyserverCallParams) => state.dispatch, (state: BindKeyserverCallParams) => state.currentUserInfo, (state: BindKeyserverCallParams) => state.keyserverInfos, ( dispatch: Dispatch, currentUserInfo: ?CurrentUserInfo, keyserverInfos: { +[keyserverID: string]: KeyserverInfo }, ) => { return _memoize( ( keyserverCall: ActionFunc, ): (Args => Promise) => { const callKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallServerEndpointOptions, ) => { const bindCallKeyserverEndpoint = (keyserverID: string) => { const { cookie, urlPrefix, sessionID, connection, lastCommunicatedPlatformDetails, } = keyserverInfos[keyserverID]; const boundCallServerEndpoint = createBoundServerCallsSelector( keyserverID, )({ dispatch, currentUserInfo, cookie, urlPrefix, sessionID, connectionStatus: connection?.status, lastCommunicatedPlatformDetails, keyserverID, }); return boundCallServerEndpoint( endpoint, requests[keyserverID], options, ); }; - const promises = {}; + const promises: { [string]: Promise } = {}; for (const keyserverID in requests) { promises[keyserverID] = bindCallKeyserverEndpoint(keyserverID); } return promiseAll(promises); }; const keyserverIDs = Object.keys(keyserverInfos); return keyserverCall(callKeyserverEndpoint, keyserverIDs); }, ); }, ); export type KeyserverCallParamOverride = $Shape; function useKeyserverCall( keyserverCall: ActionFunc, paramOverride?: ?KeyserverCallParamOverride, ): Args => Promise { const dispatch = useDispatch(); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); const currentUserInfo = useSelector(state => state.currentUserInfo); const bindCallKeyserverEndpointToAction = bindCallKeyserverEndpointSelector({ dispatch, keyserverInfos, currentUserInfo, ...paramOverride, }); return React.useMemo( () => bindCallKeyserverEndpointToAction(keyserverCall), [bindCallKeyserverEndpointToAction, keyserverCall], ); } export { useKeyserverCall };