diff --git a/lib/reducers/calendar-filters-reducer.js b/lib/reducers/calendar-filters-reducer.js index d9fcafac8..aec312e1d 100644 --- a/lib/reducers/calendar-filters-reducer.js +++ b/lib/reducers/calendar-filters-reducer.js @@ -1,221 +1,215 @@ // @flow import { updateCalendarCommunityFilter, clearCalendarCommunityFilter, } from '../actions/community-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { newThreadActionTypes, joinThreadActionTypes, leaveThreadActionTypes, deleteThreadActionTypes, } from '../actions/thread-actions.js'; import { keyserverAuthActionTypes, - logOutActionTypes, deleteKeyserverAccountActionTypes, logInActionTypes, - tempIdentityLoginActionTypes, keyserverRegisterActionTypes, - deleteAccountActionTypes, } from '../actions/user-actions.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import { filteredThreadIDs, nonThreadCalendarFilters, nonExcludeDeletedCalendarFilters, } from '../selectors/calendar-filter-selectors.js'; import { threadInFilterList } from '../shared/thread-utils.js'; import { updateSpecs } from '../shared/updates/update-specs.js'; import { type CalendarFilter, defaultCalendarFilters, updateCalendarThreadFilter, clearCalendarThreadFilter, setCalendarDeletedFilter, calendarThreadFilterTypes, } from '../types/filter-types.js'; import type { BaseAction } from '../types/redux-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; import type { RawThreadInfos, ThreadStore } from '../types/thread-types.js'; import { type ClientUpdateInfo, processUpdatesActionType, } from '../types/update-types.js'; import { filterThreadIDsBelongingToCommunity } from '../utils/drawer-utils.react.js'; export default function reduceCalendarFilters( state: $ReadOnlyArray, action: BaseAction, threadStore: ThreadStore, ): $ReadOnlyArray { if ( - action.type === tempIdentityLoginActionTypes.success || - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success || action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || action.type === keyserverRegisterActionTypes.success ) { return defaultCalendarFilters; } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated ) { return removeKeyserverThreadIDsFromFilterList(state, [ action.payload.keyserverID, ]); } else if (action.type === deleteKeyserverAccountActionTypes.success) { return removeKeyserverThreadIDsFromFilterList( state, action.payload.keyserverIDs, ); } else if (action.type === keyserverAuthActionTypes.success) { const keyserverIDs = Object.keys(action.payload.updatesCurrentAsOf); return removeKeyserverThreadIDsFromFilterList(state, keyserverIDs); } else if (action.type === updateCalendarThreadFilter) { const nonThreadFilters = nonThreadCalendarFilters(state); return [ ...nonThreadFilters, { type: calendarThreadFilterTypes.THREAD_LIST, threadIDs: action.payload.threadIDs, }, ]; } else if (action.type === clearCalendarThreadFilter) { return nonThreadCalendarFilters(state); } else if (action.type === setCalendarDeletedFilter) { const otherFilters = nonExcludeDeletedCalendarFilters(state); if (action.payload.includeDeleted && otherFilters.length === state.length) { // Attempting to remove NOT_DELETED filter, but it doesn't exist return state; } else if (action.payload.includeDeleted) { // Removing NOT_DELETED filter return otherFilters; } else if (otherFilters.length < state.length) { // Attempting to add NOT_DELETED filter, but it already exists return state; } else { // Adding NOT_DELETED filter return [...state, { type: calendarThreadFilterTypes.NOT_DELETED }]; } } else if ( action.type === newThreadActionTypes.success || action.type === joinThreadActionTypes.success || action.type === leaveThreadActionTypes.success || action.type === deleteThreadActionTypes.success || action.type === processUpdatesActionType ) { return updateFilterListFromUpdateInfos( state, action.payload.updatesResult.newUpdates, ); } else if (action.type === incrementalStateSyncActionType) { return updateFilterListFromUpdateInfos( state, action.payload.updatesResult.newUpdates, ); } else if (action.type === fullStateSyncActionType) { return removeDeletedThreadIDsFromFilterList( state, action.payload.threadInfos, ); } else if (action.type === updateCalendarCommunityFilter) { const nonThreadFilters = nonThreadCalendarFilters(state); const threadIDs = Array.from( filterThreadIDsBelongingToCommunity( action.payload, threadStore.threadInfos, ), ); return [ ...nonThreadFilters, { type: calendarThreadFilterTypes.THREAD_LIST, threadIDs, }, ]; } else if (action.type === clearCalendarCommunityFilter) { const nonThreadFilters = nonThreadCalendarFilters(state); return nonThreadFilters; } return state; } function updateFilterListFromUpdateInfos( state: $ReadOnlyArray, updateInfos: $ReadOnlyArray, ): $ReadOnlyArray { const currentlyFilteredIDs: ?$ReadOnlySet = filteredThreadIDs(state); if (!currentlyFilteredIDs) { return state; } const newFilteredThreadIDs = updateInfos.reduce( (reducedFilteredThreadIDs, update) => { const { reduceCalendarThreadFilters } = updateSpecs[update.type]; return reduceCalendarThreadFilters ? reduceCalendarThreadFilters(reducedFilteredThreadIDs, update) : reducedFilteredThreadIDs; }, currentlyFilteredIDs, ); if (currentlyFilteredIDs !== newFilteredThreadIDs) { return [ ...nonThreadCalendarFilters(state), { type: 'threads', threadIDs: [...newFilteredThreadIDs] }, ]; } return state; } function filterThreadIDsInFilterList( state: $ReadOnlyArray, filterCondition: (threadID: string) => boolean, ): $ReadOnlyArray { const currentlyFilteredIDs = filteredThreadIDs(state); if (!currentlyFilteredIDs) { return state; } const filtered = [...currentlyFilteredIDs].filter(filterCondition); if (filtered.length < currentlyFilteredIDs.size) { return [ ...nonThreadCalendarFilters(state), { type: 'threads', threadIDs: filtered }, ]; } return state; } function removeDeletedThreadIDsFromFilterList( state: $ReadOnlyArray, threadInfos: RawThreadInfos, ): $ReadOnlyArray { const filterCondition = (threadID: string) => threadInFilterList(threadInfos[threadID]); return filterThreadIDsInFilterList(state, filterCondition); } function removeKeyserverThreadIDsFromFilterList( state: $ReadOnlyArray, keyserverIDs: $ReadOnlyArray, ): $ReadOnlyArray { const keyserverIDsSet = new Set(keyserverIDs); const filterCondition = (threadID: string) => !keyserverIDsSet.has(extractKeyserverIDFromID(threadID)); return filterThreadIDsInFilterList(state, filterCondition); } export { filterThreadIDsInFilterList, removeDeletedThreadIDsFromFilterList, removeKeyserverThreadIDsFromFilterList, }; diff --git a/lib/reducers/data-loaded-reducer.js b/lib/reducers/data-loaded-reducer.js index ee839be44..866c90027 100644 --- a/lib/reducers/data-loaded-reducer.js +++ b/lib/reducers/data-loaded-reducer.js @@ -1,42 +1,37 @@ // @flow import { setDataLoadedActionType } from '../actions/client-db-store-actions.js'; import { keyserverAuthActionTypes, logOutActionTypes, - deleteAccountActionTypes, logInActionTypes, } from '../actions/user-actions.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import type { BaseAction } from '../types/redux-types.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; export default function reduceDataLoaded( state: boolean, action: BaseAction, ): boolean { if (action.type === setDataLoadedActionType) { return action.payload.dataLoaded; } else if (action.type === logInActionTypes.success) { return true; } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && action.payload.sessionChange.currentUserInfo.anonymous && !usingCommServicesAccessToken ) { return false; - } else if ( - action.type === logOutActionTypes.started || - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success - ) { + } else if (action.type === logOutActionTypes.started) { return false; } else if (action.type === keyserverAuthActionTypes.success) { if (ashoatKeyserverID in action.payload.updatesCurrentAsOf) { return true; } } return state; } diff --git a/lib/reducers/draft-reducer.js b/lib/reducers/draft-reducer.js index 67bfc678a..091ea5466 100644 --- a/lib/reducers/draft-reducer.js +++ b/lib/reducers/draft-reducer.js @@ -1,125 +1,113 @@ // @flow import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { moveDraftActionType, updateDraftActionType, } from '../actions/draft-actions.js'; -import { - deleteKeyserverAccountActionTypes, - logOutActionTypes, - deleteAccountActionTypes, -} from '../actions/user-actions.js'; +import { deleteKeyserverAccountActionTypes } from '../actions/user-actions.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import type { DraftStore, DraftStoreOperation } from '../types/draft-types.js'; import type { BaseAction } from '../types/redux-types.js'; type ReduceDraftStoreResult = { +draftStoreOperations: $ReadOnlyArray, +draftStore: DraftStore, }; function removeKeyserversDraftsFromStore( draftStore: DraftStore, keyserverIDs: $ReadOnlyArray, ): ReduceDraftStoreResult { const keyserverIDsSet = new Set(keyserverIDs); const drafts: { [key: string]: string } = {}; const ids: string[] = []; for (const key in draftStore.drafts) { if (keyserverIDsSet.has(extractKeyserverIDFromID(key))) { ids.push(key); } else { drafts[key] = draftStore.drafts[key]; } } return { draftStoreOperations: [{ type: 'remove', payload: { ids } }], draftStore: { ...draftStore, drafts }, }; } function reduceDraftStore( draftStore: DraftStore, action: BaseAction, ): ReduceDraftStoreResult { - if ( - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success - ) { - return { - draftStoreOperations: [{ type: 'remove_all' }], - draftStore: { drafts: {} }, - }; - } else if (action.type === deleteKeyserverAccountActionTypes.success) { + if (action.type === deleteKeyserverAccountActionTypes.success) { return removeKeyserversDraftsFromStore( draftStore, action.payload.keyserverIDs, ); } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated ) { return removeKeyserversDraftsFromStore(draftStore, [ action.payload.keyserverID, ]); } else if (action.type === updateDraftActionType) { const { key, text } = action.payload; const draftStoreOperations = [ { type: 'update', payload: { key, text }, }, ]; return { draftStoreOperations, draftStore: { ...draftStore, drafts: { ...draftStore.drafts, [key]: text, }, }, }; } else if (action.type === moveDraftActionType) { const { oldKey, newKey } = action.payload; const draftStoreOperations = [ { type: 'move', payload: { oldKey, newKey }, }, ]; const { [oldKey]: text, ...draftsWithoutOldKey } = draftStore.drafts; return { draftStoreOperations, draftStore: { ...draftStore, drafts: { ...draftsWithoutOldKey, [newKey]: text, }, }, }; } else if (action.type === setClientDBStoreActionType) { const drafts: { [string]: string } = {}; for (const dbDraftInfo of action.payload.drafts) { drafts[dbDraftInfo.key] = dbDraftInfo.text; } return { draftStoreOperations: [], draftStore: { ...draftStore, drafts: drafts, }, }; } return { draftStore, draftStoreOperations: [] }; } export { reduceDraftStore, removeKeyserversDraftsFromStore }; diff --git a/lib/reducers/enabled-apps-reducer.js b/lib/reducers/enabled-apps-reducer.js index fce875da8..111da7090 100644 --- a/lib/reducers/enabled-apps-reducer.js +++ b/lib/reducers/enabled-apps-reducer.js @@ -1,43 +1,34 @@ // @flow -import { - deleteAccountActionTypes, - logOutActionTypes, -} from '../actions/user-actions.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import type { EnabledApps } from '../types/enabled-apps.js'; import { defaultEnabledApps, defaultWebEnabledApps, } from '../types/enabled-apps.js'; import type { BaseAction } from '../types/redux-types.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; export const enableAppActionType = 'ENABLE_APP'; export const disableAppActionType = 'DISABLE_APP'; export default function reduceEnabledApps( state: EnabledApps, action: BaseAction, ): EnabledApps { if (action.type === enableAppActionType && action.payload === 'calendar') { return { ...state, calendar: true }; } else if ( action.type === disableAppActionType && action.payload === 'calendar' ) { return { ...state, calendar: false }; - } else if ( - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success - ) { - return process.env.BROWSER ? defaultWebEnabledApps : defaultEnabledApps; } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated && !usingCommServicesAccessToken ) { return process.env.BROWSER ? defaultWebEnabledApps : defaultEnabledApps; } return state; } diff --git a/lib/reducers/entry-reducer.js b/lib/reducers/entry-reducer.js index ea92ff5b1..f4386f7ff 100644 --- a/lib/reducers/entry-reducer.js +++ b/lib/reducers/entry-reducer.js @@ -1,685 +1,674 @@ // @flow import invariant from 'invariant'; import _filter from 'lodash/fp/filter.js'; import _flow from 'lodash/fp/flow.js'; import _groupBy from 'lodash/fp/groupBy.js'; import _isEqual from 'lodash/fp/isEqual.js'; import _map from 'lodash/fp/map.js'; import _mapKeys from 'lodash/fp/mapKeys.js'; import _mapValues from 'lodash/fp/mapValues.js'; import _omitBy from 'lodash/fp/omitBy.js'; import _pickBy from 'lodash/fp/pickBy.js'; import _sortBy from 'lodash/fp/sortBy.js'; import _union from 'lodash/fp/union.js'; import { fetchEntriesActionTypes, updateCalendarQueryActionTypes, createLocalEntryActionType, createEntryActionTypes, saveEntryActionTypes, concurrentModificationResetActionType, deleteEntryActionTypes, fetchRevisionsForEntryActionTypes, restoreEntryActionTypes, } from '../actions/entry-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { deleteThreadActionTypes, leaveThreadActionTypes, joinThreadActionTypes, changeThreadSettingsActionTypes, removeUsersFromThreadActionTypes, changeThreadMemberRolesActionTypes, newThreadActionTypes, } from '../actions/thread-actions.js'; import { keyserverAuthActionTypes, - logOutActionTypes, deleteKeyserverAccountActionTypes, - deleteAccountActionTypes, logInActionTypes, } from '../actions/user-actions.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import { entryID } from '../shared/entry-utils.js'; import { stateSyncSpecs } from '../shared/state-sync/state-sync-specs.js'; import { threadInFilterList } from '../shared/thread-utils.js'; import { updateSpecs } from '../shared/updates/update-specs.js'; import type { RawEntryInfo, EntryStore } from '../types/entry-types.js'; import type { BaseAction } from '../types/redux-types.js'; import { type ClientEntryInconsistencyReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, processServerRequestsActionType, } from '../types/request-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; import type { RawThreadInfos } from '../types/thread-types.js'; import { type ClientUpdateInfo, processUpdatesActionType, } from '../types/update-types.js'; import { dateString } from '../utils/date-utils.js'; import { values } from '../utils/objects.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; function daysToEntriesFromEntryInfos( entryInfos: $ReadOnlyArray, ): { [day: string]: string[] } { return _flow( _sortBy((['id', 'localID']: $ReadOnlyArray)), _groupBy((entryInfo: RawEntryInfo) => dateString(entryInfo.year, entryInfo.month, entryInfo.day), ), _mapValues((entryInfoGroup: $ReadOnlyArray) => _map(entryID)(entryInfoGroup), ), )([...entryInfos]); } function filterExistingDaysToEntriesWithNewEntryInfos( oldDaysToEntries: { +[id: string]: string[] }, newEntryInfos: { +[id: string]: RawEntryInfo }, ) { return _mapValues((entryIDs: string[]) => _filter((id: string) => newEntryInfos[id])(entryIDs), )(oldDaysToEntries); } function mergeNewEntryInfos( currentEntryInfos: { +[id: string]: RawEntryInfo }, currentDaysToEntries: ?{ +[day: string]: string[] }, newEntryInfos: $ReadOnlyArray, threadInfos: RawThreadInfos, ) { const mergedEntryInfos: { [string]: RawEntryInfo } = {}; let someEntryUpdated = false; for (const rawEntryInfo of newEntryInfos) { const serverID = rawEntryInfo.id; invariant(serverID, 'new entryInfos should have serverID'); const currentEntryInfo = currentEntryInfos[serverID]; let newEntryInfo; if (currentEntryInfo && currentEntryInfo.localID) { newEntryInfo = { id: serverID, // Try to preserve localIDs. This is because we use them as React // keys and changing React keys leads to loss of component state. localID: currentEntryInfo.localID, threadID: rawEntryInfo.threadID, text: rawEntryInfo.text, year: rawEntryInfo.year, month: rawEntryInfo.month, day: rawEntryInfo.day, creationTime: rawEntryInfo.creationTime, creatorID: rawEntryInfo.creatorID, deleted: rawEntryInfo.deleted, }; } else { newEntryInfo = { id: serverID, threadID: rawEntryInfo.threadID, text: rawEntryInfo.text, year: rawEntryInfo.year, month: rawEntryInfo.month, day: rawEntryInfo.day, creationTime: rawEntryInfo.creationTime, creatorID: rawEntryInfo.creatorID, deleted: rawEntryInfo.deleted, }; } if (_isEqual(currentEntryInfo)(newEntryInfo)) { mergedEntryInfos[serverID] = currentEntryInfo; } else { mergedEntryInfos[serverID] = newEntryInfo; someEntryUpdated = true; } } for (const id in currentEntryInfos) { const newEntryInfo = mergedEntryInfos[id]; if (!newEntryInfo) { mergedEntryInfos[id] = currentEntryInfos[id]; } } for (const id in mergedEntryInfos) { const entryInfo = mergedEntryInfos[id]; if (!threadInFilterList(threadInfos[entryInfo.threadID])) { someEntryUpdated = true; delete mergedEntryInfos[id]; } } const daysToEntries = !currentDaysToEntries || someEntryUpdated ? daysToEntriesFromEntryInfos(values(mergedEntryInfos)) : currentDaysToEntries; const entryInfos = someEntryUpdated ? mergedEntryInfos : currentEntryInfos; return [entryInfos, daysToEntries]; } function reduceEntryInfos( entryStore: EntryStore, action: BaseAction, newThreadInfos: RawThreadInfos, ): [EntryStore, $ReadOnlyArray] { const { entryInfos, daysToEntries, lastUserInteractionCalendar } = entryStore; if ( - action.type === logOutActionTypes.success || action.type === deleteKeyserverAccountActionTypes.success || - action.type === deleteAccountActionTypes.success || action.type === deleteThreadActionTypes.success || action.type === leaveThreadActionTypes.success ) { const authorizedThreadInfos = _pickBy(threadInFilterList)(newThreadInfos); const newEntryInfos = _pickBy( (entry: RawEntryInfo) => authorizedThreadInfos[entry.threadID], )(entryInfos); - let newLastUserInteractionCalendar = lastUserInteractionCalendar; - if ( - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success - ) { - newLastUserInteractionCalendar = 0; - } if (Object.keys(newEntryInfos).length === Object.keys(entryInfos).length) { return [ { entryInfos, daysToEntries, - lastUserInteractionCalendar: newLastUserInteractionCalendar, + lastUserInteractionCalendar, }, [], ]; } const newDaysToEntries = filterExistingDaysToEntriesWithNewEntryInfos( daysToEntries, newEntryInfos, ); return [ { entryInfos: newEntryInfos, daysToEntries: newDaysToEntries, - lastUserInteractionCalendar: newLastUserInteractionCalendar, + lastUserInteractionCalendar, }, [], ]; } else if (action.type === setNewSessionActionType) { const authorizedThreadInfos = _pickBy(threadInFilterList)(newThreadInfos); const newEntryInfos = _pickBy( (entry: RawEntryInfo) => authorizedThreadInfos[entry.threadID], )(entryInfos); const newDaysToEntries = filterExistingDaysToEntriesWithNewEntryInfos( daysToEntries, newEntryInfos, ); let newLastUserInteractionCalendar = lastUserInteractionCalendar; if ( action.payload.sessionChange.cookieInvalidated && !usingCommServicesAccessToken ) { newLastUserInteractionCalendar = 0; } if (Object.keys(newEntryInfos).length === Object.keys(entryInfos).length) { return [ { entryInfos, daysToEntries, lastUserInteractionCalendar: newLastUserInteractionCalendar, }, [], ]; } return [ { entryInfos: newEntryInfos, daysToEntries: newDaysToEntries, lastUserInteractionCalendar: newLastUserInteractionCalendar, }, [], ]; } else if (action.type === fetchEntriesActionTypes.success) { const [updatedEntryInfos, updatedDaysToEntries] = mergeNewEntryInfos( entryInfos, daysToEntries, action.payload.rawEntryInfos, newThreadInfos, ); return [ { entryInfos: updatedEntryInfos, daysToEntries: updatedDaysToEntries, lastUserInteractionCalendar, }, [], ]; } else if ( action.type === updateCalendarQueryActionTypes.started && action.payload && action.payload.calendarQuery ) { return [ { entryInfos, daysToEntries, lastUserInteractionCalendar: Date.now(), }, [], ]; } else if (action.type === updateCalendarQueryActionTypes.success) { const newLastUserInteractionCalendar = action.payload.calendarQuery ? Date.now() : lastUserInteractionCalendar; const [updatedEntryInfos, updatedDaysToEntries] = mergeNewEntryInfos( entryInfos, daysToEntries, action.payload.rawEntryInfos, newThreadInfos, ); const deletionMarkedEntryInfos = markDeletedEntries( updatedEntryInfos, action.payload.deletedEntryIDs, ); return [ { entryInfos: deletionMarkedEntryInfos, daysToEntries: updatedDaysToEntries, lastUserInteractionCalendar: newLastUserInteractionCalendar, }, [], ]; } else if (action.type === createLocalEntryActionType) { const entryInfo = action.payload; const localID = entryInfo.localID; invariant(localID, 'localID should be set in CREATE_LOCAL_ENTRY'); const newEntryInfos = { ...entryInfos, [(localID: string)]: entryInfo, }; const dayString = dateString( entryInfo.year, entryInfo.month, entryInfo.day, ); const newDaysToEntries = { ...daysToEntries, [dayString]: _union([localID])(daysToEntries[dayString]), }; return [ { entryInfos: newEntryInfos, daysToEntries: newDaysToEntries, lastUserInteractionCalendar: Date.now(), }, [], ]; } else if (action.type === createEntryActionTypes.success) { const localID = action.payload.localID; const serverID = action.payload.entryID; // If an entry with this serverID already got into the store somehow // (likely through an unrelated request), we need to dedup them. let rekeyedEntryInfos; if (entryInfos[serverID]) { // It's fair to assume the serverID entry is newer than the localID // entry, and this probably won't happen often, so for now we can just // keep the serverID entry. rekeyedEntryInfos = _omitBy( (candidate: RawEntryInfo) => !candidate.id && candidate.localID === localID, )(entryInfos); } else if (entryInfos[localID]) { rekeyedEntryInfos = _mapKeys((oldKey: string) => entryInfos[oldKey].localID === localID ? serverID : oldKey, )(entryInfos); } else { // This happens if the entry is deauthorized before it's saved return [entryStore, []]; } const [updatedEntryInfos, updatedDaysToEntries] = mergeNewEntryInfos( rekeyedEntryInfos, null, mergeUpdateEntryInfos([], action.payload.updatesResult.viewerUpdates), newThreadInfos, ); return [ { entryInfos: updatedEntryInfos, daysToEntries: updatedDaysToEntries, lastUserInteractionCalendar: Date.now(), }, [], ]; } else if (action.type === saveEntryActionTypes.success) { const serverID = action.payload.entryID; if ( !entryInfos[serverID] || !threadInFilterList(newThreadInfos[entryInfos[serverID].threadID]) ) { // This happens if the entry is deauthorized before it's saved return [entryStore, []]; } const [updatedEntryInfos, updatedDaysToEntries] = mergeNewEntryInfos( entryInfos, daysToEntries, mergeUpdateEntryInfos([], action.payload.updatesResult.viewerUpdates), newThreadInfos, ); return [ { entryInfos: updatedEntryInfos, daysToEntries: updatedDaysToEntries, lastUserInteractionCalendar: Date.now(), }, [], ]; } else if (action.type === concurrentModificationResetActionType) { const { payload } = action; if ( !entryInfos[payload.id] || !threadInFilterList(newThreadInfos[entryInfos[payload.id].threadID]) ) { // This happens if the entry is deauthorized before it's restored return [entryStore, []]; } const newEntryInfos = { ...entryInfos, [payload.id]: { ...entryInfos[payload.id], text: payload.dbText, }, }; return [ { entryInfos: newEntryInfos, daysToEntries, lastUserInteractionCalendar, }, [], ]; } else if (action.type === deleteEntryActionTypes.started) { const payload = action.payload; const id = payload.serverID && entryInfos[payload.serverID] ? payload.serverID : payload.localID; invariant(id, 'either serverID or localID should be set'); const newEntryInfos = { ...entryInfos, [(id: string)]: { ...entryInfos[id], deleted: true, }, }; return [ { entryInfos: newEntryInfos, daysToEntries, lastUserInteractionCalendar: Date.now(), }, [], ]; } else if (action.type === deleteEntryActionTypes.success) { const { payload } = action; if (payload) { const [updatedEntryInfos, updatedDaysToEntries] = mergeNewEntryInfos( entryInfos, daysToEntries, mergeUpdateEntryInfos([], payload.updatesResult.viewerUpdates), newThreadInfos, ); return [ { entryInfos: updatedEntryInfos, daysToEntries: updatedDaysToEntries, lastUserInteractionCalendar, }, [], ]; } } else if (action.type === fetchRevisionsForEntryActionTypes.success) { const id = action.payload.entryID; if ( !entryInfos[id] || !threadInFilterList(newThreadInfos[entryInfos[id].threadID]) ) { // This happens if the entry is deauthorized before it's restored return [entryStore, []]; } // Make sure the entry is in sync with its latest revision const newEntryInfos = { ...entryInfos, [id]: { ...entryInfos[id], text: action.payload.text, deleted: action.payload.deleted, }, }; return [ { entryInfos: newEntryInfos, daysToEntries, lastUserInteractionCalendar, }, [], ]; } else if (action.type === restoreEntryActionTypes.success) { const [updatedEntryInfos, updatedDaysToEntries] = mergeNewEntryInfos( entryInfos, daysToEntries, mergeUpdateEntryInfos([], action.payload.updatesResult.viewerUpdates), newThreadInfos, ); return [ { entryInfos: updatedEntryInfos, daysToEntries: updatedDaysToEntries, lastUserInteractionCalendar: Date.now(), }, [], ]; } else if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || action.type === keyserverAuthActionTypes.success ) { const { calendarResult } = action.payload; if (calendarResult) { const [updatedEntryInfos, updatedDaysToEntries] = mergeNewEntryInfos( entryInfos, daysToEntries, calendarResult.rawEntryInfos, newThreadInfos, ); return [ { entryInfos: updatedEntryInfos, daysToEntries: updatedDaysToEntries, lastUserInteractionCalendar, }, [], ]; } } else if (action.type === incrementalStateSyncActionType) { const [updatedEntryInfos, updatedDaysToEntries] = mergeNewEntryInfos( entryInfos, daysToEntries, mergeUpdateEntryInfos( action.payload.deltaEntryInfos, action.payload.updatesResult.newUpdates, ), newThreadInfos, ); const deletionMarkedEntryInfos = markDeletedEntries( updatedEntryInfos, action.payload.deletedEntryIDs, ); return [ { entryInfos: deletionMarkedEntryInfos, daysToEntries: updatedDaysToEntries, lastUserInteractionCalendar, }, [], ]; } else if ( action.type === processUpdatesActionType || action.type === joinThreadActionTypes.success || action.type === newThreadActionTypes.success ) { const [updatedEntryInfos, updatedDaysToEntries] = mergeNewEntryInfos( entryInfos, daysToEntries, mergeUpdateEntryInfos([], action.payload.updatesResult.newUpdates), newThreadInfos, ); return [ { entryInfos: updatedEntryInfos, daysToEntries: updatedDaysToEntries, lastUserInteractionCalendar, }, [], ]; } else if (action.type === fullStateSyncActionType) { const [updatedEntryInfos, updatedDaysToEntries] = mergeNewEntryInfos( entryInfos, daysToEntries, action.payload.rawEntryInfos, newThreadInfos, ); return [ { entryInfos: updatedEntryInfos, daysToEntries: updatedDaysToEntries, lastUserInteractionCalendar, }, [], ]; } else if ( action.type === changeThreadSettingsActionTypes.success || action.type === removeUsersFromThreadActionTypes.success || action.type === changeThreadMemberRolesActionTypes.success ) { const authorizedThreadInfos = _pickBy(threadInFilterList)(newThreadInfos); const newEntryInfos = _pickBy( (entry: RawEntryInfo) => authorizedThreadInfos[entry.threadID], )(entryInfos); if (Object.keys(newEntryInfos).length === Object.keys(entryInfos).length) { return [entryStore, []]; } const newDaysToEntries = filterExistingDaysToEntriesWithNewEntryInfos( daysToEntries, newEntryInfos, ); return [ { entryInfos: newEntryInfos, daysToEntries: newDaysToEntries, lastUserInteractionCalendar, }, [], ]; } else if (action.type === processServerRequestsActionType) { const checkStateRequest = action.payload.serverRequests.find( candidate => candidate.type === serverRequestTypes.CHECK_STATE, ); if (!checkStateRequest || !checkStateRequest.stateChanges) { return [entryStore, []]; } const { rawEntryInfos, deleteEntryIDs } = checkStateRequest.stateChanges; if (!rawEntryInfos && !deleteEntryIDs) { return [entryStore, []]; } const updatedEntryInfos: { [string]: RawEntryInfo } = { ...entryInfos }; if (deleteEntryIDs) { for (const deleteEntryID of deleteEntryIDs) { delete updatedEntryInfos[deleteEntryID]; } } let mergedEntryInfos: { +[string]: RawEntryInfo }; let mergedDaysToEntries; if (rawEntryInfos) { [mergedEntryInfos, mergedDaysToEntries] = mergeNewEntryInfos( updatedEntryInfos, null, rawEntryInfos, newThreadInfos, ); } else { mergedEntryInfos = updatedEntryInfos; mergedDaysToEntries = daysToEntriesFromEntryInfos( values(updatedEntryInfos), ); } const newInconsistencies = stateSyncSpecs.entries.findStoreInconsistencies( action, entryInfos, mergedEntryInfos, ); return [ { entryInfos: mergedEntryInfos, daysToEntries: mergedDaysToEntries, lastUserInteractionCalendar, }, newInconsistencies, ]; } return [entryStore, []]; } function mergeUpdateEntryInfos( entryInfos: $ReadOnlyArray, newUpdates: $ReadOnlyArray, ): RawEntryInfo[] { const entryIDs = new Set( entryInfos.map(entryInfo => entryInfo.id).filter(Boolean), ); const mergedEntryInfos = [...entryInfos]; for (const updateInfo of newUpdates) { updateSpecs[updateInfo.type].mergeEntryInfos?.( entryIDs, mergedEntryInfos, updateInfo, ); } return mergedEntryInfos; } function markDeletedEntries( entryInfos: { +[id: string]: RawEntryInfo }, deletedEntryIDs: $ReadOnlyArray, ): { +[id: string]: RawEntryInfo } { let result = entryInfos; for (const deletedEntryID of deletedEntryIDs) { const entryInfo = entryInfos[deletedEntryID]; if (!entryInfo || entryInfo.deleted) { continue; } result = { ...result, [deletedEntryID]: { ...entryInfo, deleted: true, }, }; } return result; } export { daysToEntriesFromEntryInfos, reduceEntryInfos }; diff --git a/lib/reducers/invite-links-reducer.js b/lib/reducers/invite-links-reducer.js index 7da756bfb..6f6c9989a 100644 --- a/lib/reducers/invite-links-reducer.js +++ b/lib/reducers/invite-links-reducer.js @@ -1,101 +1,90 @@ // @flow import { createOrUpdatePublicLinkActionTypes, disableInviteLinkLinkActionTypes, fetchPrimaryInviteLinkActionTypes, } from '../actions/link-actions.js'; -import { - deleteKeyserverAccountActionTypes, - deleteAccountActionTypes, - logOutActionTypes, -} from '../actions/user-actions.js'; +import { deleteKeyserverAccountActionTypes } from '../actions/user-actions.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import type { InviteLinksStore, CommunityLinks } from '../types/link-types.js'; import type { BaseAction } from '../types/redux-types.js'; function reduceInviteLinks( state: InviteLinksStore, action: BaseAction, ): InviteLinksStore { if (action.type === fetchPrimaryInviteLinkActionTypes.success) { const links: { [string]: CommunityLinks } = {}; for (const link of action.payload.links) { links[link.communityID] = { ...state.links[link.communityID], primaryLink: link, }; } return { links, }; } else if (action.type === createOrUpdatePublicLinkActionTypes.success) { const communityID = action.payload.communityID; return { ...state, links: { ...state.links, [communityID]: { ...state.links[communityID], primaryLink: action.payload, }, }, }; } else if (action.type === disableInviteLinkLinkActionTypes.success) { const communityID = action.payload.communityID; const currentPrimaryLink = state.links[communityID]?.primaryLink; if (currentPrimaryLink?.name !== action.payload.name) { return state; } const communityLinks = { ...state.links[communityID], primaryLink: null, }; return { ...state, links: { ...state.links, [communityID]: communityLinks, }, }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const keyserverIDsSet = new Set(action.payload.keyserverIDs); const newLinks: { [communityID: string]: CommunityLinks } = {}; for (const linkID in state.links) { if (!keyserverIDsSet.has(extractKeyserverIDFromID(linkID))) { newLinks[linkID] = state.links[linkID]; } } return { ...state, links: newLinks, }; - } else if ( - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success - ) { - return { - links: {}, - }; } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated ) { const { keyserverID } = action.payload; const newLinks: { [communityID: string]: CommunityLinks } = {}; for (const linkID in state.links) { if (extractKeyserverIDFromID(linkID) !== keyserverID) { newLinks[linkID] = state.links[linkID]; } } return { ...state, links: newLinks, }; } return state; } export default reduceInviteLinks; diff --git a/lib/reducers/message-reducer.js b/lib/reducers/message-reducer.js index 6f021bd64..c05939fbf 100644 --- a/lib/reducers/message-reducer.js +++ b/lib/reducers/message-reducer.js @@ -1,1748 +1,1723 @@ // @flow import invariant from 'invariant'; import _difference from 'lodash/fp/difference.js'; import _flow from 'lodash/fp/flow.js'; import _isEqual from 'lodash/fp/isEqual.js'; import _keyBy from 'lodash/fp/keyBy.js'; import _map from 'lodash/fp/map.js'; import _mapKeys from 'lodash/fp/mapKeys.js'; import _mapValues from 'lodash/fp/mapValues.js'; import _omit from 'lodash/fp/omit.js'; import _omitBy from 'lodash/fp/omitBy.js'; import _pickBy from 'lodash/fp/pickBy.js'; import _uniq from 'lodash/fp/uniq.js'; import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { createEntryActionTypes, saveEntryActionTypes, deleteEntryActionTypes, restoreEntryActionTypes, } from '../actions/entry-actions.js'; import { toggleMessagePinActionTypes, fetchMessagesBeforeCursorActionTypes, fetchMostRecentMessagesActionTypes, sendTextMessageActionTypes, sendMultimediaMessageActionTypes, sendReactionMessageActionTypes, sendEditMessageActionTypes, saveMessagesActionType, processMessagesActionType, messageStorePruneActionType, createLocalMessageActionType, fetchSingleMostRecentMessagesFromThreadsActionTypes, } from '../actions/message-actions.js'; import { sendMessageReportActionTypes } from '../actions/message-report-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { changeThreadSettingsActionTypes, deleteThreadActionTypes, leaveThreadActionTypes, newThreadActionTypes, removeUsersFromThreadActionTypes, changeThreadMemberRolesActionTypes, joinThreadActionTypes, } from '../actions/thread-actions.js'; import { updateMultimediaMessageMediaActionType } from '../actions/upload-actions.js'; import { keyserverAuthActionTypes, - tempIdentityLoginActionTypes, - logOutActionTypes, deleteKeyserverAccountActionTypes, - deleteAccountActionTypes, logInActionTypes, keyserverRegisterActionTypes, } from '../actions/user-actions.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import { messageStoreOpsHandlers, type MessageStoreOperation, type ReplaceMessageOperation, } from '../ops/message-store-ops.js'; import { pendingToRealizedThreadIDsSelector } from '../selectors/thread-selectors.js'; import { messageID, sortMessageInfoList, sortMessageIDs, mergeThreadMessageInfos, findNewestMessageTimePerKeyserver, localIDPrefix, } from '../shared/message-utils.js'; import { threadHasPermission, threadInChatList, threadIsPending, } from '../shared/thread-utils.js'; import threadWatcher from '../shared/thread-watcher.js'; import { unshimMessageInfos } from '../shared/unshim-utils.js'; import { updateSpecs } from '../shared/updates/update-specs.js'; import { logInActionSources } from '../types/account-types.js'; import type { Media, Image } from '../types/media-types.js'; import { messageTypes } from '../types/message-types-enum.js'; import { type RawMessageInfo, type LocalMessageInfo, type MessageStore, type MessageTruncationStatus, type MessageTruncationStatuses, messageTruncationStatus, defaultNumberPerThread, type ThreadMessageInfo, } from '../types/message-types.js'; import type { RawImagesMessageInfo } from '../types/messages/images.js'; import type { RawMediaMessageInfo } from '../types/messages/media.js'; import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { type BaseAction } from '../types/redux-types.js'; import { processServerRequestsActionType } from '../types/request-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; import { threadPermissions } from '../types/thread-permission-types.js'; import type { LegacyRawThreadInfo, RawThreadInfos, MixedRawThreadInfos, } from '../types/thread-types.js'; import { type ClientUpdateInfo, processUpdatesActionType, } from '../types/update-types.js'; import { translateClientDBThreadMessageInfos } from '../utils/message-ops-utils.js'; -import { ashoatKeyserverID } from '../utils/validation-utils.js'; const _mapValuesWithKeys = _mapValues.convert({ cap: false }); // Input must already be ordered! function mapThreadsToMessageIDsFromOrderedMessageInfos( orderedMessageInfos: $ReadOnlyArray, ): { [threadID: string]: string[] } { const threadsToMessageIDs: { [threadID: string]: string[] } = {}; for (const messageInfo of orderedMessageInfos) { const key = messageID(messageInfo); if (!threadsToMessageIDs[messageInfo.threadID]) { threadsToMessageIDs[messageInfo.threadID] = [key]; } else { threadsToMessageIDs[messageInfo.threadID].push(key); } } return threadsToMessageIDs; } function isThreadWatched( threadID: string, threadInfo: ?LegacyRawThreadInfo | ?RawThreadInfo, watchedIDs: $ReadOnlyArray, ) { return ( threadIsPending(threadID) || (threadInfo && threadHasPermission(threadInfo, threadPermissions.VISIBLE) && (threadInChatList(threadInfo) || watchedIDs.includes(threadID))) ); } const newThread = (): ThreadMessageInfo => ({ messageIDs: [], startReached: false, }); type FreshMessageStoreResult = { +messageStoreOperations: $ReadOnlyArray, +messageStore: MessageStore, }; function freshMessageStore( messageInfos: $ReadOnlyArray, truncationStatus: { [threadID: string]: MessageTruncationStatus }, currentAsOf: { +[keyserverID: string]: number }, threadInfos: MixedRawThreadInfos, ): FreshMessageStoreResult { const unshimmed = unshimMessageInfos(messageInfos); const orderedMessageInfos = sortMessageInfoList(unshimmed); const messages = _keyBy(messageID)(orderedMessageInfos); const messageStoreReplaceOperations = orderedMessageInfos.map( messageInfo => ({ type: 'replace', payload: { id: messageID(messageInfo), messageInfo }, }), ); const threadsToMessageIDs = mapThreadsToMessageIDsFromOrderedMessageInfos(orderedMessageInfos); const threads = _mapValuesWithKeys( (messageIDs: string[], threadID: string) => ({ ...newThread(), messageIDs, startReached: truncationStatus[threadID] === messageTruncationStatus.EXHAUSTIVE, }), )(threadsToMessageIDs); const watchedIDs = threadWatcher.getWatchedIDs(); for (const threadID in threadInfos) { const threadInfo = threadInfos[threadID]; if ( threads[threadID] || !isThreadWatched(threadID, threadInfo, watchedIDs) ) { continue; } threads[threadID] = newThread(); } const messageStoreOperations = [ { type: 'remove_all' }, { type: 'remove_all_threads', }, { type: 'replace_threads', payload: { threads }, }, ...messageStoreReplaceOperations, ]; return { messageStoreOperations, messageStore: { messages, threads, local: {}, currentAsOf, }, }; } type ReassignmentResult = { +messageStoreOperations: MessageStoreOperation[], +messageStore: MessageStore, +reassignedThreadIDs: string[], }; function reassignMessagesToRealizedThreads( messageStore: MessageStore, threadInfos: RawThreadInfos, ): ReassignmentResult { const pendingToRealizedThreadIDs = pendingToRealizedThreadIDsSelector(threadInfos); const messageStoreOperations: MessageStoreOperation[] = []; const messages: { [string]: RawMessageInfo } = {}; for (const storeMessageID in messageStore.messages) { const message = messageStore.messages[storeMessageID]; const newThreadID = pendingToRealizedThreadIDs.get(message.threadID); messages[storeMessageID] = newThreadID ? { ...message, threadID: newThreadID, time: threadInfos[newThreadID]?.creationTime ?? message.time, } : message; if (!newThreadID) { continue; } const updateMsgOperation: ReplaceMessageOperation = { type: 'replace', payload: { id: storeMessageID, messageInfo: messages[storeMessageID] }, }; messageStoreOperations.push(updateMsgOperation); } const threads: { [string]: ThreadMessageInfo } = {}; const reassignedThreadIDs = []; const updatedThreads: { [string]: ThreadMessageInfo } = {}; const threadsToRemove = []; for (const threadID in messageStore.threads) { const threadMessageInfo = messageStore.threads[threadID]; const newThreadID = pendingToRealizedThreadIDs.get(threadID); if (!newThreadID) { threads[threadID] = threadMessageInfo; continue; } const realizedThread = messageStore.threads[newThreadID]; if (!realizedThread) { reassignedThreadIDs.push(newThreadID); threads[newThreadID] = threadMessageInfo; updatedThreads[newThreadID] = threadMessageInfo; threadsToRemove.push(threadID); continue; } threads[newThreadID] = mergeThreadMessageInfos( threadMessageInfo, realizedThread, messages, ); updatedThreads[newThreadID] = threads[newThreadID]; } if (threadsToRemove.length) { messageStoreOperations.push({ type: 'remove_threads', payload: { ids: threadsToRemove, }, }); } messageStoreOperations.push({ type: 'replace_threads', payload: { threads: updatedThreads, }, }); return { messageStoreOperations, messageStore: { ...messageStore, threads, messages, }, reassignedThreadIDs, }; } type MergeNewMessagesResult = { +messageStoreOperations: $ReadOnlyArray, +messageStore: MessageStore, }; // oldMessageStore is from the old state // newMessageInfos, truncationStatus come from server function mergeNewMessages( oldMessageStore: MessageStore, newMessageInfos: $ReadOnlyArray, truncationStatus: { +[threadID: string]: MessageTruncationStatus }, threadInfos: RawThreadInfos, ): MergeNewMessagesResult { const { messageStoreOperations: updateWithLatestThreadInfosOps, messageStore: messageStoreUpdatedWithLatestThreadInfos, reassignedThreadIDs, } = updateMessageStoreWithLatestThreadInfos(oldMessageStore, threadInfos); const messageStoreAfterUpdateOps = processMessageStoreOperations( oldMessageStore, updateWithLatestThreadInfosOps, ); const updatedMessageStore = { ...messageStoreUpdatedWithLatestThreadInfos, messages: messageStoreAfterUpdateOps.messages, threads: messageStoreAfterUpdateOps.threads, }; const localIDsToServerIDs: Map = new Map(); const watchedThreadIDs = [ ...threadWatcher.getWatchedIDs(), ...reassignedThreadIDs, ]; const unshimmedNewMessages = unshimMessageInfos(newMessageInfos); const unshimmedNewMessagesOfWatchedThreads = unshimmedNewMessages.filter( msg => isThreadWatched( msg.threadID, threadInfos[msg.threadID], watchedThreadIDs, ), ); const orderedNewMessageInfos = _flow( _map((messageInfo: RawMessageInfo) => { const { id: inputID } = messageInfo; invariant(inputID, 'new messageInfos should have serverID'); invariant( !threadIsPending(messageInfo.threadID), 'new messageInfos should have realized thread id', ); const currentMessageInfo = updatedMessageStore.messages[inputID]; if ( messageInfo.type === messageTypes.TEXT || messageInfo.type === messageTypes.IMAGES || messageInfo.type === messageTypes.MULTIMEDIA ) { const { localID: inputLocalID } = messageInfo; const currentLocalMessageInfo = inputLocalID ? updatedMessageStore.messages[inputLocalID] : null; if (currentMessageInfo && currentMessageInfo.localID) { // If the client already has a RawMessageInfo with this serverID, keep // any localID associated with the existing one. This is because we // use localIDs as React keys and changing React keys leads to loss of // component state. (The conditional below is for Flow) if (messageInfo.type === messageTypes.TEXT) { messageInfo = { ...messageInfo, localID: currentMessageInfo.localID, }; } else if (messageInfo.type === messageTypes.MULTIMEDIA) { messageInfo = ({ ...messageInfo, localID: currentMessageInfo.localID, }: RawMediaMessageInfo); } else { messageInfo = ({ ...messageInfo, localID: currentMessageInfo.localID, }: RawImagesMessageInfo); } } else if (currentLocalMessageInfo && currentLocalMessageInfo.localID) { // If the client has a RawMessageInfo with this localID, but not with // the serverID, that means the message creation succeeded but the // success action never got processed. We set a key in // localIDsToServerIDs here to fix the messageIDs for the rest of the // MessageStore too. (The conditional below is for Flow) invariant(inputLocalID, 'inputLocalID should be set'); localIDsToServerIDs.set(inputLocalID, inputID); if (messageInfo.type === messageTypes.TEXT) { messageInfo = { ...messageInfo, localID: currentLocalMessageInfo.localID, }; } else if (messageInfo.type === messageTypes.MULTIMEDIA) { messageInfo = ({ ...messageInfo, localID: currentLocalMessageInfo.localID, }: RawMediaMessageInfo); } else { messageInfo = ({ ...messageInfo, localID: currentLocalMessageInfo.localID, }: RawImagesMessageInfo); } } else { // If neither the serverID nor the localID from the delivered // RawMessageInfo exists in the client store, then this message is // brand new to us. Ignore any localID provided by the server. // (The conditional below is for Flow) const { localID, ...rest } = messageInfo; if (rest.type === messageTypes.TEXT) { messageInfo = { ...rest }; } else if (rest.type === messageTypes.MULTIMEDIA) { messageInfo = ({ ...rest }: RawMediaMessageInfo); } else { messageInfo = ({ ...rest }: RawImagesMessageInfo); } } } else if ( currentMessageInfo && messageInfo.time > currentMessageInfo.time ) { // When thick threads will be introduced it will be possible for two // clients to create the same message (e.g. when they create the same // sidebar at the same time). We're going to use deterministic ids for // messages which should be unique within a thread and we have to find // a way for clients to agree which message to keep. We can't rely on // always choosing incoming messages nor messages from the store, // because a message that is in one user's store, will be send to // another user. One way to deal with it is to always choose a message // which is older, according to its timestamp. We can use this strategy // only for messages that can start a thread, because for other types // it might break the "contiguous" property of message ids (we can // consider selecting younger messages in that case, but for now we use // an invariant). invariant( messageInfo.type === messageTypes.CREATE_SIDEBAR || messageInfo.type === messageTypes.CREATE_THREAD || messageInfo.type === messageTypes.SIDEBAR_SOURCE, `Two different messages of type ${messageInfo.type} with the same ` + 'id found', ); return currentMessageInfo; } return _isEqual(messageInfo)(currentMessageInfo) ? currentMessageInfo : messageInfo; }), sortMessageInfoList, )(unshimmedNewMessagesOfWatchedThreads); const newMessageOps: MessageStoreOperation[] = []; const threadsToMessageIDs = mapThreadsToMessageIDsFromOrderedMessageInfos( orderedNewMessageInfos, ); const oldMessageInfosToCombine = []; const threadsThatNeedMessageIDsResorted = []; const local: { [string]: LocalMessageInfo } = {}; const updatedThreads: { [string]: ThreadMessageInfo } = {}; const threads = _flow( _mapValuesWithKeys((messageIDs: string[], threadID: string) => { const oldThread = updatedMessageStore.threads[threadID]; const truncate = truncationStatus[threadID]; if (!oldThread) { updatedThreads[threadID] = { ...newThread(), messageIDs, startReached: truncate === messageTruncationStatus.EXHAUSTIVE, }; return updatedThreads[threadID]; } let oldMessageIDsUnchanged = true; const oldMessageIDs = oldThread.messageIDs.map(oldID => { const newID = localIDsToServerIDs.get(oldID); if (newID !== null && newID !== undefined) { oldMessageIDsUnchanged = false; return newID; } return oldID; }); if (truncate === messageTruncationStatus.TRUNCATED) { // If the result set in the payload isn't contiguous with what we have // now, that means we need to dump what we have in the state and replace // it with the result set. We do this to achieve our two goals for the // messageStore: currentness and contiguousness. newMessageOps.push({ type: 'remove_messages_for_threads', payload: { threadIDs: [threadID] }, }); updatedThreads[threadID] = { messageIDs, startReached: false, }; return updatedThreads[threadID]; } const oldNotInNew = _difference(oldMessageIDs)(messageIDs); for (const id of oldNotInNew) { const oldMessageInfo = updatedMessageStore.messages[id]; invariant(oldMessageInfo, `could not find ${id} in messageStore`); oldMessageInfosToCombine.push(oldMessageInfo); const localInfo = updatedMessageStore.local[id]; if (localInfo) { local[id] = localInfo; } } const startReached = oldThread.startReached || truncate === messageTruncationStatus.EXHAUSTIVE; if (_difference(messageIDs)(oldMessageIDs).length === 0) { if (startReached === oldThread.startReached && oldMessageIDsUnchanged) { return oldThread; } updatedThreads[threadID] = { messageIDs: oldMessageIDs, startReached, }; return updatedThreads[threadID]; } const mergedMessageIDs = [...messageIDs, ...oldNotInNew]; threadsThatNeedMessageIDsResorted.push(threadID); return { messageIDs: mergedMessageIDs, startReached, }; }), _pickBy(thread => !!thread), )(threadsToMessageIDs); for (const threadID in updatedMessageStore.threads) { if (threads[threadID]) { continue; } let thread = updatedMessageStore.threads[threadID]; const truncate = truncationStatus[threadID]; if (truncate === messageTruncationStatus.EXHAUSTIVE) { thread = { ...thread, startReached: true, }; } threads[threadID] = thread; updatedThreads[threadID] = thread; for (const id of thread.messageIDs) { const messageInfo = updatedMessageStore.messages[id]; if (messageInfo) { oldMessageInfosToCombine.push(messageInfo); } const localInfo = updatedMessageStore.local[id]; if (localInfo) { local[id] = localInfo; } } } const messages = _flow( sortMessageInfoList, _keyBy(messageID), )([...orderedNewMessageInfos, ...oldMessageInfosToCombine]); const newMessages = _keyBy(messageID)(orderedNewMessageInfos); for (const id in newMessages) { newMessageOps.push({ type: 'replace', payload: { id, messageInfo: newMessages[id] }, }); } if (localIDsToServerIDs.size > 0) { newMessageOps.push({ type: 'remove', payload: { ids: [...localIDsToServerIDs.keys()] }, }); } for (const threadID of threadsThatNeedMessageIDsResorted) { threads[threadID].messageIDs = sortMessageIDs(messages)( threads[threadID].messageIDs, ); updatedThreads[threadID] = threads[threadID]; } newMessageOps.push({ type: 'replace_threads', payload: { threads: updatedThreads, }, }); const processedMessageStore = processMessageStoreOperations( updatedMessageStore, newMessageOps, ); const currentAsOf: { [keyserverID: string]: number } = {}; const newestMessageTimePerKeyserver = findNewestMessageTimePerKeyserver( orderedNewMessageInfos, ); for (const keyserverID in newestMessageTimePerKeyserver) { currentAsOf[keyserverID] = Math.max( newestMessageTimePerKeyserver[keyserverID], processedMessageStore.currentAsOf[keyserverID] ?? 0, ); } const messageStore = { messages: processedMessageStore.messages, threads: processedMessageStore.threads, local, currentAsOf: { ...processedMessageStore.currentAsOf, ...currentAsOf, }, }; return { messageStoreOperations: [ ...updateWithLatestThreadInfosOps, ...newMessageOps, ], messageStore, }; } type UpdateMessageStoreWithLatestThreadInfosResult = { +messageStoreOperations: MessageStoreOperation[], +messageStore: MessageStore, +reassignedThreadIDs: string[], }; function updateMessageStoreWithLatestThreadInfos( messageStore: MessageStore, threadInfos: RawThreadInfos, ): UpdateMessageStoreWithLatestThreadInfosResult { const messageStoreOperations: MessageStoreOperation[] = []; const { messageStore: reassignedMessageStore, messageStoreOperations: reassignMessagesOps, reassignedThreadIDs, } = reassignMessagesToRealizedThreads(messageStore, threadInfos); messageStoreOperations.push(...reassignMessagesOps); const watchedIDs = [...threadWatcher.getWatchedIDs(), ...reassignedThreadIDs]; const filteredThreads: { [string]: ThreadMessageInfo } = {}; const threadsToRemoveMessagesFrom = []; const messageIDsToRemove: string[] = []; for (const threadID in reassignedMessageStore.threads) { const threadMessageInfo = reassignedMessageStore.threads[threadID]; const threadInfo = threadInfos[threadID]; if (isThreadWatched(threadID, threadInfo, watchedIDs)) { filteredThreads[threadID] = threadMessageInfo; } else { threadsToRemoveMessagesFrom.push(threadID); messageIDsToRemove.push(...threadMessageInfo.messageIDs); } } const updatedThreads: { [string]: ThreadMessageInfo } = {}; for (const threadID in threadInfos) { const threadInfo = threadInfos[threadID]; if ( isThreadWatched(threadID, threadInfo, watchedIDs) && !filteredThreads[threadID] ) { filteredThreads[threadID] = newThread(); updatedThreads[threadID] = filteredThreads[threadID]; } } messageStoreOperations.push({ type: 'remove_threads', payload: { ids: threadsToRemoveMessagesFrom }, }); messageStoreOperations.push({ type: 'replace_threads', payload: { threads: updatedThreads, }, }); messageStoreOperations.push({ type: 'remove_messages_for_threads', payload: { threadIDs: threadsToRemoveMessagesFrom }, }); return { messageStoreOperations, messageStore: { messages: _omit(messageIDsToRemove)(reassignedMessageStore.messages), threads: filteredThreads, local: _omit(messageIDsToRemove)(reassignedMessageStore.local), currentAsOf: reassignedMessageStore.currentAsOf, }, reassignedThreadIDs, }; } function ensureRealizedThreadIDIsUsedWhenPossible( payload: T, threadInfos: RawThreadInfos, ): T { const pendingToRealizedThreadIDs = pendingToRealizedThreadIDsSelector(threadInfos); const realizedThreadID = pendingToRealizedThreadIDs.get(payload.threadID); return realizedThreadID ? { ...payload, threadID: realizedThreadID } : payload; } const { processStoreOperations: processMessageStoreOperations } = messageStoreOpsHandlers; type ReduceMessageStoreResult = { +messageStoreOperations: $ReadOnlyArray, +messageStore: MessageStore, }; function reduceMessageStore( messageStore: MessageStore, action: BaseAction, newThreadInfos: RawThreadInfos, ): ReduceMessageStoreResult { if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success ) { const { messagesResult } = action.payload; let { messageInfos } = messagesResult; // If it's a resolution attempt and the userID doesn't change, // then we should keep all local messages in the store // TODO we can't check if the userID changed until ENG-6126 if ( action.payload.logInActionSource === logInActionSources.cookieInvalidationResolutionAttempt || action.payload.logInActionSource === logInActionSources.socketAuthErrorResolutionAttempt ) { const localMessages = Object.values(messageStore.messages).filter( rawMessageInfo => messageID(rawMessageInfo).startsWith(localIDPrefix), ); messageInfos = [...messageInfos, ...localMessages]; } const { messageStoreOperations, messageStore: freshStore } = freshMessageStore( messageInfos, messagesResult.truncationStatus, messagesResult.currentAsOf, newThreadInfos, ); const processedMessageStore = processMessageStoreOperations( messageStore, messageStoreOperations, ); - return { - messageStoreOperations, - messageStore: { - ...freshStore, - messages: processedMessageStore.messages, - threads: processedMessageStore.threads, - }, - }; - } else if (action.type === tempIdentityLoginActionTypes.success) { - const { messageStoreOperations, messageStore: freshStore } = - freshMessageStore([], {}, { [ashoatKeyserverID]: 0 }, newThreadInfos); - - const processedMessageStore = processMessageStoreOperations( - messageStore, - messageStoreOperations, - ); - return { messageStoreOperations, messageStore: { ...freshStore, messages: processedMessageStore.messages, threads: processedMessageStore.threads, }, }; } else if (action.type === keyserverAuthActionTypes.success) { const { messagesResult } = action.payload; return mergeNewMessages( messageStore, messagesResult.messageInfos, messagesResult.truncationStatus, newThreadInfos, ); } else if (action.type === incrementalStateSyncActionType) { if ( action.payload.messagesResult.rawMessageInfos.length === 0 && action.payload.updatesResult.newUpdates.length === 0 ) { return { messageStoreOperations: [], messageStore }; } const messagesResult = mergeUpdatesWithMessageInfos( action.payload.messagesResult.rawMessageInfos, action.payload.updatesResult.newUpdates, action.payload.messagesResult.truncationStatuses, ); return mergeNewMessages( messageStore, messagesResult.rawMessageInfos, messagesResult.truncationStatuses, newThreadInfos, ); } else if (action.type === processUpdatesActionType) { if (action.payload.updatesResult.newUpdates.length === 0) { return { messageStoreOperations: [], messageStore }; } const messagesResult = mergeUpdatesWithMessageInfos( [], action.payload.updatesResult.newUpdates, ); const { messageStoreOperations, messageStore: newMessageStore } = mergeNewMessages( messageStore, messagesResult.rawMessageInfos, messagesResult.truncationStatuses, newThreadInfos, ); return { messageStoreOperations, messageStore: { messages: newMessageStore.messages, threads: newMessageStore.threads, local: newMessageStore.local, currentAsOf: messageStore.currentAsOf, }, }; } else if ( action.type === fullStateSyncActionType || action.type === processMessagesActionType ) { const { messagesResult } = action.payload; return mergeNewMessages( messageStore, messagesResult.rawMessageInfos, messagesResult.truncationStatuses, newThreadInfos, ); } else if ( action.type === fetchSingleMostRecentMessagesFromThreadsActionTypes.success ) { return mergeNewMessages( messageStore, action.payload.rawMessageInfos, action.payload.truncationStatuses, newThreadInfos, ); } else if ( action.type === fetchMessagesBeforeCursorActionTypes.success || action.type === fetchMostRecentMessagesActionTypes.success ) { return mergeNewMessages( messageStore, action.payload.rawMessageInfos, { [action.payload.threadID]: action.payload.truncationStatus }, newThreadInfos, ); - } else if ( - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success || - action.type === deleteKeyserverAccountActionTypes.success - ) { + } else if (action.type === deleteKeyserverAccountActionTypes.success) { const { messageStoreOperations, messageStore: filteredMessageStore } = updateMessageStoreWithLatestThreadInfos(messageStore, newThreadInfos); const processedMessageStore = processMessageStoreOperations( messageStore, messageStoreOperations, ); let currentAsOf = {}; if (action.payload.keyserverIDs) { currentAsOf = _omit(action.payload.keyserverIDs)( filteredMessageStore.currentAsOf, ); } return { messageStoreOperations, messageStore: { ...filteredMessageStore, currentAsOf, messages: processedMessageStore.messages, threads: processedMessageStore.threads, }, }; } else if ( action.type === deleteThreadActionTypes.success || action.type === leaveThreadActionTypes.success || action.type === setNewSessionActionType ) { const { messageStoreOperations, messageStore: filteredMessageStore } = updateMessageStoreWithLatestThreadInfos(messageStore, newThreadInfos); const processedMessageStore = processMessageStoreOperations( messageStore, messageStoreOperations, ); return { messageStoreOperations, messageStore: { ...filteredMessageStore, messages: processedMessageStore.messages, threads: processedMessageStore.threads, }, }; } else if (action.type === newThreadActionTypes.success) { const messagesResult = mergeUpdatesWithMessageInfos( action.payload.newMessageInfos, action.payload.updatesResult.newUpdates, ); return mergeNewMessages( messageStore, messagesResult.rawMessageInfos, messagesResult.truncationStatuses, newThreadInfos, ); } else if (action.type === sendMessageReportActionTypes.success) { return mergeNewMessages( messageStore, [action.payload.messageInfo], { [(action.payload.messageInfo.threadID: string)]: messageTruncationStatus.UNCHANGED, }, newThreadInfos, ); } else if (action.type === keyserverRegisterActionTypes.success) { const truncationStatuses: { [string]: MessageTruncationStatus } = {}; for (const messageInfo of action.payload.rawMessageInfos) { truncationStatuses[messageInfo.threadID] = messageTruncationStatus.EXHAUSTIVE; } return mergeNewMessages( messageStore, action.payload.rawMessageInfos, truncationStatuses, newThreadInfos, ); } else if ( action.type === changeThreadSettingsActionTypes.success || action.type === removeUsersFromThreadActionTypes.success || action.type === changeThreadMemberRolesActionTypes.success || action.type === createEntryActionTypes.success || action.type === saveEntryActionTypes.success || action.type === restoreEntryActionTypes.success || action.type === toggleMessagePinActionTypes.success ) { return mergeNewMessages( messageStore, action.payload.newMessageInfos, { [(action.payload.threadID: string)]: messageTruncationStatus.UNCHANGED, }, newThreadInfos, ); } else if (action.type === deleteEntryActionTypes.success) { const payload = action.payload; if (payload) { return mergeNewMessages( messageStore, payload.newMessageInfos, { [payload.threadID]: messageTruncationStatus.UNCHANGED }, newThreadInfos, ); } } else if (action.type === joinThreadActionTypes.success) { const messagesResult = mergeUpdatesWithMessageInfos( action.payload.rawMessageInfos, action.payload.updatesResult.newUpdates, ); return mergeNewMessages( messageStore, messagesResult.rawMessageInfos, messagesResult.truncationStatuses, newThreadInfos, ); } else if (action.type === sendEditMessageActionTypes.success) { const { newMessageInfos } = action.payload; const truncationStatuses: { [string]: MessageTruncationStatus } = {}; for (const messageInfo of newMessageInfos) { truncationStatuses[messageInfo.threadID] = messageTruncationStatus.UNCHANGED; } return mergeNewMessages( messageStore, newMessageInfos, truncationStatuses, newThreadInfos, ); } else if ( action.type === sendTextMessageActionTypes.started || action.type === sendMultimediaMessageActionTypes.started || action.type === sendReactionMessageActionTypes.started ) { const payload = ensureRealizedThreadIDIsUsedWhenPossible( action.payload, newThreadInfos, ); const { localID, threadID } = payload; invariant(localID, `localID should be set on ${action.type}`); const messageIDs = messageStore.threads[threadID]?.messageIDs ?? []; if (!messageStore.messages[localID]) { for (const existingMessageID of messageIDs) { const existingMessageInfo = messageStore.messages[existingMessageID]; if (existingMessageInfo && existingMessageInfo.localID === localID) { return { messageStoreOperations: [], messageStore }; } } } const messageStoreOperations: MessageStoreOperation[] = [ { type: 'replace', payload: { id: localID, messageInfo: payload }, }, ]; let updatedThreads; let local = { ...messageStore.local }; if (messageStore.messages[localID]) { const messages = { ...messageStore.messages, [(localID: string)]: payload, }; local = _pickBy( (localInfo: LocalMessageInfo, key: string) => key !== localID, )(messageStore.local); const thread = messageStore.threads[threadID]; updatedThreads = { [(threadID: string)]: { messageIDs: sortMessageIDs(messages)(messageIDs), startReached: thread?.startReached ?? true, }, }; } else { updatedThreads = { [(threadID: string)]: messageStore.threads[threadID] ? { ...messageStore.threads[threadID], messageIDs: [localID, ...messageIDs], } : { messageIDs: [localID], startReached: true, }, }; } messageStoreOperations.push({ type: 'replace_threads', payload: { threads: { ...updatedThreads }, }, }); const processedMessageStore = processMessageStoreOperations( messageStore, messageStoreOperations, ); const newMessageStore = { messages: processedMessageStore.messages, threads: processedMessageStore.threads, local, currentAsOf: messageStore.currentAsOf, }; return { messageStoreOperations, messageStore: newMessageStore, }; } else if ( action.type === sendTextMessageActionTypes.failed || action.type === sendMultimediaMessageActionTypes.failed ) { const { localID } = action.payload; return { messageStoreOperations: [], messageStore: { messages: messageStore.messages, threads: messageStore.threads, local: { ...messageStore.local, [(localID: string)]: { sendFailed: true }, }, currentAsOf: messageStore.currentAsOf, }, }; } else if (action.type === sendReactionMessageActionTypes.failed) { const { localID, threadID } = action.payload; const messageStoreOperations: MessageStoreOperation[] = []; messageStoreOperations.push({ type: 'remove', payload: { ids: [localID] }, }); const newMessageIDs = messageStore.threads[threadID].messageIDs.filter( id => id !== localID, ); const updatedThreads = { [threadID]: { ...messageStore.threads[threadID], messageIDs: newMessageIDs, }, }; messageStoreOperations.push({ type: 'replace_threads', payload: { threads: updatedThreads }, }); const processedMessageStore = processMessageStoreOperations( messageStore, messageStoreOperations, ); return { messageStoreOperations, messageStore: processedMessageStore, }; } else if ( action.type === sendTextMessageActionTypes.success || action.type === sendMultimediaMessageActionTypes.success || action.type === sendReactionMessageActionTypes.success ) { const { payload } = action; invariant( !threadIsPending(payload.threadID), 'Successful message action should have realized thread id', ); const replaceMessageKey = (messageKey: string) => messageKey === payload.localID ? payload.serverID : messageKey; let newMessages; const messageStoreOperations: MessageStoreOperation[] = []; if (messageStore.messages[payload.serverID]) { // If somehow the serverID got in there already, we'll just update the // serverID message and scrub the localID one newMessages = _omitBy( (messageInfo: RawMessageInfo) => messageInfo.type === messageTypes.TEXT && !messageInfo.id && messageInfo.localID === payload.localID, )(messageStore.messages); messageStoreOperations.push({ type: 'remove', payload: { ids: [payload.localID] }, }); } else if (messageStore.messages[payload.localID]) { // The normal case, the localID message gets replaced by the serverID one newMessages = _mapKeys(replaceMessageKey)(messageStore.messages); messageStoreOperations.push({ type: 'rekey', payload: { from: payload.localID, to: payload.serverID }, }); } else { // Well this is weird, we probably got deauthorized between when the // action was dispatched and when we ran this reducer... return { messageStoreOperations, messageStore }; } const newMessage = { ...newMessages[payload.serverID], id: payload.serverID, localID: payload.localID, time: payload.time, }; newMessages[payload.serverID] = newMessage; messageStoreOperations.push({ type: 'replace', payload: { id: payload.serverID, messageInfo: newMessage }, }); const threadID = payload.threadID; const newMessageIDs = _flow( _uniq, sortMessageIDs(newMessages), )(messageStore.threads[threadID].messageIDs.map(replaceMessageKey)); const local = _pickBy( (localInfo: LocalMessageInfo, key: string) => key !== payload.localID, )(messageStore.local); const updatedThreads = { [threadID]: { ...messageStore.threads[threadID], messageIDs: newMessageIDs, }, }; messageStoreOperations.push({ type: 'replace_threads', payload: { threads: updatedThreads }, }); const processedMessageStore = processMessageStoreOperations( messageStore, messageStoreOperations, ); return { messageStoreOperations, messageStore: { ...messageStore, messages: processedMessageStore.messages, threads: processedMessageStore.threads, local, }, }; } else if (action.type === saveMessagesActionType) { const truncationStatuses: { [string]: MessageTruncationStatus } = {}; for (const messageInfo of action.payload.rawMessageInfos) { truncationStatuses[messageInfo.threadID] = messageTruncationStatus.UNCHANGED; } const { messageStoreOperations, messageStore: newMessageStore } = mergeNewMessages( messageStore, action.payload.rawMessageInfos, truncationStatuses, newThreadInfos, ); return { messageStoreOperations, messageStore: { messages: newMessageStore.messages, threads: newMessageStore.threads, local: newMessageStore.local, // We avoid bumping currentAsOf because notifs may include a contracted // RawMessageInfo, so we want to make sure we still fetch it currentAsOf: messageStore.currentAsOf, }, }; } else if (action.type === messageStorePruneActionType) { const messageIDsToPrune = []; const updatedThreads: { [string]: ThreadMessageInfo } = {}; for (const threadID of action.payload.threadIDs) { let thread = messageStore.threads[threadID]; if (!thread) { continue; } const newMessageIDs = [...thread.messageIDs]; const removed = newMessageIDs.splice(defaultNumberPerThread); if (removed.length > 0) { thread = { ...thread, messageIDs: newMessageIDs, startReached: false, }; } for (const id of removed) { messageIDsToPrune.push(id); } updatedThreads[threadID] = thread; } const messageStoreOperations = [ { type: 'remove', payload: { ids: messageIDsToPrune }, }, { type: 'replace_threads', payload: { threads: updatedThreads, }, }, ]; const processedMessageStore = processMessageStoreOperations( messageStore, messageStoreOperations, ); const newMessageStore = { messages: processedMessageStore.messages, threads: processedMessageStore.threads, local: _omit(messageIDsToPrune)(messageStore.local), currentAsOf: messageStore.currentAsOf, }; return { messageStoreOperations, messageStore: newMessageStore, }; } else if (action.type === updateMultimediaMessageMediaActionType) { const { messageID: id, currentMediaID, mediaUpdate } = action.payload; const message = messageStore.messages[id]; invariant(message, `message with ID ${id} could not be found`); invariant( message.type === messageTypes.IMAGES || message.type === messageTypes.MULTIMEDIA, `message with ID ${id} is not multimedia`, ); let updatedMessage; let replaced = false; if (message.type === messageTypes.IMAGES) { const media: Image[] = []; for (const singleMedia of message.media) { if (singleMedia.id !== currentMediaID) { media.push(singleMedia); } else { let updatedMedia: Image = { id: mediaUpdate.id ?? singleMedia.id, type: 'photo', uri: mediaUpdate.uri ?? singleMedia.uri, dimensions: mediaUpdate.dimensions ?? singleMedia.dimensions, thumbHash: mediaUpdate.thumbHash ?? singleMedia.thumbHash, }; if ( 'localMediaSelection' in singleMedia && !('localMediaSelection' in mediaUpdate) ) { updatedMedia = { ...updatedMedia, localMediaSelection: singleMedia.localMediaSelection, }; } else if (mediaUpdate.localMediaSelection) { updatedMedia = { ...updatedMedia, localMediaSelection: mediaUpdate.localMediaSelection, }; } media.push(updatedMedia); replaced = true; } } updatedMessage = { ...message, media }; } else { const media: Media[] = []; for (const singleMedia of message.media) { if (singleMedia.id !== currentMediaID) { media.push(singleMedia); } else if ( singleMedia.type === 'photo' && mediaUpdate.type === 'photo' ) { media.push({ ...singleMedia, ...mediaUpdate }); replaced = true; } else if ( singleMedia.type === 'video' && mediaUpdate.type === 'video' ) { media.push({ ...singleMedia, ...mediaUpdate }); replaced = true; } else if ( singleMedia.type === 'encrypted_photo' && mediaUpdate.type === 'encrypted_photo' ) { if (singleMedia.blobURI) { const { holder, ...rest } = mediaUpdate; if (holder) { console.log( `mediaUpdate contained holder for media ${singleMedia.id} ` + 'that has blobURI', ); } media.push({ ...singleMedia, ...rest }); } else { invariant( singleMedia.holder, 'Encrypted media must have holder or blobURI', ); const { blobURI, ...rest } = mediaUpdate; if (blobURI) { console.log( `mediaUpdate contained blobURI for media ${singleMedia.id} ` + 'that has holder', ); } media.push({ ...singleMedia, ...rest }); } replaced = true; } else if ( singleMedia.type === 'encrypted_video' && mediaUpdate.type === 'encrypted_video' ) { if (singleMedia.blobURI) { const { holder, thumbnailHolder, ...rest } = mediaUpdate; if (holder || thumbnailHolder) { console.log( 'mediaUpdate contained holder or thumbnailHolder for media ' + `${singleMedia.id} that has blobURI`, ); } media.push({ ...singleMedia, ...rest }); } else { invariant( singleMedia.holder, 'Encrypted media must have holder or blobURI', ); const { blobURI, thumbnailBlobURI, ...rest } = mediaUpdate; if (blobURI || thumbnailBlobURI) { console.log( 'mediaUpdate contained blobURI or thumbnailBlobURI for media ' + `${singleMedia.id} that has holder`, ); } media.push({ ...singleMedia, ...rest }); } replaced = true; } else if ( singleMedia.type === 'photo' && mediaUpdate.type === 'encrypted_photo' ) { // extract fields that are absent in encrypted_photo type const { uri, localMediaSelection, ...original } = singleMedia; const { holder: newHolder, blobURI: newBlobURI, encryptionKey, ...update } = mediaUpdate; const blobURI = newBlobURI ?? newHolder; invariant( blobURI && encryptionKey, 'holder and encryptionKey are required for encrypted_photo message', ); media.push({ ...original, ...update, type: 'encrypted_photo', blobURI, encryptionKey, }); replaced = true; } else if ( singleMedia.type === 'video' && mediaUpdate.type === 'encrypted_video' ) { const { uri, thumbnailURI, localMediaSelection, ...original } = singleMedia; const { holder: newHolder, blobURI: newBlobURI, encryptionKey, thumbnailHolder: newThumbnailHolder, thumbnailBlobURI: newThumbnailBlobURI, thumbnailEncryptionKey, ...update } = mediaUpdate; const blobURI = newBlobURI ?? newHolder; invariant( blobURI && encryptionKey, 'holder and encryptionKey are required for encrypted_video message', ); const thumbnailBlobURI = newThumbnailBlobURI ?? newThumbnailHolder; invariant( thumbnailBlobURI && thumbnailEncryptionKey, 'thumbnailHolder and thumbnailEncryptionKey are required for ' + 'encrypted_video message', ); media.push({ ...original, ...update, type: 'encrypted_video', blobURI, encryptionKey, thumbnailBlobURI, thumbnailEncryptionKey, }); replaced = true; } else if (mediaUpdate.id) { const { id: newID } = mediaUpdate; media.push({ ...singleMedia, id: newID }); replaced = true; } } updatedMessage = { ...message, media }; } invariant( replaced, `message ${id} did not contain media with ID ${currentMediaID}`, ); const messageStoreOperations = [ { type: 'replace', payload: { id, messageInfo: updatedMessage, }, }, ]; const processedMessageStore = processMessageStoreOperations( messageStore, messageStoreOperations, ); return { messageStoreOperations, messageStore: { ...messageStore, messages: processedMessageStore.messages, }, }; } else if (action.type === createLocalMessageActionType) { const messageInfo = ensureRealizedThreadIDIsUsedWhenPossible( action.payload, newThreadInfos, ); const { localID, threadID } = messageInfo; const messageIDs = messageStore.threads[messageInfo.threadID]?.messageIDs ?? []; const threadState: ThreadMessageInfo = messageStore.threads[threadID] ? { ...messageStore.threads[threadID], messageIDs: [localID, ...messageIDs], } : { messageIDs: [localID], startReached: true, }; const messageStoreOperations = [ { type: 'replace', payload: { id: localID, messageInfo }, }, { type: 'replace_threads', payload: { threads: { [(threadID: string)]: threadState }, }, }, ]; const processedMessageStore = processMessageStoreOperations( messageStore, messageStoreOperations, ); return { messageStoreOperations, messageStore: { ...messageStore, threads: processedMessageStore.threads, messages: processedMessageStore.messages, }, }; } else if (action.type === processServerRequestsActionType) { const { messageStoreOperations, messageStore: messageStoreAfterReassignment, } = reassignMessagesToRealizedThreads(messageStore, newThreadInfos); const processedMessageStore = processMessageStoreOperations( messageStore, messageStoreOperations, ); return { messageStoreOperations, messageStore: { ...messageStoreAfterReassignment, messages: processedMessageStore.messages, threads: processedMessageStore.threads, }, }; } else if (action.type === setClientDBStoreActionType) { const actionPayloadMessageStoreThreads = translateClientDBThreadMessageInfos( action.payload.messageStoreThreads ?? [], ); const newThreads: { [threadID: string]: ThreadMessageInfo, } = { ...messageStore.threads }; for (const threadID in actionPayloadMessageStoreThreads) { newThreads[threadID] = { ...actionPayloadMessageStoreThreads[threadID], messageIDs: messageStore.threads?.[threadID]?.messageIDs ?? [], }; } const payloadMessages = action.payload.messages; if (!payloadMessages) { return { messageStoreOperations: [], messageStore: { ...messageStore, threads: newThreads }, }; } const { messageStoreOperations, messageStore: updatedMessageStore } = updateMessageStoreWithLatestThreadInfos( { ...messageStore, threads: newThreads }, newThreadInfos, ); let threads = { ...updatedMessageStore.threads }; let local = { ...updatedMessageStore.local }; // Store message IDs already contained within threads so that we // do not insert duplicates const existingMessageIDs = new Set(); for (const threadID in threads) { threads[threadID].messageIDs.forEach(msgID => { existingMessageIDs.add(msgID); }); } const threadsNeedMsgIDsResorting = new Set(); const actionPayloadMessages = messageStoreOpsHandlers.translateClientDBData(payloadMessages); // When starting the app on native, we filter out any local-only multimedia // messages because the relevant context is no longer available const messageIDsToBeRemoved = []; const threadsToAdd: { [string]: ThreadMessageInfo } = {}; for (const id in actionPayloadMessages) { const message = actionPayloadMessages[id]; const { threadID } = message; let existingThread = threads[threadID]; if (!existingThread) { existingThread = newThread(); threadsToAdd[threadID] = existingThread; } if ( (message.type === messageTypes.IMAGES || message.type === messageTypes.MULTIMEDIA) && !message.id ) { messageIDsToBeRemoved.push(id); threads = { ...threads, [(threadID: string)]: { ...existingThread, messageIDs: existingThread.messageIDs.filter( curMessageID => curMessageID !== id, ), }, }; local = _pickBy( (localInfo: LocalMessageInfo, key: string) => key !== id, )(local); } else if (!existingMessageIDs.has(id)) { threads = { ...threads, [(threadID: string)]: { ...existingThread, messageIDs: [...existingThread.messageIDs, id], }, }; threadsNeedMsgIDsResorting.add(threadID); } else if (!threads[threadID]) { threads = { ...threads, [(threadID: string)]: existingThread }; } } for (const threadID of threadsNeedMsgIDsResorting) { threads[threadID].messageIDs = sortMessageIDs(actionPayloadMessages)( threads[threadID].messageIDs, ); } const newMessageStore = { ...updatedMessageStore, messages: actionPayloadMessages, threads: threads, local: local, }; if (messageIDsToBeRemoved.length > 0) { messageStoreOperations.push({ type: 'remove', payload: { ids: messageIDsToBeRemoved }, }); } const processedMessageStore = processMessageStoreOperations( newMessageStore, messageStoreOperations, ); messageStoreOperations.push({ type: 'replace_threads', payload: { threads: threadsToAdd }, }); return { messageStoreOperations, messageStore: processedMessageStore, }; } return { messageStoreOperations: [], messageStore }; } type MergedUpdatesWithMessages = { +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, }; function mergeUpdatesWithMessageInfos( messageInfos: $ReadOnlyArray, newUpdates: $ReadOnlyArray, truncationStatuses?: MessageTruncationStatuses, ): MergedUpdatesWithMessages { const messageIDs = new Set( messageInfos.map(messageInfo => messageInfo.id).filter(Boolean), ); const mergedMessageInfos = [...messageInfos]; const mergedTruncationStatuses = { ...truncationStatuses }; for (const update of newUpdates) { const { mergeMessageInfosAndTruncationStatuses } = updateSpecs[update.type]; if (!mergeMessageInfosAndTruncationStatuses) { continue; } mergeMessageInfosAndTruncationStatuses( messageIDs, mergedMessageInfos, mergedTruncationStatuses, update, ); } return { rawMessageInfos: mergedMessageInfos, truncationStatuses: mergedTruncationStatuses, }; } export { freshMessageStore, reduceMessageStore }; diff --git a/lib/reducers/policies-reducer.js b/lib/reducers/policies-reducer.js index 24674da73..d578499f2 100644 --- a/lib/reducers/policies-reducer.js +++ b/lib/reducers/policies-reducer.js @@ -1,60 +1,55 @@ // @flow import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { - deleteAccountActionTypes, logInActionTypes, logOutActionTypes, policyAcknowledgmentActionTypes, keyserverAuthActionTypes, } from '../actions/user-actions.js'; import type { PolicyType } from '../facts/policies.js'; import { type UserPolicies, forcePolicyAcknowledgmentActionType, } from '../types/policy-types.js'; import type { BaseAction } from '../types/redux-types.js'; function policiesReducer( state: UserPolicies, action: BaseAction, ): UserPolicies { if ( action.type === forcePolicyAcknowledgmentActionType || action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || action.type === keyserverAuthActionTypes.success ) { const { notAcknowledgedPolicies } = action.payload; if (!notAcknowledgedPolicies) { return state; } const newState = { ...state }; notAcknowledgedPolicies.forEach((policy: PolicyType) => { newState[policy] = { ...newState[policy], isAcknowledged: false, }; }); return newState; } else if (action.type === policyAcknowledgmentActionTypes.success) { const { policy } = action.payload; return { ...state, [policy]: { ...state[policy], isAcknowledged: true, }, }; - } else if ( - action.type === logOutActionTypes.started || - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success - ) { + } else if (action.type === logOutActionTypes.started) { return {}; } return state; } export default policiesReducer; diff --git a/lib/reducers/services-access-token-reducer.js b/lib/reducers/services-access-token-reducer.js index e472f6da5..ab0c416e1 100644 --- a/lib/reducers/services-access-token-reducer.js +++ b/lib/reducers/services-access-token-reducer.js @@ -1,33 +1,28 @@ // @flow import { logOutActionTypes, - deleteAccountActionTypes, setAccessTokenActionType, } from '../actions/user-actions.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import type { BaseAction } from '../types/redux-types.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; export default function reduceServicesAccessToken( state: ?string, action: BaseAction, ): ?string { if (action.type === setAccessTokenActionType) { return action.payload; } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && action.payload.sessionChange.currentUserInfo.anonymous && !usingCommServicesAccessToken ) { return null; - } else if ( - action.type === logOutActionTypes.started || - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success - ) { + } else if (action.type === logOutActionTypes.started) { return null; } return state; } diff --git a/lib/reducers/theme-reducer.js b/lib/reducers/theme-reducer.js index f4192643d..4071ed2da 100644 --- a/lib/reducers/theme-reducer.js +++ b/lib/reducers/theme-reducer.js @@ -1,51 +1,41 @@ // @flow import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { updateThemeInfoActionType } from '../actions/theme-actions.js'; import { - logOutActionTypes, - deleteAccountActionTypes, logInActionTypes, - tempIdentityLoginActionTypes, keyserverRegisterActionTypes, } from '../actions/user-actions.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import type { BaseAction } from '../types/redux-types.js'; import { defaultGlobalThemeInfo, type GlobalThemeInfo, } from '../types/theme-types.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; export default function reduceGlobalThemeInfo( state: GlobalThemeInfo, action: BaseAction, ): GlobalThemeInfo { if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || - action.type === tempIdentityLoginActionTypes.success || action.type === keyserverRegisterActionTypes.success ) { return defaultGlobalThemeInfo; } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && action.payload.sessionChange.currentUserInfo.anonymous && !usingCommServicesAccessToken ) { return defaultGlobalThemeInfo; - } else if ( - action.type === logOutActionTypes.started || - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success - ) { - return defaultGlobalThemeInfo; } else if (action.type === updateThemeInfoActionType) { return { ...state, ...action.payload, }; } return state; } diff --git a/lib/reducers/thread-activity-reducer.js b/lib/reducers/thread-activity-reducer.js index 4154bced9..257d76496 100644 --- a/lib/reducers/thread-activity-reducer.js +++ b/lib/reducers/thread-activity-reducer.js @@ -1,132 +1,123 @@ // @flow import invariant from 'invariant'; import { messageStorePruneActionType } from '../actions/message-actions.js'; import { changeThreadMemberRolesActionTypes, changeThreadSettingsActionTypes, deleteCommunityRoleActionTypes, deleteThreadActionTypes, joinThreadActionTypes, leaveThreadActionTypes, modifyCommunityRoleActionTypes, newThreadActionTypes, removeUsersFromThreadActionTypes, } from '../actions/thread-actions.js'; -import { - logOutActionTypes, - deleteKeyserverAccountActionTypes, - deleteAccountActionTypes, -} from '../actions/user-actions.js'; +import { deleteKeyserverAccountActionTypes } from '../actions/user-actions.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import type { BaseAction } from '../types/redux-types.js'; import { incrementalStateSyncActionType } from '../types/socket-types.js'; import type { ThreadActivityStore } from '../types/thread-activity-types.js'; import { updateThreadLastNavigatedActionType } from '../types/thread-activity-types.js'; import { updateTypes } from '../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../types/update-types.js'; import { processUpdatesActionType } from '../types/update-types.js'; function reduceThreadActivity( state: ThreadActivityStore, action: BaseAction, ): ThreadActivityStore { if (action.type === updateThreadLastNavigatedActionType) { const { threadID, time } = action.payload; const updatedThreadActivityStore = { ...state, [threadID]: { ...state[threadID], lastNavigatedTo: time, }, }; return updatedThreadActivityStore; } else if (action.type === messageStorePruneActionType) { const now = Date.now(); let updatedThreadActivityStore = { ...state }; for (const threadID: string of action.payload.threadIDs) { updatedThreadActivityStore = { ...updatedThreadActivityStore, [threadID]: { ...updatedThreadActivityStore[threadID], lastPruned: now, }, }; } return updatedThreadActivityStore; } else if ( action.type === joinThreadActionTypes.success || action.type === leaveThreadActionTypes.success || action.type === deleteThreadActionTypes.success || action.type === changeThreadSettingsActionTypes.success || action.type === removeUsersFromThreadActionTypes.success || action.type === changeThreadMemberRolesActionTypes.success || action.type === incrementalStateSyncActionType || action.type === processUpdatesActionType || action.type === newThreadActionTypes.success || action.type === modifyCommunityRoleActionTypes.success || action.type === deleteCommunityRoleActionTypes.success ) { const { newUpdates } = action.payload.updatesResult; if (newUpdates.length === 0) { return state; } const deleteThreadUpdates = newUpdates.filter( (update: ClientUpdateInfo) => update.type === updateTypes.DELETE_THREAD, ); if (deleteThreadUpdates.length === 0) { return state; } let updatedState = { ...state }; for (const update: ClientUpdateInfo of deleteThreadUpdates) { invariant( update.type === updateTypes.DELETE_THREAD, 'update must be of type DELETE_THREAD', ); const { [update.threadID]: _, ...stateSansRemovedThread } = updatedState; updatedState = stateSansRemovedThread; } return updatedState; } else if (action.type === deleteKeyserverAccountActionTypes.success) { let updatedState = { ...state }; const keyserverIDsSet = new Set(action.payload.keyserverIDs); for (const threadID in state) { if (!keyserverIDsSet.has(extractKeyserverIDFromID(threadID))) { continue; } const { [threadID]: _, ...stateSansRemovedThread } = updatedState; updatedState = stateSansRemovedThread; } return updatedState; - } else if ( - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success - ) { - return {}; } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated ) { const { keyserverID } = action.payload; let updatedState = { ...state }; for (const threadID in state) { if (extractKeyserverIDFromID(threadID) !== keyserverID) { continue; } const { [threadID]: _, ...stateSansRemovedThread } = updatedState; updatedState = stateSansRemovedThread; } return updatedState; } return state; } export { reduceThreadActivity }; diff --git a/lib/reducers/thread-reducer.js b/lib/reducers/thread-reducer.js index 4868cd09e..0fc72081a 100644 --- a/lib/reducers/thread-reducer.js +++ b/lib/reducers/thread-reducer.js @@ -1,500 +1,473 @@ // @flow import { setThreadUnreadStatusActionTypes, updateActivityActionTypes, } from '../actions/activity-actions.js'; import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { saveMessagesActionType } from '../actions/message-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { changeThreadSettingsActionTypes, deleteThreadActionTypes, newThreadActionTypes, removeUsersFromThreadActionTypes, changeThreadMemberRolesActionTypes, joinThreadActionTypes, leaveThreadActionTypes, modifyCommunityRoleActionTypes, deleteCommunityRoleActionTypes, } from '../actions/thread-actions.js'; import { keyserverAuthActionTypes, - logOutActionTypes, deleteKeyserverAccountActionTypes, logInActionTypes, keyserverRegisterActionTypes, updateSubscriptionActionTypes, - deleteAccountActionTypes, } from '../actions/user-actions.js'; import { getThreadIDsForKeyservers } from '../keyserver-conn/keyserver-call-utils.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import { type ThreadStoreOperation, threadStoreOpsHandlers, } from '../ops/thread-store-ops.js'; import { stateSyncSpecs } from '../shared/state-sync/state-sync-specs.js'; import { updateSpecs } from '../shared/updates/update-specs.js'; import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import type { BaseAction } from '../types/redux-types.js'; import { type ClientThreadInconsistencyReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, processServerRequestsActionType, } from '../types/request-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; import type { RawThreadInfos, ThreadStore } from '../types/thread-types.js'; import { type ClientUpdateInfo, processUpdatesActionType, } from '../types/update-types.js'; const { processStoreOperations: processThreadStoreOperations } = threadStoreOpsHandlers; function generateOpsForThreadUpdates( threadInfos: RawThreadInfos, payload: { +updatesResult: { +newUpdates: $ReadOnlyArray, ... }, ... }, ): $ReadOnlyArray { return payload.updatesResult.newUpdates .map(update => updateSpecs[update.type].generateOpsForThreadUpdates?.( threadInfos, update, ), ) .filter(Boolean) .flat(); } function reduceThreadInfos( state: ThreadStore, action: BaseAction, ): { threadStore: ThreadStore, newThreadInconsistencies: $ReadOnlyArray, threadStoreOperations: $ReadOnlyArray, } { if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || action.type === keyserverRegisterActionTypes.success || action.type === fullStateSyncActionType ) { const newThreadInfos = action.payload.threadInfos; const threadStoreOperations = [ { type: 'remove_all', }, ...Object.keys(newThreadInfos).map((id: string) => ({ type: 'replace', payload: { id, threadInfo: newThreadInfos[id] }, })), ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === keyserverAuthActionTypes.success) { const keyserverIDs = Object.keys(action.payload.updatesCurrentAsOf); const threadIDsToRemove = getThreadIDsForKeyservers( Object.keys(state.threadInfos), keyserverIDs, ); const newThreadInfos = action.payload.threadInfos; const threadStoreOperations = [ { type: 'remove', payload: { ids: threadIDsToRemove }, }, ...Object.keys(newThreadInfos).map((id: string) => ({ type: 'replace', payload: { id, threadInfo: newThreadInfos[id] }, })), ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; - } else if ( - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success - ) { - if (Object.keys(state.threadInfos).length === 0) { - return { - threadStore: state, - newThreadInconsistencies: [], - threadStoreOperations: [], - }; - } - const threadStoreOperations = [ - { - type: 'remove_all', - }, - ]; - const updatedThreadStore = processThreadStoreOperations( - state, - threadStoreOperations, - ); - return { - threadStore: updatedThreadStore, - newThreadInconsistencies: [], - threadStoreOperations, - }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const threadIDsToRemove = getThreadIDsForKeyservers( Object.keys(state.threadInfos), action.payload.keyserverIDs, ); if (threadIDsToRemove.length === 0) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const threadStoreOperations = [ { type: 'remove', payload: { ids: threadIDsToRemove }, }, ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated ) { const threadIDsToRemove = getThreadIDsForKeyservers( Object.keys(state.threadInfos), [action.payload.keyserverID], ); if (threadIDsToRemove.length === 0) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const threadStoreOperations = [ { type: 'remove', payload: { ids: threadIDsToRemove }, }, ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if ( action.type === joinThreadActionTypes.success || action.type === leaveThreadActionTypes.success || action.type === deleteThreadActionTypes.success || action.type === changeThreadSettingsActionTypes.success || action.type === removeUsersFromThreadActionTypes.success || action.type === changeThreadMemberRolesActionTypes.success || action.type === incrementalStateSyncActionType || action.type === processUpdatesActionType || action.type === newThreadActionTypes.success || action.type === modifyCommunityRoleActionTypes.success || action.type === deleteCommunityRoleActionTypes.success ) { const { newUpdates } = action.payload.updatesResult; if (newUpdates.length === 0) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const threadStoreOperations = generateOpsForThreadUpdates( state.threadInfos, action.payload, ); const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === updateSubscriptionActionTypes.success) { const { threadID, subscription } = action.payload; const threadInfo = state.threadInfos[threadID]; const newThreadInfo = { ...threadInfo, currentUser: { ...threadInfo.currentUser, subscription, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: threadID, threadInfo: newThreadInfo, }, }, ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === saveMessagesActionType) { const threadIDToMostRecentTime = new Map(); for (const messageInfo of action.payload.rawMessageInfos) { const current = threadIDToMostRecentTime.get(messageInfo.threadID); if (!current || current < messageInfo.time) { threadIDToMostRecentTime.set(messageInfo.threadID, messageInfo.time); } } const changedThreadInfos: { [string]: RawThreadInfo } = {}; for (const [threadID, mostRecentTime] of threadIDToMostRecentTime) { const threadInfo = state.threadInfos[threadID]; if ( !threadInfo || threadInfo.currentUser.unread || action.payload.updatesCurrentAsOf > mostRecentTime ) { continue; } changedThreadInfos[threadID] = { ...threadInfo, currentUser: { ...threadInfo.currentUser, unread: true, }, }; } if (Object.keys(changedThreadInfos).length !== 0) { const threadStoreOperations = Object.keys(changedThreadInfos).map(id => ({ type: 'replace', payload: { id, threadInfo: changedThreadInfos[id], }, })); const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } } else if (action.type === processServerRequestsActionType) { const checkStateRequest = action.payload.serverRequests.find( candidate => candidate.type === serverRequestTypes.CHECK_STATE, ); if (!checkStateRequest || !checkStateRequest.stateChanges) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const { rawThreadInfos, deleteThreadIDs } = checkStateRequest.stateChanges; if (!rawThreadInfos && !deleteThreadIDs) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const threadStoreOperations: ThreadStoreOperation[] = []; if (rawThreadInfos) { for (const rawThreadInfo of rawThreadInfos) { threadStoreOperations.push({ type: 'replace', payload: { id: rawThreadInfo.id, threadInfo: rawThreadInfo, }, }); } } if (deleteThreadIDs) { threadStoreOperations.push({ type: 'remove', payload: { ids: deleteThreadIDs, }, }); } const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); const newThreadInconsistencies = stateSyncSpecs.threads.findStoreInconsistencies( action, state.threadInfos, updatedThreadStore.threadInfos, ); return { threadStore: updatedThreadStore, newThreadInconsistencies, threadStoreOperations, }; } else if (action.type === updateActivityActionTypes.success) { const updatedThreadInfos: { [string]: RawThreadInfo } = {}; for (const setToUnread of action.payload.result.unfocusedToUnread) { const threadInfo = state.threadInfos[setToUnread]; if (threadInfo && !threadInfo.currentUser.unread) { updatedThreadInfos[setToUnread] = { ...threadInfo, currentUser: { ...threadInfo.currentUser, unread: true, }, }; } } if (Object.keys(updatedThreadInfos).length === 0) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const threadStoreOperations = Object.keys(updatedThreadInfos).map(id => ({ type: 'replace', payload: { id, threadInfo: updatedThreadInfos[id], }, })); const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === setThreadUnreadStatusActionTypes.started) { const { threadID, unread } = action.payload; const threadInfo = state.threadInfos[threadID]; const updatedThreadInfo = { ...threadInfo, currentUser: { ...threadInfo.currentUser, unread, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: threadID, threadInfo: updatedThreadInfo, }, }, ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === setThreadUnreadStatusActionTypes.success) { const { threadID, resetToUnread } = action.payload; const threadInfo = state.threadInfos[threadID]; const { currentUser } = threadInfo; if (!resetToUnread || currentUser.unread) { return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } const updatedThread = { ...threadInfo, currentUser: { ...currentUser, unread: true }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: threadID, threadInfo: updatedThread, }, }, ]; const updatedThreadStore = processThreadStoreOperations( state, threadStoreOperations, ); return { threadStore: updatedThreadStore, newThreadInconsistencies: [], threadStoreOperations, }; } else if (action.type === setClientDBStoreActionType) { return { threadStore: action.payload.threadStore ?? state, newThreadInconsistencies: [], threadStoreOperations: [], }; } return { threadStore: state, newThreadInconsistencies: [], threadStoreOperations: [], }; } export { reduceThreadInfos }; diff --git a/lib/reducers/user-reducer.js b/lib/reducers/user-reducer.js index 0374bcf59..a868d58dd 100644 --- a/lib/reducers/user-reducer.js +++ b/lib/reducers/user-reducer.js @@ -1,476 +1,448 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import _keyBy from 'lodash/fp/keyBy.js'; import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { joinThreadActionTypes, newThreadActionTypes, } from '../actions/thread-actions.js'; import { deleteAccountActionTypes, keyserverAuthActionTypes, logOutActionTypes, logInActionTypes, keyserverRegisterActionTypes, setUserSettingsActionTypes, updateUserAvatarActionTypes, resetUserStateActionType, } from '../actions/user-actions.js'; import { setNewSessionActionType } from '../keyserver-conn/keyserver-conn-types.js'; import { convertUserInfosToReplaceUserOps, type UserStoreOperation, userStoreOpsHandlers, } from '../ops/user-store-ops.js'; import { stateSyncSpecs } from '../shared/state-sync/state-sync-specs.js'; import { updateSpecs } from '../shared/updates/update-specs.js'; import type { BaseAction } from '../types/redux-types.js'; import type { ClientUserInconsistencyReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, processServerRequestsActionType, } from '../types/request-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; import { updateTypes } from '../types/update-types-enum.js'; import { processUpdatesActionType } from '../types/update-types.js'; import type { ClientUpdateInfo } from '../types/update-types.js'; import type { CurrentUserInfo, UserInfos, UserStore, } from '../types/user-types.js'; import { getMessageForException } from '../utils/errors.js'; import { assertObjectsAreEqual } from '../utils/objects.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; import { ashoatKeyserverID } from '../utils/validation-utils.js'; function reduceCurrentUserInfo( state: ?CurrentUserInfo, action: BaseAction, ): ?CurrentUserInfo { if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || action.type === keyserverRegisterActionTypes.success || action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success ) { if (!_isEqual(action.payload.currentUserInfo)(state)) { return action.payload.currentUserInfo; } } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && action.payload.keyserverID === ashoatKeyserverID ) { const { sessionChange } = action.payload; if (!_isEqual(sessionChange.currentUserInfo)(state)) { return sessionChange.currentUserInfo; } } else if (action.type === fullStateSyncActionType) { const { currentUserInfo } = action.payload; if (!_isEqual(currentUserInfo)(state)) { return currentUserInfo; } } else if ( action.type === incrementalStateSyncActionType || action.type === processUpdatesActionType ) { return action.payload.updatesResult.newUpdates.reduce( (reducedState, update) => { const { reduceCurrentUser } = updateSpecs[update.type]; return reduceCurrentUser ? reduceCurrentUser(reducedState, update) : reducedState; }, state, ); } else if (action.type === processServerRequestsActionType) { const checkStateRequest = action.payload.serverRequests.find( candidate => candidate.type === serverRequestTypes.CHECK_STATE, ); const newCurrentUserInfo = checkStateRequest?.stateChanges?.currentUserInfo; if (newCurrentUserInfo && !_isEqual(newCurrentUserInfo)(state)) { return newCurrentUserInfo; } } else if ( action.type === updateUserAvatarActionTypes.success && state && !state.anonymous ) { const { viewerUpdates } = action.payload.updates; for (const update of viewerUpdates) { if ( update.type === updateTypes.UPDATE_CURRENT_USER && !_isEqual(update.currentUserInfo.avatar)(state.avatar) ) { return { ...state, avatar: update.currentUserInfo.avatar, }; } } return state; } else if (action.type === setUserSettingsActionTypes.success) { if (state?.settings) { return { ...state, settings: { ...state.settings, ...action.payload, }, }; } } else if (action.type === resetUserStateActionType) { state = state && state.anonymous ? state : null; } return state; } function assertUserStoresAreEqual( processedUserStore: UserInfos, expectedUserStore: UserInfos, location: string, onStateDifference: (message: string) => mixed, ) { try { assertObjectsAreEqual( processedUserStore, expectedUserStore, `UserInfos - ${location}`, ); } catch (e) { console.log( 'Error processing UserStore ops', processedUserStore, expectedUserStore, ); const message = `Error processing UserStore ops ${ getMessageForException(e) ?? '{no exception message}' }`; onStateDifference(message); } } const { processStoreOperations: processUserStoreOps } = userStoreOpsHandlers; function generateOpsForUserUpdates(payload: { +updatesResult: { +newUpdates: $ReadOnlyArray, ... }, ... }): $ReadOnlyArray { return payload.updatesResult.newUpdates .map(update => updateSpecs[update.type].generateOpsForUserInfoUpdates?.(update), ) .filter(Boolean) .flat(); } function reduceUserInfos( state: UserStore, action: BaseAction, onStateDifference: (message: string) => mixed, ): [ UserStore, $ReadOnlyArray, $ReadOnlyArray, ] { if ( action.type === joinThreadActionTypes.success || action.type === newThreadActionTypes.success ) { const newUserInfos: UserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const userStoreOps: $ReadOnlyArray = convertUserInfosToReplaceUserOps(newUserInfos); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); const updated: UserInfos = { ...state.userInfos, ...newUserInfos }; assertUserStoresAreEqual( processedUserInfos, updated, action.type, onStateDifference, ); if (!_isEqual(state.userInfos)(updated)) { return [ { ...state, userInfos: updated, }, [], userStoreOps, ]; } - } else if ( - action.type === logOutActionTypes.success || - action.type === deleteAccountActionTypes.success - ) { - const userStoreOps: $ReadOnlyArray = [ - { type: 'remove_all_users' }, - ]; - const processedUserInfos: UserInfos = processUserStoreOps( - state.userInfos, - userStoreOps, - ); - assertUserStoresAreEqual( - processedUserInfos, - {}, - action.type, - onStateDifference, - ); - - if (Object.keys(state.userInfos).length === 0) { - return [state, [], []]; - } - return [ - { - userInfos: {}, - }, - [], - userStoreOps, - ]; } else if ( action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated && !usingCommServicesAccessToken ) { const userStoreOps: $ReadOnlyArray = [ { type: 'remove_all_users' }, ]; const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); assertUserStoresAreEqual( processedUserInfos, {}, action.type, onStateDifference, ); if (Object.keys(state.userInfos).length === 0) { return [state, [], []]; } return [ { userInfos: {}, }, [], userStoreOps, ]; } else if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || action.type === keyserverRegisterActionTypes.success || action.type === fullStateSyncActionType ) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const userStoreOps: $ReadOnlyArray = [ { type: 'remove_all_users' }, ...convertUserInfosToReplaceUserOps(newUserInfos), ]; const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); assertUserStoresAreEqual( processedUserInfos, newUserInfos, action.type, onStateDifference, ); if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success || action.type === keyserverRegisterActionTypes.success || !_isEqual(state.userInfos)(newUserInfos) ) { return [ { userInfos: newUserInfos, }, [], userStoreOps, ]; } } else if (action.type === keyserverAuthActionTypes.success) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const mergedUserInfos: UserInfos = { ...state.userInfos, ...newUserInfos }; const userStoreOps: $ReadOnlyArray = convertUserInfosToReplaceUserOps(newUserInfos); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); assertUserStoresAreEqual( processedUserInfos, mergedUserInfos, action.type, onStateDifference, ); if (!_isEqual(state.userInfos)(mergedUserInfos)) { return [ { ...state, userInfos: mergedUserInfos, }, [], userStoreOps, ]; } } else if ( action.type === incrementalStateSyncActionType || action.type === processUpdatesActionType ) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.userInfos, ); const userStoreOps: $ReadOnlyArray = [ ...convertUserInfosToReplaceUserOps(newUserInfos), ...generateOpsForUserUpdates(action.payload), ]; const updated = action.payload.updatesResult.newUpdates.reduce( (reducedState, update) => { const { reduceUserInfos: reduceUserInfosUpdate } = updateSpecs[update.type]; return reduceUserInfosUpdate ? reduceUserInfosUpdate(reducedState, update) : reducedState; }, { ...state.userInfos, ...newUserInfos }, ); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); assertUserStoresAreEqual( processedUserInfos, updated, action.type, onStateDifference, ); if (!_isEqual(state.userInfos)(updated)) { return [ { ...state, userInfos: updated, }, [], userStoreOps, ]; } } else if (action.type === processServerRequestsActionType) { const checkStateRequest = action.payload.serverRequests.find( candidate => candidate.type === serverRequestTypes.CHECK_STATE, ); if (!checkStateRequest || !checkStateRequest.stateChanges) { return [state, [], []]; } const { userInfos, deleteUserInfoIDs } = checkStateRequest.stateChanges; if (!userInfos && !deleteUserInfoIDs) { return [state, [], []]; } const userStoreOps: UserStoreOperation[] = []; const newUserInfos = { ...state.userInfos }; if (userInfos) { for (const userInfo of userInfos) { newUserInfos[userInfo.id] = userInfo; userStoreOps.push({ type: 'replace_user', payload: { ...userInfo } }); } } if (deleteUserInfoIDs) { for (const deleteUserInfoID of deleteUserInfoIDs) { delete newUserInfos[deleteUserInfoID]; } userStoreOps.push({ type: 'remove_users', payload: { ids: deleteUserInfoIDs }, }); } const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); assertUserStoresAreEqual( processedUserInfos, newUserInfos, action.type, onStateDifference, ); const newInconsistencies = stateSyncSpecs.users.findStoreInconsistencies( action, state.userInfos, newUserInfos, ); return [ { userInfos: newUserInfos, }, newInconsistencies, userStoreOps, ]; } else if (action.type === updateUserAvatarActionTypes.success) { const newUserInfos = _keyBy(userInfo => userInfo.id)( action.payload.updates.userInfos, ); const userStoreOps: $ReadOnlyArray = convertUserInfosToReplaceUserOps(newUserInfos); const processedUserInfos: UserInfos = processUserStoreOps( state.userInfos, userStoreOps, ); const updated: UserInfos = { ...state.userInfos, ...newUserInfos }; assertUserStoresAreEqual( processedUserInfos, updated, action.type, onStateDifference, ); const newState = !_isEqual(state.userInfos)(updated) ? { ...state, userInfos: updated, } : state; return [newState, [], userStoreOps]; } else if (action.type === setClientDBStoreActionType) { if (!action.payload.users) { return [state, [], []]; } // Once the functionality is confirmed to work correctly, // we will proceed with returning the users from the payload. assertUserStoresAreEqual( action.payload.users ?? {}, state.userInfos, action.type, onStateDifference, ); return [state, [], []]; } return [state, [], []]; } export { reduceCurrentUserInfo, reduceUserInfos };