diff --git a/lib/reducers/master-reducer.js b/lib/reducers/master-reducer.js --- a/lib/reducers/master-reducer.js +++ b/lib/reducers/master-reducer.js @@ -18,6 +18,7 @@ import policiesReducer from './policies-reducer.js'; import reduceReportStore from './report-store-reducer.js'; import reduceServicesAccessToken from './services-access-token-reducer.js'; +import { reduceThreadActivity } from './thread-activity-reducer.js'; import { reduceThreadInfos } from './thread-reducer.js'; import { reduceCurrentUserInfo, reduceUserInfos } from './user-reducer.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; @@ -145,6 +146,10 @@ ), inviteLinksStore: reduceInviteLinks(state.inviteLinksStore, action), keyserverStore, + threadActivityStore: reduceThreadActivity( + state.threadActivityStore, + action, + ), }, storeOperations: { draftStoreOperations, diff --git a/lib/reducers/message-reducer.js b/lib/reducers/message-reducer.js --- a/lib/reducers/message-reducer.js +++ b/lib/reducers/message-reducer.js @@ -92,6 +92,7 @@ fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; +import { updateThreadLastNavigatedActionType } from '../types/thread-activity-types.js'; import { threadPermissions } from '../types/thread-permission-types.js'; import { type RawThreadInfo } from '../types/thread-types.js'; import { updateTypes } from '../types/update-types-enum.js'; @@ -1738,6 +1739,32 @@ messageStoreOperations, messageStore: processedMessageStore, }; + } else if (action.type === updateThreadLastNavigatedActionType) { + const { threadID, time } = action.payload; + if (!messageStore.threads[threadID]) { + return { messageStoreOperations: [], messageStore }; + } + + const updatedThreads = { + [threadID]: { + ...messageStore.threads[threadID], + lastNavigatedTo: time, + }, + }; + + const messageStoreOperations = [ + { + type: 'replace_threads', + payload: { threads: updatedThreads }, + }, + ]; + + const processedMessageStore = processMessageStoreOperations( + messageStore, + messageStoreOperations, + ); + + return { messageStoreOperations, messageStore: processedMessageStore }; } return { messageStoreOperations: [], messageStore }; } diff --git a/lib/reducers/thread-activity-reducer.js b/lib/reducers/thread-activity-reducer.js new file mode 100644 --- /dev/null +++ b/lib/reducers/thread-activity-reducer.js @@ -0,0 +1,43 @@ +// @flow + +import { + leaveThreadActionTypes, + deleteThreadActionTypes, +} from '../actions/thread-actions.js'; +import { + logOutActionTypes, + deleteAccountActionTypes, +} from '../actions/user-actions.js'; +import type { BaseAction } from '../types/redux-types.js'; +import type { ThreadActivityStore } from '../types/thread-activity-types.js'; +import { updateThreadLastNavigatedActionType } from '../types/thread-activity-types.js'; +import { setNewSessionActionType } from '../utils/action-utils.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 === logOutActionTypes.success || + action.type === deleteAccountActionTypes.success || + action.type === deleteThreadActionTypes.success || + action.type === leaveThreadActionTypes.success || + (action.type === setNewSessionActionType && + action.payload.sessionChange.cookieInvalidated) + ) { + return {}; + } + return state; +} + +export { reduceThreadActivity }; diff --git a/lib/reducers/thread-activity-reducer.test.js b/lib/reducers/thread-activity-reducer.test.js new file mode 100644 --- /dev/null +++ b/lib/reducers/thread-activity-reducer.test.js @@ -0,0 +1,69 @@ +// @flow + +import { reduceThreadActivity } from './thread-activity-reducer.js'; +import { updateThreadLastNavigatedActionType } from '../types/thread-activity-types.js'; + +// NOTE: These unit tests were generated by GitHub Copilot. + +describe('reduceThreadActivity', () => { + test('updates the lastNavigatedTo time for a thread', () => { + const initialState = { + thread1: { + lastNavigatedTo: 1639522314170, + lastPruned: 1639522314170, + }, + thread2: { + lastNavigatedTo: 1639522314170, + lastPruned: 1639522314170, + }, + }; + const action = { + type: updateThreadLastNavigatedActionType, + payload: { + threadID: 'thread1', + time: 1639522317443, + }, + }; + const expectedState = { + thread1: { + lastNavigatedTo: 1639522317443, + lastPruned: 1639522314170, + }, + thread2: { + lastNavigatedTo: 1639522314170, + lastPruned: 1639522314170, + }, + }; + const result = reduceThreadActivity(initialState, action); + expect(result).toEqual(expectedState); + }); + + test('returns the initial state if the action type is not recognized', () => { + const initialState = { + thread1: { + lastNavigatedTo: 1639522314170, + lastPruned: 1639522314170, + }, + thread2: { + lastNavigatedTo: 1639522314170, + lastPruned: 1639522314170, + }, + }; + const action = { + type: 'UPDATE_REPORTS_ENABLED', + payload: {}, + }; + const expectedState = { + thread1: { + lastNavigatedTo: 1639522314170, + lastPruned: 1639522314170, + }, + thread2: { + lastNavigatedTo: 1639522314170, + lastPruned: 1639522314170, + }, + }; + const result = reduceThreadActivity(initialState, action); + expect(result).toEqual(expectedState); + }); +}); diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -99,6 +99,7 @@ UpdateDisconnectedBarPayload, } from './socket-types.js'; import type { SubscriptionUpdateResult } from './subscription-types.js'; +import type { ThreadActivityStore } from './thread-activity-types.js'; import type { ThreadStore, ChangeThreadSettingsPayload, @@ -137,6 +138,7 @@ +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, + +threadActivityStore: ThreadActivityStore, ... }; @@ -1211,6 +1213,10 @@ | { +type: 'SET_ACCESS_TOKEN', +payload: string, + } + | { + +type: 'UPDATE_THREAD_LAST_NAVIGATED', + +payload: { +threadID: string, +time: number }, }; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); diff --git a/lib/types/thread-activity-types.js b/lib/types/thread-activity-types.js new file mode 100644 --- /dev/null +++ b/lib/types/thread-activity-types.js @@ -0,0 +1,12 @@ +// @flow + +export const updateThreadLastNavigatedActionType = + 'UPDATE_THREAD_LAST_NAVIGATED'; + +export type ThreadActivityStoreEntry = { + +lastNavigatedTo: number, // millisecond timestamp + +lastPruned: number, // millisecond timestamp +}; +export type ThreadActivityStore = { + +[threadID: string]: ThreadActivityStoreEntry, +}; diff --git a/native/navigation/thread-screen-tracker.react.js b/native/navigation/thread-screen-tracker.react.js --- a/native/navigation/thread-screen-tracker.react.js +++ b/native/navigation/thread-screen-tracker.react.js @@ -3,8 +3,9 @@ import * as React from 'react'; import { useDispatch } from 'react-redux'; +import { updateThreadLastNavigatedActionType } from 'lib/types/thread-activity-types.js'; + import { useActiveMessageList } from './nav-selectors.js'; -import { updateThreadLastNavigatedActionType } from '../redux/action-types.js'; const ThreadScreenTracker: React.ComponentType<{}> = React.memo<{}>( function ThreadScreenTracker() { diff --git a/native/redux/action-types.js b/native/redux/action-types.js --- a/native/redux/action-types.js +++ b/native/redux/action-types.js @@ -18,8 +18,6 @@ export const updateThemeInfoActionType = 'UPDATE_THEME_INFO'; export const updateDeviceCameraInfoActionType = 'UPDATE_DEVICE_CAMERA_INFO'; export const updateDeviceOrientationActionType = 'UPDATE_DEVICE_ORIENTATION'; -export const updateThreadLastNavigatedActionType = - 'UPDATE_THREAD_LAST_NAVIGATED'; export const setStoreLoadedActionType = 'SET_STORE_LOADED'; export const setReduxStateActionType = 'SET_REDUX_STATE'; export const setLocalSettingsActionType = 'SET_LOCAL_SETTINGS'; diff --git a/native/redux/default-state.js b/native/redux/default-state.js --- a/native/redux/default-state.js +++ b/native/redux/default-state.js @@ -85,6 +85,7 @@ localSettings: { isBackupEnabled: false, }, + threadActivityStore: {}, }: AppState); export { defaultState }; diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -32,7 +32,6 @@ updateThemeInfoActionType, updateDeviceCameraInfoActionType, updateDeviceOrientationActionType, - updateThreadLastNavigatedActionType, backgroundActionTypes, setReduxStateActionType, setStoreLoadedActionType, @@ -183,42 +182,6 @@ ...state, deviceOrientation: action.payload, }; - } else if (action.type === updateThreadLastNavigatedActionType) { - const { threadID, time } = action.payload; - if (state.messageStore.threads[threadID]) { - const updatedThreads = { - [threadID]: { - ...state.messageStore.threads[threadID], - lastNavigatedTo: time, - }, - }; - - state = { - ...state, - messageStore: { - ...state.messageStore, - threads: { - ...state.messageStore.threads, - ...updatedThreads, - }, - }, - }; - - processDBStoreOperations({ - draftStoreOperations: [], - messageStoreOperations: [ - { - type: 'replace_threads', - payload: { - threads: updatedThreads, - }, - }, - ], - threadStoreOperations: [], - reportStoreOperations: [], - }); - } - return state; } else if (action.type === setLocalSettingsActionType) { return { ...state, diff --git a/native/redux/state-types.js b/native/redux/state-types.js --- a/native/redux/state-types.js +++ b/native/redux/state-types.js @@ -14,6 +14,7 @@ import type { MessageStore } from 'lib/types/message-types.js'; import type { UserPolicies } from 'lib/types/policy-types.js'; import type { ReportStore } from 'lib/types/report-types.js'; +import type { ThreadActivityStore } from 'lib/types/thread-activity-types'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { CurrentUserInfo, UserStore } from 'lib/types/user-types.js'; import type { NotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; @@ -57,5 +58,6 @@ +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, + +threadActivityStore: ThreadActivityStore, +localSettings: LocalSettings, }; diff --git a/web/redux/default-state.js b/web/redux/default-state.js --- a/web/redux/default-state.js +++ b/web/redux/default-state.js @@ -87,6 +87,7 @@ }, }, }, + threadActivityStore: {}, initialStateLoaded: false, }); diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js --- a/web/redux/redux-setup.js +++ b/web/redux/redux-setup.js @@ -30,6 +30,7 @@ import type { UserPolicies } from 'lib/types/policy-types.js'; import type { BaseAction } from 'lib/types/redux-types.js'; import type { ReportStore } from 'lib/types/report-types.js'; +import type { ThreadActivityStore } from 'lib/types/thread-activity-types'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { CurrentUserInfo, UserStore } from 'lib/types/user-types.js'; import { setNewSessionActionType } from 'lib/utils/action-utils.js'; @@ -97,6 +98,7 @@ +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, + +threadActivityStore: ThreadActivityStore, +initialStateLoaded: boolean, };