{
const appURLFacts = getAppURLFactsFromRequestURL(req.originalUrl);
const { basePath, baseDomain } = appURLFacts;
const baseURL = basePath.replace(/\/$/, '');
const baseHref = baseDomain + baseURL;
const loadingPromise = getWebpackCompiledRootComponentForSSR();
const hasNotAcknowledgedPoliciesPromise = hasAnyNotAcknowledgedPolicies(
viewer.id,
baseLegalPolicies,
);
let initialNavInfo;
try {
initialNavInfo = navInfoFromURL(req.url, {
now: currentDateInTimeZone(viewer.timeZone),
});
} catch (e) {
throw new ServerError(e.message);
}
const calendarQuery = {
startDate: initialNavInfo.startDate,
endDate: initialNavInfo.endDate,
filters: defaultCalendarFilters,
};
const messageSelectionCriteria = { joinedThreads: true };
const initialTime = Date.now();
const assetInfoPromise = getAssetInfo();
const threadInfoPromise = fetchThreadInfos(viewer);
const messageInfoPromise = fetchMessageInfos(
viewer,
messageSelectionCriteria,
defaultNumberPerThread,
);
const entryInfoPromise = fetchEntryInfos(viewer, [calendarQuery]);
const currentUserInfoPromise = fetchCurrentUserInfo(viewer);
const userInfoPromise = fetchKnownUserInfos(viewer);
const sessionIDPromise = (async () => {
if (viewer.loggedIn) {
await setNewSession(viewer, calendarQuery, initialTime);
}
return viewer.sessionID;
})();
const threadStorePromise = (async () => {
const [{ threadInfos }, hasNotAcknowledgedPolicies] = await Promise.all([
threadInfoPromise,
hasNotAcknowledgedPoliciesPromise,
]);
return { threadInfos: hasNotAcknowledgedPolicies ? {} : threadInfos };
})();
const messageStorePromise = (async () => {
const [
{ threadInfos },
{ rawMessageInfos, truncationStatuses },
hasNotAcknowledgedPolicies,
] = await Promise.all([
threadInfoPromise,
messageInfoPromise,
hasNotAcknowledgedPoliciesPromise,
]);
if (hasNotAcknowledgedPolicies) {
return {
messages: {},
threads: {},
local: {},
currentAsOf: 0,
};
}
const { messageStore: freshStore } = freshMessageStore(
rawMessageInfos,
truncationStatuses,
mostRecentMessageTimestamp(rawMessageInfos, initialTime),
threadInfos,
);
return freshStore;
})();
const entryStorePromise = (async () => {
const [{ rawEntryInfos }, hasNotAcknowledgedPolicies] = await Promise.all([
entryInfoPromise,
hasNotAcknowledgedPoliciesPromise,
]);
if (hasNotAcknowledgedPolicies) {
return {
entryInfos: {},
daysToEntries: {},
lastUserInteractionCalendar: 0,
};
}
return {
entryInfos: _keyBy('id')(rawEntryInfos),
daysToEntries: daysToEntriesFromEntryInfos(rawEntryInfos),
lastUserInteractionCalendar: initialTime,
};
})();
const userStorePromise = (async () => {
const [userInfos, hasNotAcknowledgedPolicies] = await Promise.all([
userInfoPromise,
hasNotAcknowledgedPoliciesPromise,
]);
return {
userInfos: hasNotAcknowledgedPolicies ? {} : userInfos,
inconsistencyReports: [],
};
})();
const navInfoPromise = (async () => {
const [{ threadInfos }, messageStore, currentUserInfo, userStore] =
await Promise.all([
threadInfoPromise,
messageStorePromise,
currentUserInfoPromise,
userStorePromise,
]);
const finalNavInfo = initialNavInfo;
const requestedActiveChatThreadID = finalNavInfo.activeChatThreadID;
if (
requestedActiveChatThreadID &&
!threadIsPending(requestedActiveChatThreadID) &&
!threadHasPermission(
threadInfos[requestedActiveChatThreadID],
threadPermissions.VISIBLE,
)
) {
finalNavInfo.activeChatThreadID = null;
}
if (!finalNavInfo.activeChatThreadID) {
const mostRecentThread = mostRecentlyReadThread(
messageStore,
threadInfos,
);
if (mostRecentThread) {
finalNavInfo.activeChatThreadID = mostRecentThread;
}
}
if (
finalNavInfo.activeChatThreadID &&
threadIsPending(finalNavInfo.activeChatThreadID) &&
finalNavInfo.pendingThread?.id !== finalNavInfo.activeChatThreadID
) {
const pendingThreadData = parsePendingThreadID(
finalNavInfo.activeChatThreadID,
);
if (
pendingThreadData &&
pendingThreadData.threadType !== threadTypes.SIDEBAR &&
currentUserInfo.id
) {
const { userInfos } = userStore;
const members = [...pendingThreadData.memberIDs, currentUserInfo.id]
.map(id => {
const userInfo = userInfos[id];
if (!userInfo || !userInfo.username) {
return undefined;
}
const { username } = userInfo;
return { id, username };
})
.filter(Boolean);
const newPendingThread = createPendingThread({
viewerID: currentUserInfo.id,
threadType: pendingThreadData.threadType,
members,
});
finalNavInfo.activeChatThreadID = newPendingThread.id;
finalNavInfo.pendingThread = newPendingThread;
}
}
return finalNavInfo;
})();
const currentAsOfPromise = (async () => {
const hasNotAcknowledgedPolicies = await hasNotAcknowledgedPoliciesPromise;
return hasNotAcknowledgedPolicies ? 0 : initialTime;
})();
const pushApiPublicKeyPromise = (async () => {
const pushConfig = await getWebPushConfig();
if (!pushConfig) {
if (process.env.NODE_ENV !== 'development') {
console.warn('keyserver/secrets/web_push_config.json should exist');
}
return null;
}
return pushConfig.publicKey;
})();
const { jsURL, fontsURL, cssInclude, olmFilename } = await assetInfoPromise;
// prettier-ignore
res.write(html`
${getTitle(0)}
${cssInclude}
`);
const Loading = await loadingPromise;
const reactStream = renderToNodeStream();
reactStream.pipe(res, { end: false });
await waitForStream(reactStream);
res.write(html`
`);
}
export { websiteResponder };
diff --git a/lib/reducers/master-reducer.js b/lib/reducers/master-reducer.js
index 7930eb17e..ed57ac714 100644
--- a/lib/reducers/master-reducer.js
+++ b/lib/reducers/master-reducer.js
@@ -1,119 +1,124 @@
// @flow
import reduceCalendarFilters from './calendar-filters-reducer.js';
import reduceConnectionInfo from './connection-reducer.js';
import reduceDataLoaded from './data-loaded-reducer.js';
import { reduceDeviceToken } from './device-token-reducer.js';
import { reduceDraftStore } from './draft-reducer.js';
import reduceEnabledApps from './enabled-apps-reducer.js';
import { reduceEntryInfos } from './entry-reducer.js';
import reduceLifecycleState from './lifecycle-state-reducer.js';
import { reduceLoadingStatuses } from './loading-reducer.js';
import reduceNextLocalID from './local-id-reducer.js';
import { reduceMessageStore } from './message-reducer.js';
import reduceBaseNavInfo from './nav-reducer.js';
+import { reduceNotifPermissionAlertInfo } from './notif-permission-alert-info-reducer.js';
import policiesReducer from './policies-reducer.js';
import reduceReportStore from './report-store-reducer.js';
import { reduceThreadInfos } from './thread-reducer.js';
import reduceUpdatesCurrentAsOf from './updates-reducer.js';
import reduceURLPrefix from './url-prefix-reducer.js';
import { reduceCurrentUserInfo, reduceUserInfos } from './user-reducer.js';
import { siweAuthActionTypes } from '../actions/siwe-actions.js';
import {
registerActionTypes,
logInActionTypes,
} from '../actions/user-actions.js';
import type { BaseNavInfo } from '../types/nav-types.js';
import type { BaseAppState, BaseAction } from '../types/redux-types.js';
import {
fullStateSyncActionType,
incrementalStateSyncActionType,
} from '../types/socket-types.js';
import type { StoreOperations } from '../types/store-ops-types.js';
export default function baseReducer>(
state: T,
action: BaseAction,
): { state: T, storeOperations: StoreOperations } {
const { threadStore, newThreadInconsistencies, threadStoreOperations } =
reduceThreadInfos(state.threadStore, action);
const { threadInfos } = threadStore;
const [entryStore, newEntryInconsistencies] = reduceEntryInfos(
state.entryStore,
action,
threadInfos,
);
const newInconsistencies = [
...newEntryInconsistencies,
...newThreadInconsistencies,
];
// Only allow checkpoints to increase if we are connected
// or if the action is a STATE_SYNC
const { messageStoreOperations, messageStore: reducedMessageStore } =
reduceMessageStore(state.messageStore, action, threadInfos);
let messageStore = reducedMessageStore;
let updatesCurrentAsOf = reduceUpdatesCurrentAsOf(
state.updatesCurrentAsOf,
action,
);
const connection = reduceConnectionInfo(state.connection, action);
if (
connection.status !== 'connected' &&
action.type !== incrementalStateSyncActionType &&
action.type !== fullStateSyncActionType &&
action.type !== registerActionTypes.success &&
action.type !== logInActionTypes.success &&
action.type !== siweAuthActionTypes.success
) {
if (messageStore.currentAsOf !== state.messageStore.currentAsOf) {
messageStore = {
...messageStore,
currentAsOf: state.messageStore.currentAsOf,
};
}
if (updatesCurrentAsOf !== state.updatesCurrentAsOf) {
updatesCurrentAsOf = state.updatesCurrentAsOf;
}
}
const { draftStore, draftStoreOperations } = reduceDraftStore(
state.draftStore,
action,
);
return {
state: {
...state,
navInfo: reduceBaseNavInfo(state.navInfo, action),
draftStore,
entryStore,
loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action),
currentUserInfo: reduceCurrentUserInfo(state.currentUserInfo, action),
threadStore,
userStore: reduceUserInfos(state.userStore, action),
messageStore,
updatesCurrentAsOf,
urlPrefix: reduceURLPrefix(state.urlPrefix, action),
calendarFilters: reduceCalendarFilters(state.calendarFilters, action),
+ notifPermissionAlertInfo: reduceNotifPermissionAlertInfo(
+ state.notifPermissionAlertInfo,
+ action,
+ ),
connection,
lifecycleState: reduceLifecycleState(state.lifecycleState, action),
enabledApps: reduceEnabledApps(state.enabledApps, action),
reportStore: reduceReportStore(
state.reportStore,
action,
newInconsistencies,
),
nextLocalID: reduceNextLocalID(state.nextLocalID, action),
dataLoaded: reduceDataLoaded(state.dataLoaded, action),
userPolicies: policiesReducer(state.userPolicies, action),
deviceToken: reduceDeviceToken(state.deviceToken, action),
},
storeOperations: {
draftStoreOperations,
threadStoreOperations,
messageStoreOperations,
},
};
}
diff --git a/lib/reducers/notif-permission-alert-info-reducer.js b/lib/reducers/notif-permission-alert-info-reducer.js
new file mode 100644
index 000000000..8e1dfa5cd
--- /dev/null
+++ b/lib/reducers/notif-permission-alert-info-reducer.js
@@ -0,0 +1,22 @@
+// @flow
+
+import type { BaseAction } from '../types/redux-types';
+import {
+ type NotifPermissionAlertInfo,
+ recordNotifPermissionAlertActionType,
+} from '../utils/push-alerts.js';
+
+function reduceNotifPermissionAlertInfo(
+ state: NotifPermissionAlertInfo,
+ action: BaseAction,
+): NotifPermissionAlertInfo {
+ if (action.type === recordNotifPermissionAlertActionType) {
+ return {
+ totalAlerts: state.totalAlerts + 1,
+ lastAlertTime: action.payload.time,
+ };
+ }
+ return state;
+}
+
+export { reduceNotifPermissionAlertInfo };
diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js
index 1a8fe1b90..313e75d1d 100644
--- a/lib/types/redux-types.js
+++ b/lib/types/redux-types.js
@@ -1,932 +1,938 @@
// @flow
import type {
LogOutResult,
LogInStartingPayload,
LogInResult,
RegisterResult,
DefaultNotificationPayload,
} from './account-types.js';
import type {
ActivityUpdateSuccessPayload,
QueueActivityUpdatesPayload,
SetThreadUnreadStatusPayload,
} from './activity-types.js';
import type { CryptoStore } from './crypto-types.js';
import type { ClientDBDraftInfo, DraftStore } from './draft-types.js';
import type { EnabledApps, SupportedApps } from './enabled-apps.js';
import type {
RawEntryInfo,
EntryStore,
SaveEntryPayload,
CreateEntryPayload,
DeleteEntryResult,
RestoreEntryPayload,
FetchEntryInfosResult,
CalendarQueryUpdateResult,
CalendarQueryUpdateStartingPayload,
} from './entry-types.js';
import type {
CalendarFilter,
CalendarThreadFilter,
SetCalendarDeletedFilterPayload,
} from './filter-types.js';
import type { LifecycleState } from './lifecycle-state-types.js';
import type { LoadingStatus, LoadingInfo } from './loading-types.js';
import type { UpdateMultimediaMessageMediaPayload } from './media-types.js';
import type { MessageReportCreationResult } from './message-report-types.js';
import type {
MessageStore,
RawMultimediaMessageInfo,
FetchMessageInfosPayload,
SendMessagePayload,
SaveMessagesPayload,
NewMessagesPayload,
MessageStorePrunePayload,
LocallyComposedMessageInfo,
ClientDBMessageInfo,
SimpleMessagesPayload,
} from './message-types.js';
import type { RawReactionMessageInfo } from './messages/reaction.js';
import type { RawTextMessageInfo } from './messages/text.js';
import type { BaseNavInfo } from './nav-types.js';
import {
type ForcePolicyAcknowledgmentPayload,
type PolicyAcknowledgmentPayload,
type UserPolicies,
} from './policy-types.js';
import type { RelationshipErrors } from './relationship-types.js';
import type {
EnabledReports,
ClearDeliveredReportsPayload,
QueueReportsPayload,
ReportStore,
} from './report-types.js';
import type { ProcessServerRequestsPayload } from './request-types.js';
import type { UserSearchResult } from './search-types.js';
import type { SetSessionPayload } from './session-types.js';
import type {
ConnectionInfo,
StateSyncFullActionPayload,
StateSyncIncrementalActionPayload,
UpdateConnectionStatusPayload,
SetLateResponsePayload,
UpdateDisconnectedBarPayload,
} from './socket-types.js';
import type { SubscriptionUpdateResult } from './subscription-types.js';
import type {
ThreadStore,
ChangeThreadSettingsPayload,
LeaveThreadPayload,
NewThreadResult,
ThreadJoinPayload,
} from './thread-types.js';
import type { ClientUpdatesResultWithUserInfos } from './update-types.js';
import type { CurrentUserInfo, UserStore } from './user-types.js';
import type { Shape } from '../types/core.js';
+import type { NotifPermissionAlertInfo } from '../utils/push-alerts.js';
export type BaseAppState = {
navInfo: NavInfo,
currentUserInfo: ?CurrentUserInfo,
draftStore: DraftStore,
entryStore: EntryStore,
threadStore: ThreadStore,
userStore: UserStore,
messageStore: MessageStore,
updatesCurrentAsOf: number, // millisecond timestamp
loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } },
calendarFilters: $ReadOnlyArray,
urlPrefix: string,
+ notifPermissionAlertInfo: NotifPermissionAlertInfo,
connection: ConnectionInfo,
watchedThreadIDs: $ReadOnlyArray,
lifecycleState: LifecycleState,
enabledApps: EnabledApps,
reportStore: ReportStore,
nextLocalID: number,
dataLoaded: boolean,
userPolicies: UserPolicies,
deviceToken: ?string,
...
};
// Web JS runtime doesn't have access to the cookie for security reasons.
// Native JS doesn't have a sessionID because the cookieID is used instead.
export type NativeAppState = BaseAppState<*> & {
sessionID?: void,
cookie: ?string,
...
};
export type WebAppState = BaseAppState<*> & {
sessionID: ?string,
cookie?: void,
cryptoStore: CryptoStore,
pushApiPublicKey: ?string,
...
};
export type AppState = NativeAppState | WebAppState;
export type BaseAction =
| {
+type: '@@redux/INIT',
+payload?: void,
}
| {
+type: 'FETCH_ENTRIES_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_ENTRIES_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_ENTRIES_SUCCESS',
+payload: FetchEntryInfosResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'LOG_OUT_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'LOG_OUT_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'LOG_OUT_SUCCESS',
+payload: LogOutResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'DELETE_ACCOUNT_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'DELETE_ACCOUNT_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'DELETE_ACCOUNT_SUCCESS',
+payload: LogOutResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CREATE_LOCAL_ENTRY',
+payload: RawEntryInfo,
}
| {
+type: 'CREATE_ENTRY_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CREATE_ENTRY_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CREATE_ENTRY_SUCCESS',
+payload: CreateEntryPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SAVE_ENTRY_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SAVE_ENTRY_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SAVE_ENTRY_SUCCESS',
+payload: SaveEntryPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CONCURRENT_MODIFICATION_RESET',
+payload: {
+id: string,
+dbText: string,
},
}
| {
+type: 'DELETE_ENTRY_STARTED',
+loadingInfo: LoadingInfo,
+payload: {
+localID: ?string,
+serverID: ?string,
},
}
| {
+type: 'DELETE_ENTRY_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'DELETE_ENTRY_SUCCESS',
+payload: ?DeleteEntryResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'LOG_IN_STARTED',
+loadingInfo: LoadingInfo,
+payload: LogInStartingPayload,
}
| {
+type: 'LOG_IN_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'LOG_IN_SUCCESS',
+payload: LogInResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'REGISTER_STARTED',
+loadingInfo: LoadingInfo,
+payload: LogInStartingPayload,
}
| {
+type: 'REGISTER_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'REGISTER_SUCCESS',
+payload: RegisterResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CHANGE_USER_PASSWORD_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CHANGE_USER_PASSWORD_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CHANGE_USER_PASSWORD_SUCCESS',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CHANGE_THREAD_SETTINGS_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CHANGE_THREAD_SETTINGS_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CHANGE_THREAD_SETTINGS_SUCCESS',
+payload: ChangeThreadSettingsPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'DELETE_THREAD_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'DELETE_THREAD_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'DELETE_THREAD_SUCCESS',
+payload: LeaveThreadPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'NEW_THREAD_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'NEW_THREAD_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'NEW_THREAD_SUCCESS',
+payload: NewThreadResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'REMOVE_USERS_FROM_THREAD_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'REMOVE_USERS_FROM_THREAD_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'REMOVE_USERS_FROM_THREAD_SUCCESS',
+payload: ChangeThreadSettingsPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CHANGE_THREAD_MEMBER_ROLES_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CHANGE_THREAD_MEMBER_ROLES_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'CHANGE_THREAD_MEMBER_ROLES_SUCCESS',
+payload: ChangeThreadSettingsPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_REVISIONS_FOR_ENTRY_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_REVISIONS_FOR_ENTRY_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_REVISIONS_FOR_ENTRY_SUCCESS',
+payload: {
+entryID: string,
+text: string,
+deleted: boolean,
},
+loadingInfo: LoadingInfo,
}
| {
+type: 'RESTORE_ENTRY_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'RESTORE_ENTRY_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'RESTORE_ENTRY_SUCCESS',
+payload: RestoreEntryPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'JOIN_THREAD_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'JOIN_THREAD_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'JOIN_THREAD_SUCCESS',
+payload: ThreadJoinPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'LEAVE_THREAD_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'LEAVE_THREAD_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'LEAVE_THREAD_SUCCESS',
+payload: LeaveThreadPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SET_NEW_SESSION',
+payload: SetSessionPayload,
}
| {
+type: 'persist/REHYDRATE',
+payload: ?BaseAppState<*>,
}
| {
+type: 'FETCH_MESSAGES_BEFORE_CURSOR_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_MESSAGES_BEFORE_CURSOR_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_MESSAGES_BEFORE_CURSOR_SUCCESS',
+payload: FetchMessageInfosPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_MOST_RECENT_MESSAGES_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_MOST_RECENT_MESSAGES_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_MOST_RECENT_MESSAGES_SUCCESS',
+payload: FetchMessageInfosPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_SUCCESS',
+payload: SimpleMessagesPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_TEXT_MESSAGE_STARTED',
+loadingInfo?: LoadingInfo,
+payload: RawTextMessageInfo,
}
| {
+type: 'SEND_TEXT_MESSAGE_FAILED',
+error: true,
+payload: Error & {
+localID: string,
+threadID: string,
},
+loadingInfo?: LoadingInfo,
}
| {
+type: 'SEND_TEXT_MESSAGE_SUCCESS',
+payload: SendMessagePayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_MULTIMEDIA_MESSAGE_STARTED',
+loadingInfo?: LoadingInfo,
+payload: RawMultimediaMessageInfo,
}
| {
+type: 'SEND_MULTIMEDIA_MESSAGE_FAILED',
+error: true,
+payload: Error & {
+localID: string,
+threadID: string,
},
+loadingInfo?: LoadingInfo,
}
| {
+type: 'SEND_MULTIMEDIA_MESSAGE_SUCCESS',
+payload: SendMessagePayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_REACTION_MESSAGE_STARTED',
+loadingInfo?: LoadingInfo,
+payload: RawReactionMessageInfo,
}
| {
+type: 'SEND_REACTION_MESSAGE_FAILED',
+error: true,
+payload: Error & {
+localID: string,
+threadID: string,
+targetMessageID: string,
+reaction: string,
+action: string,
},
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_REACTION_MESSAGE_SUCCESS',
+payload: SendMessagePayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEARCH_USERS_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEARCH_USERS_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEARCH_USERS_SUCCESS',
+payload: UserSearchResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_DRAFT',
+payload: {
+key: string,
+text: string,
},
}
| {
+type: 'MOVE_DRAFT',
+payload: {
+oldKey: string,
+newKey: string,
},
}
| {
+type: 'SET_CLIENT_DB_STORE',
+payload: {
+currentUserID: ?string,
+drafts: $ReadOnlyArray,
+messages: $ReadOnlyArray,
+threadStore: ThreadStore,
},
}
| {
+type: 'UPDATE_ACTIVITY_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_ACTIVITY_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_ACTIVITY_SUCCESS',
+payload: ActivityUpdateSuccessPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SET_DEVICE_TOKEN_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SET_DEVICE_TOKEN_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SET_DEVICE_TOKEN_SUCCESS',
+payload: ?string,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_REPORT_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_REPORT_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_REPORT_SUCCESS',
+payload?: ClearDeliveredReportsPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_REPORTS_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_REPORTS_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_REPORTS_SUCCESS',
+payload?: ClearDeliveredReportsPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'QUEUE_REPORTS',
+payload: QueueReportsPayload,
}
| {
+type: 'SET_URL_PREFIX',
+payload: string,
}
| {
+type: 'SAVE_MESSAGES',
+payload: SaveMessagesPayload,
}
| {
+type: 'UPDATE_CALENDAR_THREAD_FILTER',
+payload: CalendarThreadFilter,
}
| {
+type: 'CLEAR_CALENDAR_THREAD_FILTER',
+payload?: void,
}
| {
+type: 'SET_CALENDAR_DELETED_FILTER',
+payload: SetCalendarDeletedFilterPayload,
}
| {
+type: 'UPDATE_SUBSCRIPTION_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_SUBSCRIPTION_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_SUBSCRIPTION_SUCCESS',
+payload: SubscriptionUpdateResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_CALENDAR_QUERY_STARTED',
+loadingInfo: LoadingInfo,
+payload?: CalendarQueryUpdateStartingPayload,
}
| {
+type: 'UPDATE_CALENDAR_QUERY_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_CALENDAR_QUERY_SUCCESS',
+payload: CalendarQueryUpdateResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FULL_STATE_SYNC',
+payload: StateSyncFullActionPayload,
}
| {
+type: 'INCREMENTAL_STATE_SYNC',
+payload: StateSyncIncrementalActionPayload,
}
| {
+type: 'PROCESS_SERVER_REQUESTS',
+payload: ProcessServerRequestsPayload,
}
| {
+type: 'UPDATE_CONNECTION_STATUS',
+payload: UpdateConnectionStatusPayload,
}
| {
+type: 'QUEUE_ACTIVITY_UPDATES',
+payload: QueueActivityUpdatesPayload,
}
| {
+type: 'UNSUPERVISED_BACKGROUND',
+payload?: void,
}
| {
+type: 'UPDATE_LIFECYCLE_STATE',
+payload: LifecycleState,
}
| {
+type: 'ENABLE_APP',
+payload: SupportedApps,
}
| {
+type: 'DISABLE_APP',
+payload: SupportedApps,
}
| {
+type: 'UPDATE_REPORTS_ENABLED',
+payload: Shape,
}
| {
+type: 'PROCESS_UPDATES',
+payload: ClientUpdatesResultWithUserInfos,
}
| {
+type: 'PROCESS_MESSAGES',
+payload: NewMessagesPayload,
}
| {
+type: 'MESSAGE_STORE_PRUNE',
+payload: MessageStorePrunePayload,
}
| {
+type: 'SET_LATE_RESPONSE',
+payload: SetLateResponsePayload,
}
| {
+type: 'UPDATE_DISCONNECTED_BAR',
+payload: UpdateDisconnectedBarPayload,
}
| {
+type: 'REQUEST_ACCESS_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'REQUEST_ACCESS_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'REQUEST_ACCESS_SUCCESS',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA',
+payload: UpdateMultimediaMessageMediaPayload,
}
| {
+type: 'CREATE_LOCAL_MESSAGE',
+payload: LocallyComposedMessageInfo,
}
| {
+type: 'UPDATE_RELATIONSHIPS_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_RELATIONSHIPS_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_RELATIONSHIPS_SUCCESS',
+payload: RelationshipErrors,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SET_THREAD_UNREAD_STATUS_STARTED',
+payload: {
+threadID: string,
+unread: boolean,
},
+loadingInfo: LoadingInfo,
}
| {
+type: 'SET_THREAD_UNREAD_STATUS_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SET_THREAD_UNREAD_STATUS_SUCCESS',
+payload: SetThreadUnreadStatusPayload,
}
| {
+type: 'SET_USER_SETTINGS_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SET_USER_SETTINGS_SUCCESS',
+payload: DefaultNotificationPayload,
}
| {
+type: 'SET_USER_SETTINGS_FAILED',
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_MESSAGE_REPORT_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_MESSAGE_REPORT_SUCCESS',
+payload: MessageReportCreationResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_MESSAGE_REPORT_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'FORCE_POLICY_ACKNOWLEDGMENT',
+payload: ForcePolicyAcknowledgmentPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'POLICY_ACKNOWLEDGMENT_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'POLICY_ACKNOWLEDGMENT_SUCCESS',
+payload: PolicyAcknowledgmentPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'POLICY_ACKNOWLEDGMENT_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'GET_SIWE_NONCE_STARTED',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'GET_SIWE_NONCE_SUCCESS',
+payload?: void,
+loadingInfo: LoadingInfo,
}
| {
+type: 'GET_SIWE_NONCE_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SIWE_AUTH_STARTED',
+payload: LogInStartingPayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SIWE_AUTH_SUCCESS',
+payload: LogInResult,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SIWE_AUTH_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
+ }
+ | {
+ +type: 'RECORD_NOTIF_PERMISSION_ALERT',
+ +payload: { +time: number },
};
export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string);
export type SuperAction = {
type: string,
payload?: ActionPayload,
loadingInfo?: LoadingInfo,
error?: boolean,
};
type ThunkedAction = (dispatch: Dispatch) => void;
export type PromisedAction = (dispatch: Dispatch) => Promise;
export type Dispatch = ((promisedAction: PromisedAction) => Promise) &
((thunkedAction: ThunkedAction) => void) &
((action: SuperAction) => boolean);
// This is lifted from redux-persist/lib/constants.js
// I don't want to add redux-persist to the web/server bundles...
// import { REHYDRATE } from 'redux-persist';
export const rehydrateActionType = 'persist/REHYDRATE';
diff --git a/native/push/alerts.js b/lib/utils/push-alerts.js
similarity index 56%
rename from native/push/alerts.js
rename to lib/utils/push-alerts.js
index 3b99a7732..53228ad1a 100644
--- a/native/push/alerts.js
+++ b/lib/utils/push-alerts.js
@@ -1,13 +1,18 @@
// @flow
export type NotifPermissionAlertInfo = {
+totalAlerts: number,
+lastAlertTime: number,
};
const defaultNotifPermissionAlertInfo: NotifPermissionAlertInfo = {
totalAlerts: 0,
lastAlertTime: 0,
};
-export { defaultNotifPermissionAlertInfo };
+const recordNotifPermissionAlertActionType = 'RECORD_NOTIF_PERMISSION_ALERT';
+
+export {
+ defaultNotifPermissionAlertInfo,
+ recordNotifPermissionAlertActionType,
+};
diff --git a/native/push/push-handler.react.js b/native/push/push-handler.react.js
index f1f5fe0ed..5f89daa2a 100644
--- a/native/push/push-handler.react.js
+++ b/native/push/push-handler.react.js
@@ -1,614 +1,616 @@
// @flow
import * as Haptics from 'expo-haptics';
import * as React from 'react';
import { Platform, Alert, LogBox } from 'react-native';
import { Notification as InAppNotification } from 'react-native-in-app-message';
import { useDispatch } from 'react-redux';
import {
setDeviceTokenActionTypes,
setDeviceToken,
} from 'lib/actions/device-actions.js';
import { saveMessagesActionType } from 'lib/actions/message-actions.js';
import {
unreadCount,
threadInfoSelector,
} from 'lib/selectors/thread-selectors.js';
import { isLoggedIn } from 'lib/selectors/user-selectors.js';
import { mergePrefixIntoBody } from 'lib/shared/notif-utils.js';
import type { RawMessageInfo } from 'lib/types/message-types.js';
import type { Dispatch } from 'lib/types/redux-types.js';
import { type ConnectionInfo } from 'lib/types/socket-types.js';
import { type ThreadInfo } from 'lib/types/thread-types.js';
import {
useServerCall,
useDispatchActionPromise,
type DispatchActionPromise,
} from 'lib/utils/action-utils.js';
+import {
+ type NotifPermissionAlertInfo,
+ recordNotifPermissionAlertActionType,
+} from 'lib/utils/push-alerts.js';
-import { type NotifPermissionAlertInfo } from './alerts.js';
import {
androidNotificationChannelID,
handleAndroidMessage,
getCommAndroidNotificationsEventEmitter,
type AndroidForegroundMessage,
CommAndroidNotifications,
} from './android.js';
import {
CommIOSNotification,
type CoreIOSNotificationData,
type CoreIOSNotificationDataWithRequestIdentifier,
} from './comm-ios-notification.js';
import InAppNotif from './in-app-notif.react.js';
import {
requestIOSPushPermissions,
iosPushPermissionResponseReceived,
CommIOSNotifications,
getCommIOSNotificationsEventEmitter,
} from './ios.js';
import {
type MessageListParams,
useNavigateToThread,
} from '../chat/message-list-types.js';
import {
addLifecycleListener,
getCurrentLifecycleState,
} from '../lifecycle/lifecycle.js';
import { replaceWithThreadActionType } from '../navigation/action-types.js';
import { activeMessageListSelector } from '../navigation/nav-selectors.js';
import { NavContext } from '../navigation/navigation-context.js';
import type { RootNavigationProp } from '../navigation/root-navigator.react.js';
-import { recordNotifPermissionAlertActionType } from '../redux/action-types.js';
import { useSelector } from '../redux/redux-utils.js';
import { RootContext, type RootContextType } from '../root-context.js';
import type { EventSubscription } from '../types/react-native.js';
import { type GlobalTheme } from '../types/themes.js';
LogBox.ignoreLogs([
// react-native-in-app-message
'ForceTouchGestureHandler is not available',
]);
const msInDay = 24 * 60 * 60 * 1000;
type BaseProps = {
+navigation: RootNavigationProp<'App'>,
};
type Props = {
...BaseProps,
// Navigation state
+activeThread: ?string,
// Redux state
+unreadCount: number,
+deviceToken: ?string,
+threadInfos: { +[id: string]: ThreadInfo },
+notifPermissionAlertInfo: NotifPermissionAlertInfo,
+connection: ConnectionInfo,
+updatesCurrentAsOf: number,
+activeTheme: ?GlobalTheme,
+loggedIn: boolean,
+navigateToThread: (params: MessageListParams) => void,
// Redux dispatch functions
+dispatch: Dispatch,
+dispatchActionPromise: DispatchActionPromise,
// async functions that hit server APIs
+setDeviceToken: (deviceToken: ?string) => Promise,
// withRootContext
+rootContext: ?RootContextType,
};
type State = {
+inAppNotifProps: ?{
+customComponent: React.Node,
+blurType: ?('xlight' | 'dark'),
+onPress: () => void,
},
};
class PushHandler extends React.PureComponent {
state: State = {
inAppNotifProps: null,
};
currentState: ?string = getCurrentLifecycleState();
appStarted = 0;
androidNotificationsEventSubscriptions: Array = [];
initialAndroidNotifHandled = false;
openThreadOnceReceived: Set = new Set();
lifecycleSubscription: ?EventSubscription;
iosNotificationEventSubscriptions: Array = [];
componentDidMount() {
this.appStarted = Date.now();
this.lifecycleSubscription = addLifecycleListener(
this.handleAppStateChange,
);
this.onForeground();
if (Platform.OS === 'ios') {
const commIOSNotificationsEventEmitter =
getCommIOSNotificationsEventEmitter();
this.iosNotificationEventSubscriptions.push(
commIOSNotificationsEventEmitter.addListener(
'remoteNotificationsRegistered',
registration =>
this.registerPushPermissions(registration?.deviceToken),
),
commIOSNotificationsEventEmitter.addListener(
'remoteNotificationsRegistrationFailed',
this.failedToRegisterPushPermissions,
),
commIOSNotificationsEventEmitter.addListener(
'notificationReceivedForeground',
this.iosForegroundNotificationReceived,
),
commIOSNotificationsEventEmitter.addListener(
'notificationOpened',
this.iosNotificationOpened,
),
);
} else if (Platform.OS === 'android') {
CommAndroidNotifications.createChannel(
androidNotificationChannelID,
'Default',
CommAndroidNotifications.getConstants().NOTIFICATIONS_IMPORTANCE_HIGH,
'Comm notifications channel',
);
const commAndroidNotificationsEventEmitter =
getCommAndroidNotificationsEventEmitter();
this.androidNotificationsEventSubscriptions.push(
commAndroidNotificationsEventEmitter.addListener(
'commAndroidNotificationsToken',
this.handleAndroidDeviceToken,
),
commAndroidNotificationsEventEmitter.addListener(
'commAndroidNotificationsForegroundMessage',
this.androidMessageReceived,
),
commAndroidNotificationsEventEmitter.addListener(
'commAndroidNotificationsNotificationOpened',
this.androidNotificationOpened,
),
);
}
if (this.props.connection.status === 'connected') {
this.updateBadgeCount();
}
}
componentWillUnmount() {
if (this.lifecycleSubscription) {
this.lifecycleSubscription.remove();
}
if (Platform.OS === 'ios') {
for (const iosNotificationEventSubscription of this
.iosNotificationEventSubscriptions) {
iosNotificationEventSubscription.remove();
}
} else if (Platform.OS === 'android') {
for (const androidNotificationsEventSubscription of this
.androidNotificationsEventSubscriptions) {
androidNotificationsEventSubscription.remove();
}
this.androidNotificationsEventSubscriptions = [];
}
}
handleAppStateChange = (nextState: ?string) => {
if (!nextState || nextState === 'unknown') {
return;
}
const lastState = this.currentState;
this.currentState = nextState;
if (lastState === 'background' && nextState === 'active') {
this.onForeground();
this.clearNotifsOfThread();
}
};
onForeground() {
if (this.props.loggedIn) {
this.ensurePushNotifsEnabled();
} else if (this.props.deviceToken) {
// We do this in case there was a crash, so we can clear deviceToken from
// any other cookies it might be set for
this.setDeviceToken(this.props.deviceToken);
}
}
componentDidUpdate(prevProps: Props, prevState: State) {
if (this.props.activeThread !== prevProps.activeThread) {
this.clearNotifsOfThread();
}
if (
this.props.connection.status === 'connected' &&
(prevProps.connection.status !== 'connected' ||
this.props.unreadCount !== prevProps.unreadCount)
) {
this.updateBadgeCount();
}
for (const threadID of this.openThreadOnceReceived) {
const threadInfo = this.props.threadInfos[threadID];
if (threadInfo) {
this.navigateToThread(threadInfo, false);
this.openThreadOnceReceived.clear();
break;
}
}
if (
(this.props.loggedIn && !prevProps.loggedIn) ||
(!this.props.deviceToken && prevProps.deviceToken)
) {
this.ensurePushNotifsEnabled();
}
if (!this.props.loggedIn && prevProps.loggedIn) {
this.clearAllNotifs();
}
if (
this.state.inAppNotifProps &&
this.state.inAppNotifProps !== prevState.inAppNotifProps
) {
Haptics.notificationAsync();
InAppNotification.show();
}
}
updateBadgeCount() {
const curUnreadCount = this.props.unreadCount;
if (Platform.OS === 'ios') {
CommIOSNotifications.setBadgesCount(curUnreadCount);
} else if (Platform.OS === 'android') {
CommAndroidNotifications.setBadge(curUnreadCount);
}
}
clearAllNotifs() {
if (Platform.OS === 'ios') {
CommIOSNotifications.removeAllDeliveredNotifications();
} else if (Platform.OS === 'android') {
CommAndroidNotifications.removeAllDeliveredNotifications();
}
}
clearNotifsOfThread() {
const { activeThread } = this.props;
if (!activeThread) {
return;
}
if (Platform.OS === 'ios') {
CommIOSNotifications.getDeliveredNotifications(notifications =>
PushHandler.clearDeliveredIOSNotificationsForThread(
activeThread,
notifications,
),
);
} else if (Platform.OS === 'android') {
CommAndroidNotifications.removeAllActiveNotificationsForThread(
activeThread,
);
}
}
static clearDeliveredIOSNotificationsForThread(
threadID: string,
notifications: $ReadOnlyArray,
) {
const identifiersToClear = [];
for (const notification of notifications) {
if (notification.threadID === threadID) {
identifiersToClear.push(notification.identifier);
}
}
if (identifiersToClear) {
CommIOSNotifications.removeDeliveredNotifications(identifiersToClear);
}
}
async ensurePushNotifsEnabled() {
if (!this.props.loggedIn) {
return;
}
if (Platform.OS === 'ios') {
const missingDeviceToken =
this.props.deviceToken === null || this.props.deviceToken === undefined;
await requestIOSPushPermissions(missingDeviceToken);
} else if (Platform.OS === 'android') {
await this.ensureAndroidPushNotifsEnabled();
}
}
async ensureAndroidPushNotifsEnabled() {
const hasPermission = await CommAndroidNotifications.hasPermission();
if (!hasPermission) {
this.failedToRegisterPushPermissions();
return;
}
try {
const fcmToken = await CommAndroidNotifications.getToken();
await this.handleAndroidDeviceToken(fcmToken);
} catch (e) {
this.failedToRegisterPushPermissions();
}
}
handleAndroidDeviceToken = async (deviceToken: string) => {
this.registerPushPermissions(deviceToken);
await this.handleInitialAndroidNotification();
};
async handleInitialAndroidNotification() {
if (this.initialAndroidNotifHandled) {
return;
}
this.initialAndroidNotifHandled = true;
const initialNotif =
await CommAndroidNotifications.getInitialNotification();
if (initialNotif) {
await this.androidNotificationOpened(initialNotif);
}
}
registerPushPermissions = (deviceToken: ?string) => {
const deviceType = Platform.OS;
if (deviceType !== 'android' && deviceType !== 'ios') {
return;
}
if (deviceType === 'ios') {
iosPushPermissionResponseReceived();
}
if (deviceToken !== this.props.deviceToken) {
this.setDeviceToken(deviceToken);
}
};
setDeviceToken(deviceToken: ?string) {
this.props.dispatchActionPromise(
setDeviceTokenActionTypes,
this.props.setDeviceToken(deviceToken),
);
}
failedToRegisterPushPermissions = () => {
this.setDeviceToken(null);
if (!this.props.loggedIn) {
return;
}
const deviceType = Platform.OS;
if (deviceType === 'ios') {
iosPushPermissionResponseReceived();
} else {
this.showNotifAlertOnAndroid();
}
};
showNotifAlertOnAndroid() {
const alertInfo = this.props.notifPermissionAlertInfo;
if (
(alertInfo.totalAlerts > 3 &&
alertInfo.lastAlertTime > Date.now() - msInDay) ||
(alertInfo.totalAlerts > 6 &&
alertInfo.lastAlertTime > Date.now() - msInDay * 3) ||
(alertInfo.totalAlerts > 9 &&
alertInfo.lastAlertTime > Date.now() - msInDay * 7)
) {
return;
}
this.props.dispatch({
type: recordNotifPermissionAlertActionType,
payload: { time: Date.now() },
});
Alert.alert(
'Unable to initialize notifs!',
'Please check your network connection, make sure Google Play ' +
'services are installed and enabled, and confirm that your Google ' +
'Play credentials are valid in the Google Play Store.',
undefined,
{ cancelable: true },
);
}
navigateToThread(threadInfo: ThreadInfo, clearChatRoutes: boolean) {
if (clearChatRoutes) {
this.props.navigation.dispatch({
type: replaceWithThreadActionType,
payload: { threadInfo },
});
} else {
this.props.navigateToThread({ threadInfo });
}
}
onPressNotificationForThread(threadID: string, clearChatRoutes: boolean) {
const threadInfo = this.props.threadInfos[threadID];
if (threadInfo) {
this.navigateToThread(threadInfo, clearChatRoutes);
} else {
this.openThreadOnceReceived.add(threadID);
}
}
saveMessageInfos(messageInfosString: ?string) {
if (!messageInfosString) {
return;
}
const rawMessageInfos: $ReadOnlyArray =
JSON.parse(messageInfosString);
const { updatesCurrentAsOf } = this.props;
this.props.dispatch({
type: saveMessagesActionType,
payload: { rawMessageInfos, updatesCurrentAsOf },
});
}
iosForegroundNotificationReceived = (
rawNotification: CoreIOSNotificationData,
) => {
const notification = new CommIOSNotification(rawNotification);
if (Date.now() < this.appStarted + 1500) {
// On iOS, when the app is opened from a notif press, for some reason this
// callback gets triggered before iosNotificationOpened. In fact this
// callback shouldn't be triggered at all. To avoid weirdness we are
// ignoring any foreground notification received within the first second
// of the app being started, since they are most likely to be erroneous.
notification.finish(
CommIOSNotifications.getConstants().FETCH_RESULT_NO_DATA,
);
return;
}
const threadID = notification.getData().threadID;
const messageInfos = notification.getData().messageInfos;
this.saveMessageInfos(messageInfos);
let title = notification.getData().title;
let body = notification.getData().body;
if (title && body) {
({ title, body } = mergePrefixIntoBody({ title, body }));
} else {
body = notification.getMessage();
}
if (body) {
this.showInAppNotification(threadID, body, title);
} else {
console.log(
'Non-rescind foreground notification without alert received!',
);
}
notification.finish(
CommIOSNotifications.getConstants().FETCH_RESULT_NEW_DATA,
);
};
onPushNotifBootsApp() {
if (
this.props.rootContext &&
this.props.rootContext.detectUnsupervisedBackground
) {
this.props.rootContext.detectUnsupervisedBackground(false);
}
}
iosNotificationOpened = (rawNotification: CoreIOSNotificationData) => {
const notification = new CommIOSNotification(rawNotification);
this.onPushNotifBootsApp();
const threadID = notification.getData().threadID;
const messageInfos = notification.getData().messageInfos;
this.saveMessageInfos(messageInfos);
this.onPressNotificationForThread(threadID, true);
notification.finish(
CommIOSNotifications.getConstants().FETCH_RESULT_NEW_DATA,
);
};
showInAppNotification(threadID: string, message: string, title?: ?string) {
if (threadID === this.props.activeThread) {
return;
}
this.setState({
inAppNotifProps: {
customComponent: (
),
blurType: this.props.activeTheme === 'dark' ? 'xlight' : 'dark',
onPress: () => {
InAppNotification.hide();
this.onPressNotificationForThread(threadID, false);
},
},
});
}
androidNotificationOpened = async (
notificationOpen: AndroidForegroundMessage,
) => {
this.onPushNotifBootsApp();
const { threadID } = notificationOpen;
this.onPressNotificationForThread(threadID, true);
};
androidMessageReceived = async (message: AndroidForegroundMessage) => {
this.onPushNotifBootsApp();
const { messageInfos } = message;
this.saveMessageInfos(messageInfos);
handleAndroidMessage(
message,
this.props.updatesCurrentAsOf,
this.handleAndroidNotificationIfActive,
);
};
handleAndroidNotificationIfActive = (
threadID: string,
texts: { body: string, title: ?string },
) => {
if (this.currentState !== 'active') {
return false;
}
this.showInAppNotification(threadID, texts.body, texts.title);
return true;
};
render() {
return (
);
}
}
const ConnectedPushHandler: React.ComponentType =
React.memo(function ConnectedPushHandler(props: BaseProps) {
const navContext = React.useContext(NavContext);
const activeThread = activeMessageListSelector(navContext);
const boundUnreadCount = useSelector(unreadCount);
const deviceToken = useSelector(state => state.deviceToken);
const threadInfos = useSelector(threadInfoSelector);
const notifPermissionAlertInfo = useSelector(
state => state.notifPermissionAlertInfo,
);
const connection = useSelector(state => state.connection);
const updatesCurrentAsOf = useSelector(state => state.updatesCurrentAsOf);
const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme);
const loggedIn = useSelector(isLoggedIn);
const navigateToThread = useNavigateToThread();
const dispatch = useDispatch();
const dispatchActionPromise = useDispatchActionPromise();
const boundSetDeviceToken = useServerCall(setDeviceToken);
const rootContext = React.useContext(RootContext);
return (
);
});
export default ConnectedPushHandler;
diff --git a/native/redux/action-types.js b/native/redux/action-types.js
index dbfed6c82..f2a45e514 100644
--- a/native/redux/action-types.js
+++ b/native/redux/action-types.js
@@ -1,73 +1,67 @@
// @flow
import type { Orientations } from 'react-native-orientation-locker';
import { saveMessagesActionType } from 'lib/actions/message-actions.js';
import type { Shape } from 'lib/types/core.js';
import type { BaseAction } from 'lib/types/redux-types.js';
import type { DimensionsInfo } from './dimensions-updater.react.js';
import type { AppState } from './state-types.js';
import type { DeviceCameraInfo } from '../types/camera.js';
import type { ConnectivityInfo } from '../types/connectivity.js';
import type { GlobalThemeInfo } from '../types/themes.js';
export const resetUserStateActionType = 'RESET_USER_STATE';
-export const recordNotifPermissionAlertActionType =
- 'RECORD_NOTIF_PERMISSION_ALERT';
export const updateDimensionsActiveType = 'UPDATE_DIMENSIONS';
export const updateConnectivityActiveType = 'UPDATE_CONNECTIVITY';
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 backgroundActionTypes: Set = new Set([
saveMessagesActionType,
]);
export type Action =
| BaseAction
| {
+type: 'SET_REDUX_STATE',
+payload: { +state: AppState, +hideFromMonitor: boolean },
}
| {
+type: 'SET_CUSTOM_SERVER',
+payload: string,
}
- | {
- +type: 'RECORD_NOTIF_PERMISSION_ALERT',
- +payload: { +time: number },
- }
| { +type: 'RESET_USER_STATE' }
| {
+type: 'UPDATE_DIMENSIONS',
+payload: Shape,
}
| {
+type: 'UPDATE_CONNECTIVITY',
+payload: ConnectivityInfo,
}
| {
+type: 'UPDATE_THEME_INFO',
+payload: Shape,
}
| {
+type: 'UPDATE_DEVICE_CAMERA_INFO',
+payload: Shape,
}
| {
+type: 'UPDATE_DEVICE_ORIENTATION',
+payload: Orientations,
}
| {
+type: 'UPDATE_THREAD_LAST_NAVIGATED',
+payload: { +threadID: string, +time: number },
}
| {
+type: 'SET_STORE_LOADED',
};
diff --git a/native/redux/persist.js b/native/redux/persist.js
index 69e5a6a5d..3b8435ffa 100644
--- a/native/redux/persist.js
+++ b/native/redux/persist.js
@@ -1,576 +1,576 @@
// @flow
import AsyncStorage from '@react-native-async-storage/async-storage';
import invariant from 'invariant';
import { Platform } from 'react-native';
import Orientation from 'react-native-orientation-locker';
import { createMigrate, createTransform } from 'redux-persist';
import type { Transform } from 'redux-persist/es/types.js';
import { highestLocalIDSelector } from 'lib/selectors/local-id-selectors.js';
import { inconsistencyResponsesToReports } from 'lib/shared/report-utils.js';
import {
getContainingThreadID,
getCommunity,
} from 'lib/shared/thread-utils.js';
import { unshimMessageStore, unshimFunc } from 'lib/shared/unshim-utils.js';
import { defaultEnabledApps } from 'lib/types/enabled-apps.js';
import { defaultCalendarFilters } from 'lib/types/filter-types.js';
import {
type LocalMessageInfo,
type MessageStore,
messageTypes,
type ClientDBMessageStoreOperation,
type ClientDBMessageInfo,
} from 'lib/types/message-types.js';
import { defaultConnectionInfo } from 'lib/types/socket-types.js';
import {
translateClientDBMessageInfoToRawMessageInfo,
translateRawMessageInfoToClientDBMessageInfo,
} from 'lib/utils/message-ops-utils.js';
+import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js';
import { convertThreadStoreOperationsToClientDBOperations } from 'lib/utils/thread-ops-utils.js';
import { migrateThreadStoreForEditThreadPermissions } from './edit-thread-permission-migration.js';
import type { AppState } from './state-types.js';
import { commCoreModule } from '../native-modules.js';
-import { defaultNotifPermissionAlertInfo } from '../push/alerts.js';
import { defaultDeviceCameraInfo } from '../types/camera.js';
import { defaultGlobalThemeInfo } from '../types/themes.js';
import { isTaskCancelledError } from '../utils/error-handling.js';
const migrations = {
[1]: (state: AppState) => ({
...state,
notifPermissionAlertInfo: defaultNotifPermissionAlertInfo,
}),
[2]: (state: AppState) => ({
...state,
messageSentFromRoute: [],
}),
[3]: state => ({
currentUserInfo: state.currentUserInfo,
entryStore: state.entryStore,
threadInfos: state.threadInfos,
userInfos: state.userInfos,
messageStore: {
...state.messageStore,
currentAsOf: state.currentAsOf,
},
updatesCurrentAsOf: state.currentAsOf,
cookie: state.cookie,
deviceToken: state.deviceToken,
urlPrefix: state.urlPrefix,
customServer: state.customServer,
notifPermissionAlertInfo: state.notifPermissionAlertInfo,
messageSentFromRoute: state.messageSentFromRoute,
_persist: state._persist,
}),
[4]: (state: AppState) => ({
...state,
pingTimestamps: undefined,
activeServerRequests: undefined,
}),
[5]: (state: AppState) => ({
...state,
calendarFilters: defaultCalendarFilters,
}),
[6]: state => ({
...state,
threadInfos: undefined,
threadStore: {
threadInfos: state.threadInfos,
inconsistencyResponses: [],
},
}),
[7]: state => ({
...state,
lastUserInteraction: undefined,
sessionID: undefined,
entryStore: {
...state.entryStore,
inconsistencyResponses: [],
},
}),
[8]: (state: AppState) => ({
...state,
pingTimestamps: undefined,
activeServerRequests: undefined,
connection: defaultConnectionInfo(Platform.OS),
watchedThreadIDs: [],
entryStore: {
...state.entryStore,
actualizedCalendarQuery: undefined,
},
}),
[9]: (state: AppState) => ({
...state,
connection: {
...state.connection,
lateResponses: [],
},
}),
[10]: (state: AppState) => ({
...state,
nextLocalID: highestLocalIDSelector(state) + 1,
connection: {
...state.connection,
showDisconnectedBar: false,
},
messageStore: {
...state.messageStore,
local: {},
},
}),
[11]: (state: AppState) => ({
...state,
messageStore: unshimMessageStore(state.messageStore, [messageTypes.IMAGES]),
}),
[12]: (state: AppState) => ({
...state,
globalThemeInfo: defaultGlobalThemeInfo,
}),
[13]: (state: AppState) => ({
...state,
deviceCameraInfo: defaultDeviceCameraInfo,
deviceOrientation: Orientation.getInitialOrientation(),
}),
[14]: (state: AppState) => state,
[15]: state => ({
...state,
threadStore: {
...state.threadStore,
inconsistencyReports: inconsistencyResponsesToReports(
state.threadStore.inconsistencyResponses,
),
inconsistencyResponses: undefined,
},
entryStore: {
...state.entryStore,
inconsistencyReports: inconsistencyResponsesToReports(
state.entryStore.inconsistencyResponses,
),
inconsistencyResponses: undefined,
},
queuedReports: [],
}),
[16]: state => {
const result = {
...state,
messageSentFromRoute: undefined,
dataLoaded: !!state.currentUserInfo && !state.currentUserInfo.anonymous,
};
if (state.navInfo) {
result.navInfo = {
...state.navInfo,
navigationState: undefined,
};
}
return result;
},
[17]: state => ({
...state,
userInfos: undefined,
userStore: {
userInfos: state.userInfos,
inconsistencyResponses: [],
},
}),
[18]: state => ({
...state,
userStore: {
userInfos: state.userStore.userInfos,
inconsistencyReports: [],
},
}),
[19]: state => {
const threadInfos = {};
for (const threadID in state.threadStore.threadInfos) {
const threadInfo = state.threadStore.threadInfos[threadID];
const { visibilityRules, ...rest } = threadInfo;
threadInfos[threadID] = rest;
}
return {
...state,
threadStore: {
...state.threadStore,
threadInfos,
},
};
},
[20]: (state: AppState) => ({
...state,
messageStore: unshimMessageStore(state.messageStore, [
messageTypes.UPDATE_RELATIONSHIP,
]),
}),
[21]: (state: AppState) => ({
...state,
messageStore: unshimMessageStore(state.messageStore, [
messageTypes.CREATE_SIDEBAR,
messageTypes.SIDEBAR_SOURCE,
]),
}),
[22]: state => {
for (const key in state.drafts) {
const value = state.drafts[key];
try {
commCoreModule.updateDraft(key, value);
} catch (e) {
if (!isTaskCancelledError(e)) {
throw e;
}
}
}
return {
...state,
drafts: undefined,
};
},
[23]: state => ({
...state,
globalThemeInfo: defaultGlobalThemeInfo,
}),
[24]: state => ({
...state,
enabledApps: defaultEnabledApps,
}),
[25]: state => ({
...state,
crashReportsEnabled: __DEV__,
}),
[26]: state => {
const { currentUserInfo } = state;
if (currentUserInfo.anonymous) {
return state;
}
return {
...state,
crashReportsEnabled: undefined,
currentUserInfo: {
id: currentUserInfo.id,
username: currentUserInfo.username,
},
enabledReports: {
crashReports: __DEV__,
inconsistencyReports: __DEV__,
mediaReports: __DEV__,
},
};
},
[27]: state => ({
...state,
queuedReports: undefined,
enabledReports: undefined,
threadStore: {
...state.threadStore,
inconsistencyReports: undefined,
},
entryStore: {
...state.entryStore,
inconsistencyReports: undefined,
},
reportStore: {
enabledReports: {
crashReports: __DEV__,
inconsistencyReports: __DEV__,
mediaReports: __DEV__,
},
queuedReports: [
...state.entryStore.inconsistencyReports,
...state.threadStore.inconsistencyReports,
...state.queuedReports,
],
},
}),
[28]: state => {
const threadParentToChildren = {};
for (const threadID in state.threadStore.threadInfos) {
const threadInfo = state.threadStore.threadInfos[threadID];
const parentThreadInfo = threadInfo.parentThreadID
? state.threadStore.threadInfos[threadInfo.parentThreadID]
: null;
const parentIndex = parentThreadInfo ? parentThreadInfo.id : '-1';
if (!threadParentToChildren[parentIndex]) {
threadParentToChildren[parentIndex] = [];
}
threadParentToChildren[parentIndex].push(threadID);
}
const rootIDs = threadParentToChildren['-1'];
if (!rootIDs) {
// This should never happen, but if it somehow does we'll let the state
// check mechanism resolve it...
return state;
}
const threadInfos = {};
const stack = [...rootIDs];
while (stack.length > 0) {
const threadID = stack.shift();
const threadInfo = state.threadStore.threadInfos[threadID];
const parentThreadInfo = threadInfo.parentThreadID
? threadInfos[threadInfo.parentThreadID]
: null;
threadInfos[threadID] = {
...threadInfo,
containingThreadID: getContainingThreadID(
parentThreadInfo,
threadInfo.type,
),
community: getCommunity(parentThreadInfo),
};
const children = threadParentToChildren[threadID];
if (children) {
stack.push(...children);
}
}
return { ...state, threadStore: { ...state.threadStore, threadInfos } };
},
[29]: (state: AppState) => {
const updatedThreadInfos = migrateThreadStoreForEditThreadPermissions(
state.threadStore.threadInfos,
);
return {
...state,
threadStore: {
...state.threadStore,
threadInfos: updatedThreadInfos,
},
};
},
[30]: (state: AppState) => {
const threadInfos = state.threadStore.threadInfos;
const operations = [
{
type: 'remove_all',
},
...Object.keys(threadInfos).map((id: string) => ({
type: 'replace',
payload: { id, threadInfo: threadInfos[id] },
})),
];
try {
commCoreModule.processThreadStoreOperationsSync(
convertThreadStoreOperationsToClientDBOperations(operations),
);
} catch (exception) {
console.log(exception);
if (isTaskCancelledError(exception)) {
return state;
}
return { ...state, cookie: null };
}
return state;
},
[31]: (state: AppState) => {
const messages = state.messageStore.messages;
const operations: $ReadOnlyArray = [
{
type: 'remove_all',
},
...Object.keys(messages).map((id: string) => ({
type: 'replace',
payload: translateRawMessageInfoToClientDBMessageInfo(messages[id]),
})),
];
try {
commCoreModule.processMessageStoreOperationsSync(operations);
} catch (exception) {
console.log(exception);
if (isTaskCancelledError(exception)) {
return state;
}
return { ...state, cookie: null };
}
return state;
},
[32]: (state: AppState) => {
// 1. Get messages from SQLite `messages` table.
const clientDBMessageInfos = commCoreModule.getAllMessagesSync();
// 2. Translate `ClientDBMessageInfo`s to `RawMessageInfo`s.
const rawMessageInfos = clientDBMessageInfos.map(
translateClientDBMessageInfoToRawMessageInfo,
);
// 3. "Unshim" translated `RawMessageInfo`s.
const unshimmedRawMessageInfos = rawMessageInfos.map(messageInfo =>
unshimFunc(messageInfo, new Set([messageTypes.MULTIMEDIA])),
);
// 4. Translate unshimmed `RawMessageInfo`s back to `ClientDBMessageInfo`s.
const unshimmedClientDBMessageInfos = unshimmedRawMessageInfos.map(
translateRawMessageInfoToClientDBMessageInfo,
);
// 5. Construct `ClientDBMessageStoreOperation`s to clear SQLite `messages`
// table and repopulate with unshimmed `ClientDBMessageInfo`s.
const operations: $ReadOnlyArray = [
{
type: 'remove_all',
},
...unshimmedClientDBMessageInfos.map((message: ClientDBMessageInfo) => ({
type: 'replace',
payload: message,
})),
];
// 6. Try processing `ClientDBMessageStoreOperation`s and log out if
// `processMessageStoreOperationsSync(...)` throws an exception.
try {
commCoreModule.processMessageStoreOperationsSync(operations);
} catch (exception) {
console.log(exception);
return { ...state, cookie: null };
}
return state;
},
[33]: (state: AppState) => {
// 1. Get messages from SQLite `messages` table.
const clientDBMessageInfos = commCoreModule.getAllMessagesSync();
// 2. Translate `ClientDBMessageInfo`s to `RawMessageInfo`s.
const rawMessageInfos = clientDBMessageInfos.map(
translateClientDBMessageInfoToRawMessageInfo,
);
// 3. "Unshim" translated `RawMessageInfo`s.
const unshimmedRawMessageInfos = rawMessageInfos.map(messageInfo =>
unshimFunc(messageInfo, new Set([messageTypes.REACTION])),
);
// 4. Translate unshimmed `RawMessageInfo`s back to `ClientDBMessageInfo`s.
const unshimmedClientDBMessageInfos = unshimmedRawMessageInfos.map(
translateRawMessageInfoToClientDBMessageInfo,
);
// 5. Construct `ClientDBMessageStoreOperation`s to clear SQLite `messages`
// table and repopulate with unshimmed `ClientDBMessageInfo`s.
const operations: $ReadOnlyArray = [
{
type: 'remove_all',
},
...unshimmedClientDBMessageInfos.map((message: ClientDBMessageInfo) => ({
type: 'replace',
payload: message,
})),
];
// 6. Try processing `ClientDBMessageStoreOperation`s and log out if
// `processMessageStoreOperationsSync(...)` throws an exception.
try {
commCoreModule.processMessageStoreOperationsSync(operations);
} catch (exception) {
console.log(exception);
return { ...state, cookie: null };
}
return state;
},
[34]: state => {
const { threadIDsToNotifIDs, ...stateSansThreadIDsToNotifIDs } = state;
return stateSansThreadIDsToNotifIDs;
},
};
// After migration 31, we'll no longer want to persist `messageStore.messages`
// via redux-persist. However, we DO want to continue persisting everything in
// `messageStore` EXCEPT for `messages`. The `blacklist` property in
// `persistConfig` allows us to specify top-level keys that shouldn't be
// persisted. However, we aren't able to specify nested keys in `blacklist`.
// As a result, if we want to prevent nested keys from being persisted we'll
// need to use `createTransform(...)` to specify an `inbound` function that
// allows us to modify the `state` object before it's passed through
// `JSON.stringify(...)` and written to disk. We specify the keys for which
// this transformation should be executed in the `whitelist` property of the
// `config` object that's passed to `createTransform(...)`.
// eslint-disable-next-line no-unused-vars
type PersistedThreadMessageInfo = {
+startReached: boolean,
+lastNavigatedTo: number,
+lastPruned: number,
};
type PersistedMessageStore = {
+local: { +[id: string]: LocalMessageInfo },
+currentAsOf: number,
+threads: { +[threadID: string]: PersistedThreadMessageInfo },
};
const messageStoreMessagesBlocklistTransform: Transform = createTransform(
(state: MessageStore): PersistedMessageStore => {
const { messages, threads, ...messageStoreSansMessages } = state;
// We also do not want to persist `messageStore.threads[ID].messageIDs`
// because they can be deterministically computed based on messages we have
// from SQLite
const threadsToPersist = {};
for (const threadID in threads) {
const { messageIDs, ...threadsData } = threads[threadID];
threadsToPersist[threadID] = threadsData;
}
return { ...messageStoreSansMessages, threads: threadsToPersist };
},
(state: MessageStore): MessageStore => {
const { threads: persistedThreads, ...messageStore } = state;
const threads = {};
for (const threadID in persistedThreads) {
threads[threadID] = { ...persistedThreads[threadID], messageIDs: [] };
}
// We typically expect `messageStore.messages` to be `undefined` because
// messages are persisted in the SQLite `messages` table rather than via
// `redux-persist`. In this case we want to set `messageStore.messages`
// to {} so we don't run into issues with `messageStore.messages` being
// `undefined` (https://phab.comm.dev/D5545).
//
// However, in the case that a user is upgrading from a client where
// `persistConfig.version` < 31, we expect `messageStore.messages` to
// contain messages stored via `redux-persist` that we need in order
// to correctly populate the SQLite `messages` table in migration 31
// (https://phab.comm.dev/D2600).
//
// However, because `messageStoreMessagesBlocklistTransform` modifies
// `messageStore` before migrations are run, we need to make sure we aren't
// inadvertently clearing `messageStore.messages` (by setting to {}) before
// messages are stored in SQLite (https://linear.app/comm/issue/ENG-2377).
return { ...messageStore, threads, messages: messageStore.messages ?? {} };
},
{ whitelist: ['messageStore'] },
);
const persistConfig = {
key: 'root',
storage: AsyncStorage,
blacklist: [
'loadingStatuses',
'lifecycleState',
'dimensions',
'draftStore',
'connectivity',
'deviceOrientation',
'frozen',
'threadStore',
'storeLoaded',
],
debug: __DEV__,
version: 34,
transforms: [messageStoreMessagesBlocklistTransform],
migrate: (createMigrate(migrations, { debug: __DEV__ }): any),
timeout: ((__DEV__ ? 0 : undefined): number | void),
};
const codeVersion: number = commCoreModule.getCodeVersion();
// This local exists to avoid a circular dependency where redux-setup needs to
// import all the navigation and screen stuff, but some of those screens want to
// access the persistor to purge its state.
let storedPersistor = null;
function setPersistor(persistor: *) {
storedPersistor = persistor;
}
function getPersistor(): empty {
invariant(storedPersistor, 'should be set');
return storedPersistor;
}
export { persistConfig, codeVersion, setPersistor, getPersistor };
diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js
index 7f3ede425..92cea1072 100644
--- a/native/redux/redux-setup.js
+++ b/native/redux/redux-setup.js
@@ -1,522 +1,513 @@
// @flow
import { AppState as NativeAppState, Platform, Alert } from 'react-native';
import Orientation from 'react-native-orientation-locker';
import { createStore, applyMiddleware, type Store, compose } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import thunk from 'redux-thunk';
import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js';
import { siweAuthActionTypes } from 'lib/actions/siwe-actions.js';
import {
logOutActionTypes,
deleteAccountActionTypes,
logInActionTypes,
} from 'lib/actions/user-actions.js';
import baseReducer from 'lib/reducers/master-reducer.js';
import { processThreadStoreOperations } from 'lib/reducers/thread-reducer.js';
import {
invalidSessionDowngrade,
invalidSessionRecovery,
} from 'lib/shared/account-utils.js';
import { isStaff } from 'lib/shared/user-utils.js';
import { defaultEnabledApps } from 'lib/types/enabled-apps.js';
import { defaultCalendarFilters } from 'lib/types/filter-types.js';
import type { Dispatch, BaseAction } from 'lib/types/redux-types.js';
import { rehydrateActionType } from 'lib/types/redux-types.js';
import type { SetSessionPayload } from 'lib/types/session-types.js';
import { defaultConnectionInfo } from 'lib/types/socket-types.js';
import type { ThreadStoreOperation } from 'lib/types/thread-types.js';
import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js';
import { setNewSessionActionType } from 'lib/utils/action-utils.js';
import { convertMessageStoreOperationsToClientDBOperations } from 'lib/utils/message-ops-utils.js';
+import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js';
import { convertThreadStoreOperationsToClientDBOperations } from 'lib/utils/thread-ops-utils.js';
import {
resetUserStateActionType,
- recordNotifPermissionAlertActionType,
updateDimensionsActiveType,
updateConnectivityActiveType,
updateThemeInfoActionType,
updateDeviceCameraInfoActionType,
updateDeviceOrientationActionType,
updateThreadLastNavigatedActionType,
backgroundActionTypes,
setReduxStateActionType,
setStoreLoadedActionType,
type Action,
} from './action-types.js';
import { remoteReduxDevServerConfig } from './dev-tools.js';
import { defaultDimensionsInfo } from './dimensions-updater.react.js';
import { persistConfig, setPersistor } from './persist.js';
import type { AppState } from './state-types.js';
import { commCoreModule } from '../native-modules.js';
import { defaultNavInfo } from '../navigation/default-state.js';
import { getGlobalNavContext } from '../navigation/icky-global.js';
import { activeMessageListSelector } from '../navigation/nav-selectors.js';
-import { defaultNotifPermissionAlertInfo } from '../push/alerts.js';
import reactotron from '../reactotron.js';
import { defaultDeviceCameraInfo } from '../types/camera.js';
import { defaultConnectivityInfo } from '../types/connectivity.js';
import { defaultGlobalThemeInfo } from '../types/themes.js';
import { isTaskCancelledError } from '../utils/error-handling.js';
import { isStaffRelease } from '../utils/staff-utils.js';
import {
defaultURLPrefix,
natNodeServer,
setCustomServer,
getDevServerHostname,
} from '../utils/url-utils.js';
const defaultState = ({
navInfo: defaultNavInfo,
currentUserInfo: null,
draftStore: { drafts: {} },
entryStore: {
entryInfos: {},
daysToEntries: {},
lastUserInteractionCalendar: 0,
},
threadStore: {
threadInfos: {},
},
userStore: {
userInfos: {},
inconsistencyReports: [],
},
messageStore: {
messages: {},
threads: {},
local: {},
currentAsOf: 0,
},
storeLoaded: false,
updatesCurrentAsOf: 0,
loadingStatuses: {},
calendarFilters: defaultCalendarFilters,
cookie: null,
deviceToken: null,
dataLoaded: false,
urlPrefix: defaultURLPrefix,
customServer: natNodeServer,
notifPermissionAlertInfo: defaultNotifPermissionAlertInfo,
connection: defaultConnectionInfo(Platform.OS),
watchedThreadIDs: [],
lifecycleState: 'active',
enabledApps: defaultEnabledApps,
reportStore: {
enabledReports: {
crashReports: __DEV__,
inconsistencyReports: __DEV__,
mediaReports: __DEV__,
},
queuedReports: [],
},
nextLocalID: 0,
_persist: null,
dimensions: defaultDimensionsInfo,
connectivity: defaultConnectivityInfo,
globalThemeInfo: defaultGlobalThemeInfo,
deviceCameraInfo: defaultDeviceCameraInfo,
deviceOrientation: Orientation.getInitialOrientation(),
frozen: false,
userPolicies: {},
}: AppState);
function reducer(state: AppState = defaultState, action: Action) {
if (action.type === setReduxStateActionType) {
return action.payload.state;
}
// We want to alert staff/developers if there's a difference between the keys
// we expect to see REHYDRATED and the keys that are actually REHYDRATED.
// Context: https://linear.app/comm/issue/ENG-2127/
if (
action.type === rehydrateActionType &&
(__DEV__ ||
isStaffRelease ||
(state.currentUserInfo &&
state.currentUserInfo.id &&
isStaff(state.currentUserInfo.id)))
) {
// 1. Construct set of keys expected to be REHYDRATED
const defaultKeys = Object.keys(defaultState);
const expectedKeys = defaultKeys.filter(
each => !persistConfig.blacklist.includes(each),
);
const expectedKeysSet = new Set(expectedKeys);
// 2. Construct set of keys actually REHYDRATED
const rehydratedKeys = Object.keys(action.payload ?? {});
const rehydratedKeysSet = new Set(rehydratedKeys);
// 3. Determine the difference between the two sets
const expectedKeysNotRehydrated = expectedKeys.filter(
each => !rehydratedKeysSet.has(each),
);
const rehydratedKeysNotExpected = rehydratedKeys.filter(
each => !expectedKeysSet.has(each),
);
// 4. Display alerts with the differences between the two sets
if (expectedKeysNotRehydrated.length > 0) {
Alert.alert(
`EXPECTED KEYS NOT REHYDRATED: ${JSON.stringify(
expectedKeysNotRehydrated,
)}`,
);
}
if (rehydratedKeysNotExpected.length > 0) {
Alert.alert(
`REHYDRATED KEYS NOT EXPECTED: ${JSON.stringify(
rehydratedKeysNotExpected,
)}`,
);
}
}
if (
(action.type === setNewSessionActionType &&
invalidSessionDowngrade(
state,
action.payload.sessionChange.currentUserInfo,
action.payload.preRequestUserState,
)) ||
(action.type === logOutActionTypes.success &&
invalidSessionDowngrade(
state,
action.payload.currentUserInfo,
action.payload.preRequestUserState,
)) ||
(action.type === deleteAccountActionTypes.success &&
invalidSessionDowngrade(
state,
action.payload.currentUserInfo,
action.payload.preRequestUserState,
))
) {
return state;
}
if (
(action.type === setNewSessionActionType &&
action.payload.sessionChange.currentUserInfo &&
invalidSessionRecovery(
state,
action.payload.sessionChange.currentUserInfo,
action.payload.logInActionSource,
)) ||
((action.type === logInActionTypes.success ||
action.type === siweAuthActionTypes.success) &&
invalidSessionRecovery(
state,
action.payload.currentUserInfo,
action.payload.logInActionSource,
))
) {
return state;
}
if (action.type === setCustomServer) {
return {
...state,
customServer: action.payload,
};
- } else if (action.type === recordNotifPermissionAlertActionType) {
- return {
- ...state,
- notifPermissionAlertInfo: {
- totalAlerts: state.notifPermissionAlertInfo.totalAlerts + 1,
- lastAlertTime: action.payload.time,
- },
- };
} else if (action.type === resetUserStateActionType) {
const cookie =
state.cookie && state.cookie.startsWith('anonymous=')
? state.cookie
: null;
const currentUserInfo =
state.currentUserInfo && state.currentUserInfo.anonymous
? state.currentUserInfo
: null;
return {
...state,
currentUserInfo,
cookie,
};
} else if (action.type === updateDimensionsActiveType) {
return {
...state,
dimensions: {
...state.dimensions,
...action.payload,
},
};
} else if (action.type === updateConnectivityActiveType) {
return {
...state,
connectivity: action.payload,
};
} else if (action.type === updateThemeInfoActionType) {
return {
...state,
globalThemeInfo: {
...state.globalThemeInfo,
...action.payload,
},
};
} else if (action.type === updateDeviceCameraInfoActionType) {
return {
...state,
deviceCameraInfo: {
...state.deviceCameraInfo,
...action.payload,
},
};
} else if (action.type === updateDeviceOrientationActionType) {
return {
...state,
deviceOrientation: action.payload,
};
} else if (action.type === updateThreadLastNavigatedActionType) {
const { threadID, time } = action.payload;
if (state.messageStore.threads[threadID]) {
state = {
...state,
messageStore: {
...state.messageStore,
threads: {
...state.messageStore.threads,
[threadID]: {
...state.messageStore.threads[threadID],
lastNavigatedTo: time,
},
},
},
};
}
return state;
}
if (action.type === setNewSessionActionType) {
sessionInvalidationAlert(action.payload);
state = {
...state,
cookie: action.payload.sessionChange.cookie,
};
}
if (action.type === setStoreLoadedActionType) {
return {
...state,
storeLoaded: true,
};
}
if (action.type === setClientDBStoreActionType) {
state = {
...state,
storeLoaded: true,
};
const currentLoggedInUserID = state.currentUserInfo?.anonymous
? undefined
: state.currentUserInfo?.id;
const actionCurrentLoggedInUserID = action.payload.currentUserID;
if (
!currentLoggedInUserID ||
!actionCurrentLoggedInUserID ||
actionCurrentLoggedInUserID !== currentLoggedInUserID
) {
// If user is logged out now, was logged out at the time action was
// dispatched or their ID changed between action dispatch and a
// call to reducer we ignore the SQLite data since it is not valid
return state;
}
}
const baseReducerResult = baseReducer(state, (action: BaseAction));
state = baseReducerResult.state;
const { storeOperations } = baseReducerResult;
const {
draftStoreOperations,
threadStoreOperations,
messageStoreOperations,
} = storeOperations;
const fixUnreadActiveThreadResult = fixUnreadActiveThread(state, action);
state = fixUnreadActiveThreadResult.state;
const threadStoreOperationsWithUnreadFix = [
...threadStoreOperations,
...fixUnreadActiveThreadResult.threadStoreOperations,
];
const convertedThreadStoreOperations =
convertThreadStoreOperationsToClientDBOperations(
threadStoreOperationsWithUnreadFix,
);
const convertedMessageStoreOperations =
convertMessageStoreOperationsToClientDBOperations(messageStoreOperations);
(async () => {
try {
const promises = [];
if (convertedThreadStoreOperations.length > 0) {
promises.push(
commCoreModule.processThreadStoreOperations(
convertedThreadStoreOperations,
),
);
}
if (convertedMessageStoreOperations.length > 0) {
promises.push(
commCoreModule.processMessageStoreOperations(
convertedMessageStoreOperations,
),
);
}
if (draftStoreOperations.length > 0) {
promises.push(
commCoreModule.processDraftStoreOperations(draftStoreOperations),
);
}
await Promise.all(promises);
} catch (e) {
if (isTaskCancelledError(e)) {
return;
}
// this code will make an entry in SecureStore and cause re-creating
// database when user will open app again
commCoreModule.reportDBOperationsFailure();
commCoreModule.terminate();
}
})();
return state;
}
function sessionInvalidationAlert(payload: SetSessionPayload) {
if (
!payload.sessionChange.cookieInvalidated ||
!payload.preRequestUserState ||
!payload.preRequestUserState.currentUserInfo ||
payload.preRequestUserState.currentUserInfo.anonymous
) {
return;
}
if (payload.error === 'client_version_unsupported') {
const app = Platform.select({
ios: 'App Store',
android: 'Play Store',
});
Alert.alert(
'App out of date',
'Your app version is pretty old, and the server doesn’t know how to ' +
`speak to it anymore. Please use the ${app} app to update!`,
[{ text: 'OK' }],
{ cancelable: true },
);
} else {
Alert.alert(
'Session invalidated',
'We’re sorry, but your session was invalidated by the server. ' +
'Please log in again.',
[{ text: 'OK' }],
{ cancelable: true },
);
}
}
// Makes sure a currently focused thread is never unread. Note that we consider
// a backgrounded NativeAppState to actually be active if it last changed to
// inactive more than 10 seconds ago. This is because there is a delay when
// NativeAppState is updating in response to a foreground, and actions don't get
// processed more than 10 seconds after a backgrounding anyways. However we
// don't consider this for action types that can be expected to happen while the
// app is backgrounded.
type FixUnreadActiveThreadResult = {
+state: AppState,
+threadStoreOperations: $ReadOnlyArray,
};
function fixUnreadActiveThread(
state: AppState,
action: *,
): FixUnreadActiveThreadResult {
const navContext = getGlobalNavContext();
const activeThread = activeMessageListSelector(navContext);
if (
!activeThread ||
!state.threadStore.threadInfos[activeThread]?.currentUser.unread ||
(NativeAppState.currentState !== 'active' &&
(appLastBecameInactive + 10000 >= Date.now() ||
backgroundActionTypes.has(action.type)))
) {
return { state, threadStoreOperations: [] };
}
const updatedActiveThreadInfo = {
...state.threadStore.threadInfos[activeThread],
currentUser: {
...state.threadStore.threadInfos[activeThread].currentUser,
unread: false,
},
};
const threadStoreOperations = [
{
type: 'replace',
payload: {
id: activeThread,
threadInfo: updatedActiveThreadInfo,
},
},
];
const updatedThreadStore = processThreadStoreOperations(
state.threadStore,
threadStoreOperations,
);
return {
state: { ...state, threadStore: updatedThreadStore },
threadStoreOperations,
};
}
let appLastBecameInactive = 0;
function appBecameInactive() {
appLastBecameInactive = Date.now();
}
const middleware = applyMiddleware(thunk, reduxLoggerMiddleware);
let composeFunc = compose;
if (__DEV__ && global.HermesInternal) {
const { composeWithDevTools } = require('remote-redux-devtools/src/index.js');
composeFunc = composeWithDevTools({
name: 'Redux',
hostname: getDevServerHostname(),
...remoteReduxDevServerConfig,
});
} else if (global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
composeFunc = global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
name: 'Redux',
});
}
let enhancers;
if (reactotron) {
enhancers = composeFunc(middleware, reactotron.createEnhancer());
} else {
enhancers = composeFunc(middleware);
}
const store: Store = createStore(
persistReducer(persistConfig, reducer),
defaultState,
enhancers,
);
const persistor = persistStore(store);
setPersistor(persistor);
const unsafeDispatch: any = store.dispatch;
const dispatch: Dispatch = unsafeDispatch;
export { store, dispatch, appBecameInactive };
diff --git a/native/redux/state-types.js b/native/redux/state-types.js
index 13778c8d2..ee89a8488 100644
--- a/native/redux/state-types.js
+++ b/native/redux/state-types.js
@@ -1,59 +1,59 @@
// @flow
import type { Orientations } from 'react-native-orientation-locker';
import type { PersistState } from 'redux-persist/es/types.js';
import type { DraftStore } from 'lib/types/draft-types.js';
import type { EnabledApps } from 'lib/types/enabled-apps.js';
import type { EntryStore } from 'lib/types/entry-types.js';
import type { CalendarFilter } from 'lib/types/filter-types.js';
import type { LifecycleState } from 'lib/types/lifecycle-state-types.js';
import type { LoadingStatus } from 'lib/types/loading-types.js';
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 { ConnectionInfo } from 'lib/types/socket-types.js';
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';
import type { DimensionsInfo } from './dimensions-updater.react.js';
import type { NavInfo } from '../navigation/default-state.js';
-import type { NotifPermissionAlertInfo } from '../push/alerts.js';
import type { DeviceCameraInfo } from '../types/camera.js';
import type { ConnectivityInfo } from '../types/connectivity.js';
import type { GlobalThemeInfo } from '../types/themes.js';
export type AppState = {
navInfo: NavInfo,
currentUserInfo: ?CurrentUserInfo,
draftStore: DraftStore,
entryStore: EntryStore,
threadStore: ThreadStore,
userStore: UserStore,
messageStore: MessageStore,
storeLoaded: boolean,
updatesCurrentAsOf: number,
loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } },
calendarFilters: $ReadOnlyArray,
cookie: ?string,
deviceToken: ?string,
dataLoaded: boolean,
urlPrefix: string,
customServer: ?string,
notifPermissionAlertInfo: NotifPermissionAlertInfo,
connection: ConnectionInfo,
watchedThreadIDs: $ReadOnlyArray,
lifecycleState: LifecycleState,
enabledApps: EnabledApps,
reportStore: ReportStore,
nextLocalID: number,
_persist: ?PersistState,
sessionID?: void,
dimensions: DimensionsInfo,
connectivity: ConnectivityInfo,
globalThemeInfo: GlobalThemeInfo,
deviceCameraInfo: DeviceCameraInfo,
deviceOrientation: Orientations,
frozen: boolean,
userPolicies: UserPolicies,
};
diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js
index c457dbcd4..83d7be987 100644
--- a/web/redux/redux-setup.js
+++ b/web/redux/redux-setup.js
@@ -1,316 +1,318 @@
// @flow
import invariant from 'invariant';
import type { PersistState } from 'redux-persist/es/types.js';
import {
logOutActionTypes,
deleteAccountActionTypes,
} from 'lib/actions/user-actions.js';
import baseReducer from 'lib/reducers/master-reducer.js';
import { nonThreadCalendarFilters } from 'lib/selectors/calendar-filter-selectors.js';
import { mostRecentlyReadThreadSelector } from 'lib/selectors/thread-selectors.js';
import { isLoggedIn } from 'lib/selectors/user-selectors.js';
import { invalidSessionDowngrade } from 'lib/shared/account-utils.js';
import type { Shape } from 'lib/types/core.js';
import type {
CryptoStore,
OLMIdentityKeys,
PickledOLMAccount,
} from 'lib/types/crypto-types.js';
import type { DraftStore } from 'lib/types/draft-types.js';
import type { EnabledApps } from 'lib/types/enabled-apps.js';
import type { EntryStore } from 'lib/types/entry-types.js';
import {
type CalendarFilter,
calendarThreadFilterTypes,
} from 'lib/types/filter-types.js';
import type { LifecycleState } from 'lib/types/lifecycle-state-types.js';
import type { LoadingStatus } from 'lib/types/loading-types.js';
import type { MessageStore } from 'lib/types/message-types.js';
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 { ConnectionInfo } from 'lib/types/socket-types.js';
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';
+import type { NotifPermissionAlertInfo } from 'lib/utils/push-alerts.js';
import {
updateWindowActiveActionType,
setDeviceIDActionType,
updateNavInfoActionType,
updateWindowDimensionsActionType,
updateCalendarCommunityFilter,
clearCalendarCommunityFilter,
} from './action-types.js';
import {
reduceCryptoStore,
setPrimaryIdentityKeys,
setNotificationIdentityKeys,
setPickledNotificationAccount,
setPickledPrimaryAccount,
} from './crypto-store-reducer.js';
import { reduceDeviceID } from './device-id-reducer.js';
import reduceNavInfo from './nav-reducer.js';
import { getVisibility } from './visibility.js';
import { filterThreadIDsBelongingToCommunity } from '../selectors/calendar-selectors.js';
import { activeThreadSelector } from '../selectors/nav-selectors.js';
import { type NavInfo } from '../types/nav-types.js';
export type WindowDimensions = { width: number, height: number };
export type AppState = {
navInfo: NavInfo,
deviceID: ?string,
currentUserInfo: ?CurrentUserInfo,
draftStore: DraftStore,
sessionID: ?string,
entryStore: EntryStore,
threadStore: ThreadStore,
userStore: UserStore,
messageStore: MessageStore,
updatesCurrentAsOf: number,
loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } },
calendarFilters: $ReadOnlyArray,
calendarPickedCommunityID: ?string,
urlPrefix: string,
windowDimensions: WindowDimensions,
cookie?: void,
deviceToken: ?string,
baseHref: string,
+ notifPermissionAlertInfo: NotifPermissionAlertInfo,
connection: ConnectionInfo,
watchedThreadIDs: $ReadOnlyArray,
lifecycleState: LifecycleState,
enabledApps: EnabledApps,
reportStore: ReportStore,
nextLocalID: number,
dataLoaded: boolean,
windowActive: boolean,
userPolicies: UserPolicies,
cryptoStore: CryptoStore,
pushApiPublicKey: ?string,
_persist: ?PersistState,
};
export type Action =
| BaseAction
| { type: 'UPDATE_NAV_INFO', payload: Shape }
| {
type: 'UPDATE_WINDOW_DIMENSIONS',
payload: WindowDimensions,
}
| {
type: 'UPDATE_WINDOW_ACTIVE',
payload: boolean,
}
| {
type: 'SET_DEVICE_ID',
payload: string,
}
| { +type: 'SET_PRIMARY_IDENTITY_KEYS', payload: ?OLMIdentityKeys }
| { +type: 'SET_NOTIFICATION_IDENTITY_KEYS', payload: ?OLMIdentityKeys }
| { +type: 'SET_PICKLED_PRIMARY_ACCOUNT', payload: ?PickledOLMAccount }
| { +type: 'SET_PICKLED_NOTIFICATION_ACCOUNT', payload: ?PickledOLMAccount }
| {
+type: 'UPDATE_CALENDAR_COMMUNITY_FILTER',
+payload: string,
}
| {
+type: 'CLEAR_CALENDAR_COMMUNITY_FILTER',
+payload: void,
};
export function reducer(oldState: AppState | void, action: Action): AppState {
invariant(oldState, 'should be set');
let state = oldState;
if (action.type === updateWindowDimensionsActionType) {
return validateState(oldState, {
...state,
windowDimensions: action.payload,
});
} else if (action.type === updateWindowActiveActionType) {
return validateState(oldState, {
...state,
windowActive: action.payload,
});
} else if (action.type === updateCalendarCommunityFilter) {
const nonThreadFilters = nonThreadCalendarFilters(state.calendarFilters);
const threadIDs = Array.from(
filterThreadIDsBelongingToCommunity(
action.payload,
state.threadStore.threadInfos,
),
);
return {
...state,
calendarFilters: [
...nonThreadFilters,
{
type: calendarThreadFilterTypes.THREAD_LIST,
threadIDs,
},
],
calendarPickedCommunityID: action.payload,
};
} else if (action.type === clearCalendarCommunityFilter) {
const nonThreadFilters = nonThreadCalendarFilters(state.calendarFilters);
return {
...state,
calendarFilters: nonThreadFilters,
calendarPickedCommunityID: null,
};
} else if (action.type === setNewSessionActionType) {
if (
invalidSessionDowngrade(
oldState,
action.payload.sessionChange.currentUserInfo,
action.payload.preRequestUserState,
)
) {
return oldState;
}
state = {
...state,
sessionID: action.payload.sessionChange.sessionID,
};
} else if (
(action.type === logOutActionTypes.success &&
invalidSessionDowngrade(
oldState,
action.payload.currentUserInfo,
action.payload.preRequestUserState,
)) ||
(action.type === deleteAccountActionTypes.success &&
invalidSessionDowngrade(
oldState,
action.payload.currentUserInfo,
action.payload.preRequestUserState,
))
) {
return oldState;
}
if (
action.type !== updateNavInfoActionType &&
action.type !== setDeviceIDActionType &&
action.type !== setPrimaryIdentityKeys &&
action.type !== setNotificationIdentityKeys &&
action.type !== setPickledPrimaryAccount &&
action.type !== setPickledNotificationAccount
) {
state = baseReducer(state, action).state;
}
state = {
...state,
navInfo: reduceNavInfo(
state.navInfo,
action,
state.threadStore.threadInfos,
),
deviceID: reduceDeviceID(state.deviceID, action),
cryptoStore: reduceCryptoStore(state.cryptoStore, action),
};
return validateState(oldState, state);
}
function validateState(oldState: AppState, state: AppState): AppState {
if (
(state.navInfo.activeChatThreadID &&
!state.navInfo.pendingThread &&
!state.threadStore.threadInfos[state.navInfo.activeChatThreadID]) ||
(!state.navInfo.activeChatThreadID && isLoggedIn(state))
) {
// Makes sure the active thread always exists
state = {
...state,
navInfo: {
...state.navInfo,
activeChatThreadID: mostRecentlyReadThreadSelector(state),
},
};
}
const activeThread = activeThreadSelector(state);
if (
activeThread &&
!state.navInfo.pendingThread &&
state.threadStore.threadInfos[activeThread].currentUser.unread &&
getVisibility().hidden()
) {
console.warn(
`thread ${activeThread} is active and unread, ` +
'but visibilityjs reports the window is not visible',
);
}
if (
activeThread &&
!state.navInfo.pendingThread &&
state.threadStore.threadInfos[activeThread].currentUser.unread &&
typeof document !== 'undefined' &&
document &&
'hasFocus' in document &&
!document.hasFocus()
) {
console.warn(
`thread ${activeThread} is active and unread, ` +
'but document.hasFocus() is false',
);
}
if (
activeThread &&
!getVisibility().hidden() &&
typeof document !== 'undefined' &&
document &&
'hasFocus' in document &&
document.hasFocus() &&
!state.navInfo.pendingThread &&
state.threadStore.threadInfos[activeThread].currentUser.unread
) {
// Makes sure a currently focused thread is never unread
state = {
...state,
threadStore: {
...state.threadStore,
threadInfos: {
...state.threadStore.threadInfos,
[activeThread]: {
...state.threadStore.threadInfos[activeThread],
currentUser: {
...state.threadStore.threadInfos[activeThread].currentUser,
unread: false,
},
},
},
},
};
}
const oldActiveThread = activeThreadSelector(oldState);
if (
activeThread &&
oldActiveThread !== activeThread &&
state.messageStore.threads[activeThread]
) {
// Update messageStore.threads[activeThread].lastNavigatedTo
state = {
...state,
messageStore: {
...state.messageStore,
threads: {
...state.messageStore.threads,
[activeThread]: {
...state.messageStore.threads[activeThread],
lastNavigatedTo: Date.now(),
},
},
},
};
}
return state;
}