{
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, sqljsFilename, opaqueURL } =
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/types/redux-types.js b/lib/types/redux-types.js
index e0adf3f15..8815915b9 100644
--- a/lib/types/redux-types.js
+++ b/lib/types/redux-types.js
@@ -1,972 +1,973 @@
// @flow
import type {
LogOutResult,
LogInStartingPayload,
LogInResult,
RegisterResult,
DefaultNotificationPayload,
} from './account-types.js';
import type {
ActivityUpdateSuccessPayload,
QueueActivityUpdatesPayload,
SetThreadUnreadStatusPayload,
} from './activity-types.js';
import type { ClientAvatar, UpdateUserAvatarRequest } from './avatar-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,
EditMessagePayload,
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,
+ +commServicesAccessToken: ?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 },
}
| {
+type: 'UPDATE_USER_AVATAR_STARTED',
+payload: UpdateUserAvatarRequest,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_USER_AVATAR_SUCCESS',
+payload: ?ClientAvatar,
+loadingInfo: LoadingInfo,
}
| {
+type: 'UPDATE_USER_AVATAR_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_EDIT_MESSAGE_STARTED',
+loadingInfo?: LoadingInfo,
+payload?: void,
}
| {
+type: 'SEND_EDIT_MESSAGE_SUCCESS',
+payload: EditMessagePayload,
+loadingInfo: LoadingInfo,
}
| {
+type: 'SEND_EDIT_MESSAGE_FAILED',
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
};
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/redux/redux-setup.js b/native/redux/redux-setup.js
index 92cea1072..1ebf274d2 100644
--- a/native/redux/redux-setup.js
+++ b/native/redux/redux-setup.js
@@ -1,513 +1,514 @@
// @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,
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 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: {},
+ commServicesAccessToken: null,
}: 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 === 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 ee89a8488..89e494f24 100644
--- a/native/redux/state-types.js
+++ b/native/redux/state-types.js
@@ -1,59 +1,60 @@
// @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 { 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,
+ +commServicesAccessToken: ?string,
};
diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js
index 83d7be987..08b0693b0 100644
--- a/web/redux/redux-setup.js
+++ b/web/redux/redux-setup.js
@@ -1,318 +1,319 @@
// @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,
+ +commServicesAccessToken: ?string,
};
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;
}
diff --git a/web/root.js b/web/root.js
index ef68535e6..5145e63a7 100644
--- a/web/root.js
+++ b/web/root.js
@@ -1,77 +1,78 @@
// @flow
import * as React from 'react';
import { Provider } from 'react-redux';
import { Router, Route } from 'react-router';
import { createStore, applyMiddleware, type Store } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction.js';
import { createMigrate, persistReducer, persistStore } from 'redux-persist';
import { PersistGate } from 'redux-persist/es/integration/react.js';
import storage from 'redux-persist/es/storage/index.js';
import thunk from 'redux-thunk';
import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js';
import { isDev } from 'lib/utils/dev-utils.js';
import App from './app.react.js';
import ErrorBoundary from './error-boundary.react.js';
import Loading from './loading.react.js';
import { reducer } from './redux/redux-setup.js';
import type { AppState, Action } from './redux/redux-setup.js';
import history from './router-history.js';
import Socket from './socket.react.js';
const migrations = {
[1]: state => {
const {
primaryIdentityPublicKey,
...stateWithoutPrimaryIdentityPublicKey
} = state;
return {
...stateWithoutPrimaryIdentityPublicKey,
cryptoStore: {
primaryAccount: null,
primaryIdentityKeys: null,
notificationAccount: null,
notificationIdentityKeys: null,
},
};
},
};
const persistConfig = {
key: 'root',
storage,
whitelist: [
'enabledApps',
'deviceID',
'draftStore',
'cryptoStore',
'notifPermissionAlertInfo',
+ 'commServicesAccessToken',
],
migrate: (createMigrate(migrations, { debug: isDev }): any),
version: 1,
};
declare var preloadedState: AppState;
const persistedReducer = persistReducer(persistConfig, reducer);
const store: Store = createStore(
persistedReducer,
preloadedState,
composeWithDevTools({})(applyMiddleware(thunk, reduxLoggerMiddleware)),
);
const persistor = persistStore(store);
const RootProvider = (): React.Node => (
}>
);
export default RootProvider;