diff --git a/lib/selectors/app-state-selectors.js b/lib/selectors/app-state-selectors.js new file mode 100644 index 000000000..0e833ac62 --- /dev/null +++ b/lib/selectors/app-state-selectors.js @@ -0,0 +1,12 @@ +// @flow + +import { useSelector } from '../utils/redux-utils.js'; + +function usePersistedStateLoaded(): boolean { + const rehydrateConcluded = useSelector(state => !!state._persist?.rehydrated); + const initialStateLoaded = useSelector(state => state.initialStateLoaded); + + return rehydrateConcluded && initialStateLoaded; +} + +export { usePersistedStateLoaded }; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js index 63c20617f..0a6ff0c88 100644 --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -1,1692 +1,1693 @@ // @flow import type { PersistState } from 'redux-persist/es/types'; import type { LogOutResult, KeyserverLogOutResult, LegacyLogInStartingPayload, LegacyLogInResult, LegacyRegisterResult, DefaultNotificationPayload, ClaimUsernameResponse, KeyserverAuthResult, } from './account-types.js'; import type { ActivityUpdateSuccessPayload, QueueActivityUpdatesPayload, SetThreadUnreadStatusPayload, } from './activity-types.js'; import type { AlertStore, RecordAlertActionPayload } from './alert-types.js'; import type { AuxUserStore, SetAuxUserFIDsPayload, AddAuxUserFIDsPayload, RemovePeerUsersPayload, SetPeerDeviceListsPayload, SetMissingDeviceListsPayload, } from './aux-user-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from './avatar-types.js'; import type { CommunityStore, AddCommunityPayload, FetchCommunityInfosResponse, FetchAllCommunityInfosWithNamesResponse, CreateOrUpdateFarcasterChannelTagResponse, DeleteFarcasterChannelTagPayload, } from './community-types.js'; import type { DBOpsStore } from './db-ops-types.js'; import type { GetVersionActionPayload, LastCommunicatedPlatformDetails, } from './device-types.js'; import type { ProcessDMOpsPayload, QueuedDMOperations, QueueDMOpsPayload, PruneDMOpsQueuePayload, ClearQueuedThreadDMOpsPayload, ClearQueuedMessageDMOpsPayload, ClearQueuedEntryDMOpsPayload, ClearQueuedMembershipDMOpsPayload, } from './dm-ops.js'; import type { DraftStore } from './draft-types.js'; import type { EnabledApps, SupportedApps } from './enabled-apps.js'; import type { RawEntryInfo, EntryStore, SaveEntryPayload, CreateEntryPayload, DeleteEntryResult, RestoreEntryPayload, FetchEntryInfosResult, CalendarQueryUpdateResult, CalendarQueryUpdateStartingPayload, FetchRevisionsForEntryPayload, } from './entry-types.js'; import type { CalendarFilter, CalendarThreadFilter, SetCalendarDeletedFilterPayload, } from './filter-types.js'; import type { HolderStore, BlobHashAndHolder } from './holder-types.js'; import type { IdentityAuthResult } from './identity-service-types'; import type { IntegrityStore } from './integrity-types.js'; import type { KeyserverStore, AddKeyserverPayload, RemoveKeyserverPayload, } from './keyserver-types.js'; import type { LifecycleState } from './lifecycle-state-types.js'; import type { FetchInviteLinksResponse, InviteLink, InviteLinksStore, InviteLinkVerificationResponse, DisableInviteLinkPayload, } from './link-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, SimpleMessagesPayload, FetchPinnedMessagesResult, SearchMessagesResponse, } from './message-types.js'; import type { RawReactionMessageInfo } from './messages/reaction.js'; import type { RawTextMessageInfo } from './messages/text.js'; import type { BaseNavInfo, WebNavInfo } from './nav-types.js'; import type { GetOlmSessionInitializationDataResponse } from './olm-session-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 { ProcessServerRequestAction } from './request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from './search-types.js'; import type { SetSessionPayload } from './session-types.js'; import type { ConnectionIssue, StateSyncFullActionPayload, StateSyncIncrementalActionPayload, SetActiveSessionRecoveryPayload, ClientStateSyncSocketResult, } from './socket-types.js'; import { type ClientStore } from './store-ops-types.js'; import type { SubscriptionUpdateResult } from './subscription-types.js'; import type { SyncedMetadataStore, SetSyncedMetadataEntryPayload, ClearSyncedMetadataEntryPayload, } from './synced-metadata-types.js'; import type { GlobalThemeInfo } from './theme-types.js'; import type { ThreadActivityStore } from './thread-activity-types.js'; import type { ThreadStore, ChangeThreadSettingsPayload, LeaveThreadPayload, NewThreadResult, ThreadJoinPayload, ToggleMessagePinResult, LegacyThreadStore, RoleModificationPayload, RoleDeletionPayload, } from './thread-types.js'; import type { TunnelbrokerDeviceToken } from './tunnelbroker-device-token-types.js'; import type { ClientUpdatesResultWithUserInfos } from './update-types.js'; import type { CurrentUserInfo, UserInfos, UserStore, UserInfo, } from './user-types.js'; import type { SetDeviceTokenActionPayload, SetDeviceTokenStartedPayload, } from '../actions/device-actions.js'; import type { ProcessHoldersStartedPayload, ProcessHoldersFailedPayload, ProcessHoldersFinishedPayload, } from '../actions/holder-actions.js'; import type { UpdateConnectionStatusPayload, SetLateResponsePayload, UpdateKeyserverReachabilityPayload, } from '../keyserver-conn/keyserver-conn-types.js'; export type BaseAppState = { +navInfo: NavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +alertStore: AlertStore, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +dataLoaded: boolean, +userPolicies: UserPolicies, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +integrityStore: IntegrityStore, +globalThemeInfo: GlobalThemeInfo, +customServer: ?string, +communityStore: CommunityStore, +dbOpsStore: DBOpsStore, +syncedMetadataStore: SyncedMetadataStore, +auxUserStore: AuxUserStore, +tunnelbrokerDeviceToken: TunnelbrokerDeviceToken, +_persist: ?PersistState, +queuedDMOperations: QueuedDMOperations, +holderStore: HolderStore, + +initialStateLoaded: boolean, ... }; export type NativeAppState = BaseAppState<>; export type WebAppState = BaseAppState<> & { +pushApiPublicKey: ?string, ... }; export type AppState = NativeAppState | WebAppState; export type ClientWebInitialReduxStateResponse = { +navInfo: WebNavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: ThreadStore, +userInfos: UserInfos, +messageStore: MessageStore, +pushApiPublicKey: ?string, +inviteLinksStore: InviteLinksStore, +keyserverInfo: WebInitialKeyserverInfo, }; export type ServerWebInitialReduxStateResponse = { +navInfo: WebNavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: LegacyThreadStore, +userInfos: UserInfos, +messageStore: MessageStore, +pushApiPublicKey: ?string, +inviteLinksStore: InviteLinksStore, +keyserverInfo: WebInitialKeyserverInfo, }; export type WebInitialKeyserverInfo = { +sessionID: ?string, +updatesCurrentAsOf: number, }; export type BaseAction = $ReadOnly<{ +dispatchMetadata?: DispatchMetadata, ... | { +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: 'CLAIM_USERNAME_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_SUCCESS', +payload: ClaimUsernameResponse, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_SUCCESS', +payload: KeyserverLogOutResult, +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: 'IDENTITY_LOG_IN_STARTED', +loadingInfo: LoadingInfo, +payload?: void, } | { +type: 'IDENTITY_LOG_IN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_LOG_IN_SUCCESS', +payload: IdentityAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_AUTH_STARTED', +loadingInfo: LoadingInfo, +payload?: void, } | { +type: 'KEYSERVER_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_AUTH_SUCCESS', +payload: KeyserverAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_LOG_IN_STARTED', +loadingInfo: LoadingInfo, +payload: LegacyLogInStartingPayload, } | { +type: 'LEGACY_LOG_IN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_LOG_IN_SUCCESS', +payload: LegacyLogInResult, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_KEYSERVER_REGISTER_STARTED', +loadingInfo: LoadingInfo, +payload: LegacyLogInStartingPayload, } | { +type: 'LEGACY_KEYSERVER_REGISTER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_KEYSERVER_REGISTER_SUCCESS', +payload: LegacyRegisterResult, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_SUCCESS', +payload: IdentityAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_IDENTITY_USER_PASSWORD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_IDENTITY_USER_PASSWORD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_IDENTITY_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: FetchRevisionsForEntryPayload, +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, +failedOutboundP2PMessageIDs?: $ReadOnlyArray, }, +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, +failedOutboundP2PMessageIDs?: $ReadOnlyArray, }, +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: 'EXACT_SEARCH_USER_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_SUCCESS', +payload: ExactUserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_DRAFT', +payload: { +key: string, +text: string, }, } | { +type: 'MOVE_DRAFT', +payload: { +oldKey: string, +newKey: string, }, } | { +type: 'SET_CLIENT_DB_STORE', +payload: ClientStore, } | { +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: SetDeviceTokenStartedPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_SUCCESS', +payload: SetDeviceTokenActionPayload, +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, } | ProcessServerRequestAction | { +type: 'UPDATE_CONNECTION_STATUS', +payload: UpdateConnectionStatusPayload, } | { +type: 'QUEUE_ACTIVITY_UPDATES', +payload: QueueActivityUpdatesPayload, } | { +type: 'UNSUPERVISED_BACKGROUND', +payload: { +keyserverID: string }, } | { +type: 'UPDATE_LIFECYCLE_STATE', +payload: LifecycleState, } | { +type: 'ENABLE_APP', +payload: SupportedApps, } | { +type: 'DISABLE_APP', +payload: SupportedApps, } | { +type: 'UPDATE_REPORTS_ENABLED', +payload: Partial, } | { +type: 'PROCESS_UPDATES', +payload: ClientUpdatesResultWithUserInfos, } | { +type: 'PROCESS_MESSAGES', +payload: NewMessagesPayload, } | { +type: 'MESSAGE_STORE_PRUNE', +payload: MessageStorePrunePayload, } | { +type: 'SET_LATE_RESPONSE', +payload: SetLateResponsePayload, } | { +type: 'UPDATE_KEYSERVER_REACHABILITY', +payload: UpdateKeyserverReachabilityPayload, } | { +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: 'LEGACY_SIWE_AUTH_STARTED', +payload: LegacyLogInStartingPayload, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_SIWE_AUTH_SUCCESS', +payload: LegacyLogInResult, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_SIWE_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RECORD_ALERT', +payload: RecordAlertActionPayload, } | { +type: 'UPDATE_USER_AVATAR_STARTED', +payload: UpdateUserAvatarRequest, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_SUCCESS', +payload: UpdateUserAvatarResponse, +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, } | { +type: 'TOGGLE_MESSAGE_PIN_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'TOGGLE_MESSAGE_PIN_SUCCESS', +payload: ToggleMessagePinResult, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PINNED_MESSAGES_SUCCESS', +payload: FetchPinnedMessagesResult, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'VERIFY_INVITE_LINK_SUCCESS', +payload: InviteLinkVerificationResponse, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', +payload: FetchInviteLinksResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CALENDAR_COMMUNITY_FILTER', +payload: void, } | { +type: 'UPDATE_CHAT_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CHAT_COMMUNITY_FILTER', +payload: void, } | { +type: 'SEARCH_MESSAGES_STARTED', +payload: void, +loadingInfo?: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_SUCCESS', +payload: SearchMessagesResponse, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', +payload: InviteLink, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DISABLE_INVITE_LINK_SUCCESS', +payload: DisableInviteLinkPayload, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', +payload: GetOlmSessionInitializationDataResponse, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DATA_LOADED', +payload: { +dataLoaded: boolean, }, } | { +type: 'GET_VERSION_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_VERSION_SUCCESS', +payload: GetVersionActionPayload, +loadingInfo: LoadingInfo, } | { +type: 'GET_VERSION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_LAST_COMMUNICATED_PLATFORM_DETAILS', +payload: LastCommunicatedPlatformDetails, } | { +type: 'RESET_USER_STATE', +payload?: void } | { +type: 'MODIFY_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'MODIFY_COMMUNITY_ROLE_SUCCESS', +payload: RoleModificationPayload, +loadingInfo: LoadingInfo, } | { +type: 'MODIFY_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DELETE_COMMUNITY_ROLE_SUCCESS', +payload: RoleDeletionPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_THREAD_LAST_NAVIGATED', +payload: { +threadID: string, +time: number }, } | { +type: 'UPDATE_INTEGRITY_STORE', +payload: { +threadIDsToHash?: $ReadOnlyArray, +threadHashingStatus?: 'starting' | 'running' | 'completed', }, } | { +type: 'UPDATE_THEME_INFO', +payload: Partial, } | { +type: 'ADD_KEYSERVER', +payload: AddKeyserverPayload, } | { +type: 'REMOVE_KEYSERVER', +payload: RemoveKeyserverPayload, } | { +type: 'SET_CUSTOM_SERVER', +payload: string, } | { +type: 'SET_CONNECTION_ISSUE', +payload: { +connectionIssue: ?ConnectionIssue, +keyserverID: string }, } | { +type: 'ADD_COMMUNITY', +payload: AddCommunityPayload, } | { +type: 'SET_SYNCED_METADATA_ENTRY', +payload: SetSyncedMetadataEntryPayload, } | { +type: 'CLEAR_SYNCED_METADATA_ENTRY', +payload: ClearSyncedMetadataEntryPayload, } | { +type: 'SET_ACTIVE_SESSION_RECOVERY', +payload: SetActiveSessionRecoveryPayload, } | { +type: 'SET_AUX_USER_FIDS', +payload: SetAuxUserFIDsPayload, } | { +type: 'ADD_AUX_USER_FIDS', +payload: AddAuxUserFIDsPayload, } | { +type: 'CLEAR_AUX_USER_FIDS', +payload?: void, } | { +type: 'REMOVE_PEER_USERS', +payload: RemovePeerUsersPayload, } | { +type: 'SET_PEER_DEVICE_LISTS', +payload: SetPeerDeviceListsPayload, } | { +type: 'OPS_PROCESSING_FINISHED_ACTION_TYPE', +payload?: void, } | { +type: 'FETCH_COMMUNITY_INFOS_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_COMMUNITY_INFOS_SUCCESS', +payload: FetchCommunityInfosResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_COMMUNITY_INFOS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ALL_COMMUNITY_INFOS_WITH_NAMES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_ALL_COMMUNITY_INFOS_WITH_NAMES_SUCCESS', +payload: FetchAllCommunityInfosWithNamesResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ALL_COMMUNITY_INFOS_WITH_NAMES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_SUCCESS', +payload: CreateOrUpdateFarcasterChannelTagResponse, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_FARCASTER_CHANNEL_TAG_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DELETE_FARCASTER_CHANNEL_TAG_SUCCESS', +payload: DeleteFarcasterChannelTagPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_FARCASTER_CHANNEL_TAG_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'PROCESS_NEW_USER_IDS', +payload: { +userIDs: $ReadOnlyArray, }, +loadingInfo: LoadingInfo, } | { +type: 'FIND_USER_IDENTITIES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FIND_USER_IDENTITIES_SUCCESS', +payload: { +userInfos: $ReadOnlyArray, }, +loadingInfo: LoadingInfo, } | { +type: 'FIND_USER_IDENTITIES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'VERSION_SUPPORTED_BY_IDENTITY_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'VERSION_SUPPORTED_BY_IDENTITY_SUCCESS', +payload: { +supported: boolean, }, +loadingInfo: LoadingInfo, } | { +type: 'VERSION_SUPPORTED_BY_IDENTITY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PENDING_UPDATES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PENDING_UPDATES_SUCCESS', +payload: ClientStateSyncSocketResult, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PENDING_UPDATES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_TB_DEVICE_TOKEN_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'SET_TB_DEVICE_TOKEN_SUCCESS', +payload: { +deviceToken: string, }, +loadingInfo: LoadingInfo, } | { +type: 'SET_TB_DEVICE_TOKEN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'PROCESS_DM_OPS', +payload: ProcessDMOpsPayload, } | { +type: 'INVALIDATE_TUNNELBROKER_DEVICE_TOKEN', +payload: { +deviceToken: string, }, } | { +type: 'QUEUE_DM_OPS', +payload: QueueDMOpsPayload } | { +type: 'PRUNE_DM_OPS_QUEUE', +payload: PruneDMOpsQueuePayload } | { +type: 'CLEAR_QUEUED_THREAD_DM_OPS', +payload: ClearQueuedThreadDMOpsPayload, } | { +type: 'CLEAR_QUEUED_MESSAGE_DM_OPS', +payload: ClearQueuedMessageDMOpsPayload, } | { +type: 'CLEAR_QUEUED_ENTRY_DM_OPS', +payload: ClearQueuedEntryDMOpsPayload, } | { +type: 'CLEAR_QUEUED_MEMBERSHIP_DM_OPS', +payload: ClearQueuedMembershipDMOpsPayload, } | { +type: 'STORE_ESTABLISHED_HOLDER', +payload: BlobHashAndHolder, } | { +type: 'PROCESS_HOLDERS_STARTED', +payload: ProcessHoldersStartedPayload, +loadingInfo: LoadingInfo, } | { +type: 'PROCESS_HOLDERS_FAILED', +error: true, +payload: Error & ProcessHoldersFailedPayload, +loadingInfo: LoadingInfo, } | { +type: 'PROCESS_HOLDERS_SUCCESS', +payload: ProcessHoldersFinishedPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_MISSING_DEVICE_LISTS', +payload: SetMissingDeviceListsPayload, }, }>; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); export type DispatchSource = 'tunnelbroker' | 'tab-sync'; // Data added when dispatching action as a result of message received // from other peer. It is used to send processing confirmation. export type InboundActionMetadata = { +messageID: string, +senderDeviceID: string, }; // Data added when dispatching action triggered locally, used to resolve // promise associated with sending messages and track failed messages. export type OutboundActionMetadata = { +dmOpID: string, }; export type DispatchMetadata = InboundActionMetadata | OutboundActionMetadata; export type SuperAction = { +type: string, +payload?: ActionPayload, +loadingInfo?: LoadingInfo, +error?: boolean, +dispatchSource?: DispatchSource, +dispatchMetadata?: DispatchMetadata, }; 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/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js index adf96643b..93e0c8447 100644 --- a/native/account/logged-out-modal.react.js +++ b/native/account/logged-out-modal.react.js @@ -1,657 +1,657 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome.js'; import * as React from 'react'; import { View, Text, TouchableOpacity, Image, Keyboard, Platform, BackHandler, ActivityIndicator, } from 'react-native'; import { Easing, useSharedValue, withTiming, useAnimatedStyle, runOnJS, } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useIsLoggedInToAuthoritativeKeyserver } from 'lib/hooks/account-hooks.js'; import { setActiveSessionRecoveryActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; +import { usePersistedStateLoaded } from 'lib/selectors/app-state-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { recoveryFromReduxActionSources } from 'lib/types/account-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { splashBackgroundURI } from './background-info.js'; import FullscreenSIWEPanel from './fullscreen-siwe-panel.react.js'; import LogInPanel from './log-in-panel.react.js'; import type { LogInState } from './log-in-panel.react.js'; import LoggedOutStaffInfo from './logged-out-staff-info.react.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import KeyboardAvoidingView from '../components/keyboard-avoiding-view.react.js'; import ConnectedStatusBar from '../connected-status-bar.react.js'; import { useRatchetingKeyboardHeight } from '../keyboard/animated-keyboard.js'; import { createIsForegroundSelector } from '../navigation/nav-selectors.js'; import { NavContext } from '../navigation/navigation-context.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import { type NavigationRoute, LoggedOutModalRouteName, RegistrationRouteName, QRCodeSignInNavigatorRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; -import { usePersistedStateLoaded } from '../selectors/app-state-selectors.js'; import { derivedDimensionsInfoSelector } from '../selectors/dimensions-selectors.js'; import { splashStyleSelector } from '../splash.js'; import { useStyles } from '../themes/colors.js'; import { AnimatedView } from '../types/styles.js'; import EthereumLogo from '../vectors/ethereum-logo.react.js'; let initialAppLoad = true; const safeAreaEdges = ['top', 'bottom']; export type LoggedOutMode = 'loading' | 'prompt' | 'log-in' | 'siwe'; const timingConfig = { duration: 250, easing: Easing.out(Easing.ease), }; // prettier-ignore function getPanelPaddingTop( modeValue /*: string */, keyboardHeightValue /*: number */, contentHeightValue /*: number */, ) /*: number */ { 'worklet'; const headerHeight = Platform.OS === 'ios' ? 62.33 : 58.54; let containerSize = headerHeight; if (modeValue === 'loading' || modeValue === 'prompt') { containerSize += Platform.OS === 'ios' ? 40 : 61; } else if (modeValue === 'log-in') { containerSize += 140; } else if (modeValue === 'siwe') { containerSize += 250; } const freeSpace = contentHeightValue - keyboardHeightValue - containerSize; const targetPanelPaddingTop = Math.max(freeSpace, 0) / 2; return withTiming(targetPanelPaddingTop, timingConfig); } // prettier-ignore function getPanelOpacity( modeValue /*: string */, finishResettingToPrompt/*: () => void */, ) /*: number */ { 'worklet'; const targetPanelOpacity = modeValue === 'loading' || modeValue === 'prompt' ? 0 : 1; return withTiming( targetPanelOpacity, timingConfig, (succeeded /*?: boolean */) => { if (succeeded && targetPanelOpacity === 0) { runOnJS(finishResettingToPrompt)(); } }, ); } const unboundStyles = { animationContainer: { flex: 1, }, backButton: { position: 'absolute', top: 13, }, button: { borderRadius: 4, marginBottom: 4, marginTop: 4, marginLeft: 4, marginRight: 4, paddingBottom: 14, paddingLeft: 18, paddingRight: 18, paddingTop: 14, flex: 1, }, buttonContainer: { bottom: 0, left: 0, marginLeft: 26, marginRight: 26, paddingBottom: 20, position: 'absolute', right: 0, }, buttonText: { fontFamily: 'OpenSans-Semibold', fontSize: 17, textAlign: 'center', }, classicAuthButton: { backgroundColor: 'purpleButton', }, classicAuthButtonText: { color: 'whiteText', }, registerButtons: { flexDirection: 'row', }, signInButtons: { flexDirection: 'row', }, container: { backgroundColor: 'transparent', flex: 1, }, header: { color: 'white', fontFamily: Platform.OS === 'ios' ? 'IBMPlexSans' : 'IBMPlexSans-Medium', fontSize: 56, fontWeight: '500', lineHeight: 66, textAlign: 'center', }, loadingIndicator: { paddingTop: 15, }, modalBackground: { bottom: 0, left: 0, position: 'absolute', right: 0, top: 0, }, siweButton: { backgroundColor: 'siweButton', flex: 1, flexDirection: 'row', justifyContent: 'center', }, siweButtonText: { color: 'siweButtonText', }, siweOr: { flex: 1, flexDirection: 'row', marginBottom: 18, marginTop: 14, }, siweOrLeftHR: { borderColor: 'logInSpacer', borderTopWidth: 1, flex: 1, marginRight: 18, marginTop: 10, }, siweOrRightHR: { borderColor: 'logInSpacer', borderTopWidth: 1, flex: 1, marginLeft: 18, marginTop: 10, }, siweOrText: { color: 'whiteText', fontSize: 17, textAlign: 'center', }, siweIcon: { paddingRight: 10, }, }; const isForegroundSelector = createIsForegroundSelector( LoggedOutModalRouteName, ); const backgroundSource = { uri: splashBackgroundURI }; const initialLogInState = { usernameInputText: null, passwordInputText: null, }; type Mode = { +curMode: LoggedOutMode, +nextMode: LoggedOutMode, }; type Props = { +navigation: RootNavigationProp<'LoggedOutModal'>, +route: NavigationRoute<'LoggedOutModal'>, }; function LoggedOutModal(props: Props) { const mountedRef = React.useRef(false); React.useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const [logInState, baseSetLogInState] = React.useState(initialLogInState); const setLogInState = React.useCallback( (newLogInState: Partial) => { if (!mountedRef.current) { return; } baseSetLogInState(prevLogInState => ({ ...prevLogInState, ...newLogInState, })); }, [], ); const logInStateContainer = React.useMemo( () => ({ state: logInState, setState: setLogInState, }), [logInState, setLogInState], ); const persistedStateLoaded = usePersistedStateLoaded(); const initialMode = persistedStateLoaded ? 'prompt' : 'loading'; const [mode, baseSetMode] = React.useState(() => ({ curMode: initialMode, nextMode: initialMode, })); const setMode = React.useCallback((newMode: Partial) => { if (!mountedRef.current) { return; } baseSetMode(prevMode => ({ ...prevMode, ...newMode, })); }, []); const nextModeRef = React.useRef(initialMode); const dimensions = useSelector(derivedDimensionsInfoSelector); const contentHeight = useSharedValue(dimensions.safeAreaHeight); const modeValue = useSharedValue(initialMode); const buttonOpacity = useSharedValue(persistedStateLoaded ? 1 : 0); const onPrompt = mode.curMode === 'prompt'; const prevOnPromptRef = React.useRef(onPrompt); React.useEffect(() => { if (onPrompt && !prevOnPromptRef.current) { buttonOpacity.value = withTiming(1, { easing: Easing.out(Easing.ease), }); } prevOnPromptRef.current = onPrompt; }, [onPrompt, buttonOpacity]); const curContentHeight = dimensions.safeAreaHeight; const prevContentHeightRef = React.useRef(curContentHeight); React.useEffect(() => { if (curContentHeight === prevContentHeightRef.current) { return; } prevContentHeightRef.current = curContentHeight; contentHeight.value = curContentHeight; }, [curContentHeight, contentHeight]); const combinedSetMode = React.useCallback( (newMode: LoggedOutMode) => { nextModeRef.current = newMode; setMode({ curMode: newMode, nextMode: newMode }); modeValue.value = newMode; }, [setMode, modeValue], ); const goBackToPrompt = React.useCallback(() => { nextModeRef.current = 'prompt'; setMode({ nextMode: 'prompt' }); modeValue.value = 'prompt'; Keyboard.dismiss(); }, [setMode, modeValue]); const loadingCompleteRef = React.useRef(persistedStateLoaded); React.useEffect(() => { if (!loadingCompleteRef.current && persistedStateLoaded) { combinedSetMode('prompt'); loadingCompleteRef.current = true; } }, [persistedStateLoaded, combinedSetMode]); const [activeAlert, setActiveAlert] = React.useState(false); const navContext = React.useContext(NavContext); const isForeground = isForegroundSelector(navContext); const ratchetingKeyboardHeightInput = React.useMemo( () => ({ ignoreKeyboardDismissal: activeAlert, disabled: !isForeground, }), [activeAlert, isForeground], ); const keyboardHeightValue = useRatchetingKeyboardHeight( ratchetingKeyboardHeightInput, ); // We remove the password from the TextInput on iOS before dismissing it, // because otherwise iOS will prompt the user to save the password if the // iCloud password manager is enabled. We'll put the password back after the // dismissal concludes. const temporarilyHiddenPassword = React.useRef(); const curLogInPassword = logInState.passwordInputText; const resetToPrompt = React.useCallback(() => { if (nextModeRef.current === 'prompt') { return false; } if (Platform.OS === 'ios' && curLogInPassword) { temporarilyHiddenPassword.current = curLogInPassword; setLogInState({ passwordInputText: null }); } goBackToPrompt(); return true; }, [goBackToPrompt, curLogInPassword, setLogInState]); const finishResettingToPrompt = React.useCallback(() => { setMode({ curMode: nextModeRef.current }); if (temporarilyHiddenPassword.current) { setLogInState({ passwordInputText: temporarilyHiddenPassword.current }); temporarilyHiddenPassword.current = null; } }, [setMode, setLogInState]); React.useEffect(() => { if (!isForeground) { return undefined; } BackHandler.addEventListener('hardwareBackPress', resetToPrompt); return () => { BackHandler.removeEventListener('hardwareBackPress', resetToPrompt); }; }, [isForeground, resetToPrompt]); const rehydrateConcluded = useSelector( state => !!(state._persist && state._persist.rehydrated && navContext), ); const isLoggedInToAuthKeyserver = useIsLoggedInToAuthoritativeKeyserver(); const loggedIn = useSelector(isLoggedIn); const dispatch = useDispatch(); React.useEffect(() => { // This gets triggered when an app is killed and restarted // Not when it is returned from being backgrounded if (!initialAppLoad || !rehydrateConcluded) { return; } initialAppLoad = false; if (usingCommServicesAccessToken || __DEV__) { return; } if (loggedIn === isLoggedInToAuthKeyserver) { return; } const actionSource = loggedIn ? recoveryFromReduxActionSources.appStartReduxLoggedInButInvalidCookie : recoveryFromReduxActionSources.appStartCookieLoggedInButInvalidRedux; dispatch({ type: setActiveSessionRecoveryActionType, payload: { activeSessionRecovery: actionSource, keyserverID: authoritativeKeyserverID, }, }); }, [rehydrateConcluded, loggedIn, isLoggedInToAuthKeyserver, dispatch]); const onPressSIWE = React.useCallback(() => { combinedSetMode('siwe'); }, [combinedSetMode]); const onPressLogIn = React.useCallback(() => { combinedSetMode('log-in'); }, [combinedSetMode]); const { navigate } = props.navigation; const onPressQRCodeSignIn = React.useCallback(() => { navigate(QRCodeSignInNavigatorRouteName); }, [navigate]); const onPressNewRegister = React.useCallback(() => { navigate(RegistrationRouteName); }, [navigate]); const opacityStyle = useAnimatedStyle(() => ({ opacity: getPanelOpacity(modeValue.value, finishResettingToPrompt), })); const styles = useStyles(unboundStyles); const panel = React.useMemo(() => { if (mode.curMode === 'log-in') { return ( ); } else if (mode.curMode === 'loading') { return ( ); } return null; }, [ mode.curMode, setActiveAlert, opacityStyle, logInStateContainer, styles.loadingIndicator, ]); const classicAuthButtonStyle = React.useMemo( () => [styles.button, styles.classicAuthButton], [styles.button, styles.classicAuthButton], ); const classicAuthButtonTextStyle = React.useMemo( () => [styles.buttonText, styles.classicAuthButtonText], [styles.buttonText, styles.classicAuthButtonText], ); const siweAuthButtonStyle = React.useMemo( () => [styles.button, styles.siweButton], [styles.button, styles.siweButton], ); const siweAuthButtonTextStyle = React.useMemo( () => [styles.buttonText, styles.siweButtonText], [styles.buttonText, styles.siweButtonText], ); const buttonsViewOpacity = useAnimatedStyle(() => ({ opacity: buttonOpacity.value, })); const buttonsViewStyle = React.useMemo( () => [styles.buttonContainer, buttonsViewOpacity], [styles.buttonContainer, buttonsViewOpacity], ); const buttons = React.useMemo(() => { if (mode.curMode !== 'prompt') { return null; } const signInButtons = []; signInButtons.push( Sign in , ); if (__DEV__) { signInButtons.push( Sign in (QR) , ); } return ( Sign in with Ethereum or {signInButtons} Register ); }, [ mode.curMode, onPressNewRegister, onPressLogIn, onPressQRCodeSignIn, onPressSIWE, classicAuthButtonStyle, classicAuthButtonTextStyle, siweAuthButtonStyle, siweAuthButtonTextStyle, buttonsViewStyle, styles.siweIcon, styles.siweOr, styles.siweOrLeftHR, styles.siweOrText, styles.siweOrRightHR, styles.signInButtons, styles.registerButtons, ]); const windowWidth = dimensions.width; const backButtonStyle = React.useMemo( () => [ styles.backButton, opacityStyle, { left: windowWidth < 360 ? 28 : 40 }, ], [styles.backButton, opacityStyle, windowWidth], ); const paddingTopStyle = useAnimatedStyle(() => ({ paddingTop: getPanelPaddingTop( modeValue.value, keyboardHeightValue.value, contentHeight.value, ), })); const animatedContentStyle = React.useMemo( () => [styles.animationContainer, paddingTopStyle], [styles.animationContainer, paddingTopStyle], ); const animatedContent = React.useMemo( () => ( Comm {panel} ), [ animatedContentStyle, styles.header, backButtonStyle, resetToPrompt, panel, ], ); const curModeIsSIWE = mode.curMode === 'siwe'; const nextModeIsPrompt = mode.nextMode === 'prompt'; const siwePanel = React.useMemo(() => { if (!curModeIsSIWE) { return null; } return ( ); }, [curModeIsSIWE, goBackToPrompt, nextModeIsPrompt]); const splashStyle = useSelector(splashStyleSelector); const backgroundStyle = React.useMemo( () => [styles.modalBackground, splashStyle], [styles.modalBackground, splashStyle], ); return React.useMemo( () => ( <> {animatedContent} {buttons} {siwePanel} ), [backgroundStyle, styles.container, animatedContent, buttons, siwePanel], ); } const MemoizedLoggedOutModal: React.ComponentType = React.memo(LoggedOutModal); export default MemoizedLoggedOutModal; diff --git a/native/components/persisted-state-gate.js b/native/components/persisted-state-gate.js index 88184b0c1..80c2e6c78 100644 --- a/native/components/persisted-state-gate.js +++ b/native/components/persisted-state-gate.js @@ -1,16 +1,16 @@ // @flow import * as React from 'react'; -import { usePersistedStateLoaded } from '../selectors/app-state-selectors.js'; +import { usePersistedStateLoaded } from 'lib/selectors/app-state-selectors.js'; function PersistedStateGate(props: { +children: React.Node }): React.Node { const persistedStateLoaded = usePersistedStateLoaded(); if (!persistedStateLoaded) { return null; } return props.children; } export default PersistedStateGate; diff --git a/native/data/sqlite-data-handler.js b/native/data/sqlite-data-handler.js index b04eb1c47..0fbc3b75c 100644 --- a/native/data/sqlite-data-handler.js +++ b/native/data/sqlite-data-handler.js @@ -1,329 +1,329 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { MediaCacheContext } from 'lib/components/media-cache-provider.react.js'; import type { CallSingleKeyserverEndpoint } from 'lib/keyserver-conn/call-single-keyserver-endpoint.js'; import type { CallKeyserverEndpoint } from 'lib/keyserver-conn/keyserver-conn-types.js'; import { useKeyserverRecoveryLogIn } from 'lib/keyserver-conn/recovery-utils.js'; import { auxUserStoreOpsHandlers } from 'lib/ops/aux-user-store-ops.js'; import { communityStoreOpsHandlers } from 'lib/ops/community-store-ops.js'; import { entryStoreOpsHandlers } from 'lib/ops/entries-store-ops.js'; import { integrityStoreOpsHandlers } from 'lib/ops/integrity-store-ops.js'; import { keyserverStoreOpsHandlers } from 'lib/ops/keyserver-store-ops.js'; import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.js'; import { syncedMetadataStoreOpsHandlers } from 'lib/ops/synced-metadata-store-ops.js'; import { threadActivityStoreOpsHandlers } from 'lib/ops/thread-activity-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { userStoreOpsHandlers } from 'lib/ops/user-store-ops.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import { shouldClearData } from 'lib/shared/data-utils.js'; import { recoveryFromDataHandlerActionSources, type RecoveryFromDataHandlerActionSource, } from 'lib/types/account-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { translateClientDBLocalMessageInfos } from 'lib/utils/message-ops-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { supportingMultipleKeyservers } from 'lib/utils/services-utils.js'; import { reportDatabaseDeleted } from 'lib/utils/wait-until-db-deleted.js'; import { resolveKeyserverSessionInvalidationUsingNativeCredentials } from '../account/legacy-recover-keyserver-session.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { filesystemMediaCache } from '../media/media-cache.js'; import { commCoreModule } from '../native-modules.js'; import { setStoreLoadedActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; import Alert from '../utils/alert.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; async function clearSensitiveData() { try { await commCoreModule.clearSensitiveData(); reportDatabaseDeleted(); } catch (error) { console.log( `Error clearing SQLite database: ${ getMessageForException(error) ?? 'unknown' }`, ); throw error; } try { await filesystemMediaCache.clearCache(); } catch { throw new Error('clear_media_cache_failed'); } } const returnsFalseSinceDoesntNeedToSupportCancellation = () => false; function SQLiteDataHandler(): React.Node { - const storeLoaded = useSelector(state => state.storeLoaded); + const initialStateLoaded = useSelector(state => state.initialStateLoaded); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const rehydrateConcluded = useSelector( state => !!(state._persist && state._persist.rehydrated), ); const staffCanSee = useStaffCanSee(); const loggedIn = useSelector(isLoggedIn); const currentLoggedInUserID = useSelector(state => state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id, ); const mediaCacheContext = React.useContext(MediaCacheContext); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); const keyserverRecoveryLogIn = useKeyserverRecoveryLogIn( authoritativeKeyserverID, ); const recoverDataFromAuthoritativeKeyserver = React.useCallback( async (source: RecoveryFromDataHandlerActionSource) => { const innerRecoverDataFromAuthoritativeKeyserver = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, ) => resolveKeyserverSessionInvalidationUsingNativeCredentials( callSingleKeyserverEndpoint, callKeyserverEndpoint, dispatchActionPromise, source, authoritativeKeyserverID, getInitialNotificationsEncryptedMessage, returnsFalseSinceDoesntNeedToSupportCancellation, ); try { await keyserverRecoveryLogIn( source, innerRecoverDataFromAuthoritativeKeyserver, returnsFalseSinceDoesntNeedToSupportCancellation, ); dispatch({ type: setStoreLoadedActionType }); } catch (fetchCookieException) { if (staffCanSee) { Alert.alert( `Error fetching new cookie from native credentials: ${ getMessageForException(fetchCookieException) ?? '{no exception message}' }. Please kill the app.`, ); } else { commCoreModule.terminate(); } } }, [ dispatch, dispatchActionPromise, keyserverRecoveryLogIn, staffCanSee, getInitialNotificationsEncryptedMessage, ], ); const recoverData = React.useCallback( (source: RecoveryFromDataHandlerActionSource) => { if (supportingMultipleKeyservers) { invariant( false, 'recoverData in SQLiteDataHandler is not yet implemented when ' + 'supportingMultipleKeyservers is enabled. It should recover ' + 'from broken SQLite state by restoring from backup service', ); } return recoverDataFromAuthoritativeKeyserver(source); }, [recoverDataFromAuthoritativeKeyserver], ); const callClearSensitiveData = React.useCallback( async (triggeredBy: string) => { await clearSensitiveData(); console.log(`SQLite database deletion was triggered by ${triggeredBy}`); }, [], ); const handleSensitiveData = React.useCallback(async () => { try { let sqliteStampedUserID, errorGettingStampedUserID = false; try { sqliteStampedUserID = await commCoreModule.getSQLiteStampedUserID(); } catch (error) { errorGettingStampedUserID = true; console.log( `Error getting SQLite stamped user ID: ${ getMessageForException(error) ?? 'unknown' }`, ); } if ( errorGettingStampedUserID || shouldClearData(sqliteStampedUserID, currentLoggedInUserID) ) { await callClearSensitiveData('change in logged-in user credentials'); } if ( currentLoggedInUserID && currentLoggedInUserID !== sqliteStampedUserID ) { await commCoreModule.stampSQLiteDBUserID(currentLoggedInUserID); } } catch (e) { if (isTaskCancelledError(e)) { return; } if (__DEV__) { throw e; } console.log(e); if (e.message !== 'clear_media_cache_failed') { commCoreModule.terminate(); } } }, [callClearSensitiveData, currentLoggedInUserID]); React.useEffect(() => { if (!rehydrateConcluded) { return; } const databaseNeedsDeletion = commCoreModule.checkIfDatabaseNeedsDeletion(); if (databaseNeedsDeletion) { void (async () => { try { await callClearSensitiveData('detecting corrupted database'); } catch (e) { if (__DEV__) { throw e; } console.log(e); if (e.message !== 'clear_media_cache_failed') { commCoreModule.terminate(); } } await recoverData( recoveryFromDataHandlerActionSources.corruptedDatabaseDeletion, ); })(); return; } const sensitiveDataHandled = handleSensitiveData(); - if (storeLoaded) { + if (initialStateLoaded) { return; } if (!loggedIn) { dispatch({ type: setStoreLoadedActionType }); return; } void (async () => { await Promise.all([ sensitiveDataHandled, mediaCacheContext?.evictCache(), ]); try { const { threads, messages, drafts, messageStoreThreads, reports, users, keyservers, communities, integrityThreadHashes, syncedMetadata, auxUserInfos, threadActivityEntries, entries, messageStoreLocalMessageInfos, } = await commCoreModule.getClientDBStore(); const threadInfosFromDB = threadStoreOpsHandlers.translateClientDBData(threads); const reportsFromDB = reportStoreOpsHandlers.translateClientDBData(reports); const usersFromDB = userStoreOpsHandlers.translateClientDBData(users); const keyserverInfosFromDB = keyserverStoreOpsHandlers.translateClientDBData(keyservers); const communityInfosFromDB = communityStoreOpsHandlers.translateClientDBData(communities); const threadHashesFromDB = integrityStoreOpsHandlers.translateClientDBData( integrityThreadHashes, ); const syncedMetadataFromDB = syncedMetadataStoreOpsHandlers.translateClientDBData(syncedMetadata); const auxUserInfosFromDB = auxUserStoreOpsHandlers.translateClientDBData(auxUserInfos); const threadActivityStoreFromDB = threadActivityStoreOpsHandlers.translateClientDBData( threadActivityEntries, ); const entriesFromDB = entryStoreOpsHandlers.translateClientDBData(entries); const localMessageInfosFromDB = translateClientDBLocalMessageInfos( messageStoreLocalMessageInfos, ); dispatch({ type: setClientDBStoreActionType, payload: { drafts, messages, threadStore: { threadInfos: threadInfosFromDB }, currentUserID: currentLoggedInUserID, messageStoreThreads, reports: reportsFromDB, users: usersFromDB, keyserverInfos: keyserverInfosFromDB, communities: communityInfosFromDB, threadHashes: threadHashesFromDB, syncedMetadata: syncedMetadataFromDB, auxUserInfos: auxUserInfosFromDB, threadActivityStore: threadActivityStoreFromDB, entries: entriesFromDB, messageStoreLocalMessageInfos: localMessageInfosFromDB, }, }); } catch (setStoreException) { if (isTaskCancelledError(setStoreException)) { dispatch({ type: setStoreLoadedActionType }); return; } if (staffCanSee) { Alert.alert( 'Error setting threadStore or messageStore', getMessageForException(setStoreException) ?? '{no exception message}', ); } await recoverData( recoveryFromDataHandlerActionSources.sqliteLoadFailure, ); } })(); }, [ currentLoggedInUserID, handleSensitiveData, loggedIn, dispatch, rehydrateConcluded, staffCanSee, - storeLoaded, + initialStateLoaded, recoverData, callClearSensitiveData, mediaCacheContext, ]); return null; } export { SQLiteDataHandler, clearSensitiveData }; diff --git a/native/navigation/app-navigator.react.js b/native/navigation/app-navigator.react.js index 12b988a94..af66efe38 100644 --- a/native/navigation/app-navigator.react.js +++ b/native/navigation/app-navigator.react.js @@ -1,186 +1,188 @@ // @flow import * as SplashScreen from 'expo-splash-screen'; import * as React from 'react'; import { PersistGate } from 'redux-persist/es/integration/react.js'; import ActionResultModal from './action-result-modal.react.js'; import { CommunityDrawerNavigator } from './community-drawer-navigator.react.js'; import CommunityDrawerTip from './community-drawer-tip.react.js'; import HomeTabTip from './home-tab-tip.react.js'; import IntroTip from './intro-tip.react.js'; import MutedTabTip from './muted-tab-tip.react.js'; import NUXTipOverlayBackdrop from './nux-tip-overlay-backdrop.react.js'; import { createOverlayNavigator } from './overlay-navigator.react.js'; import type { OverlayNavigationProp, OverlayNavigationHelpers, } from './overlay-navigator.react.js'; import type { RootNavigationProp } from './root-navigator.react.js'; import { HomeTabTipRouteName, CommunityDrawerTipRouteName, MutedTabTipRouteName, NUXTipOverlayBackdropRouteName, IntroTipRouteName, } from './route-names.js'; import { UserAvatarCameraModalRouteName, ThreadAvatarCameraModalRouteName, ImageModalRouteName, MultimediaMessageTooltipModalRouteName, ActionResultModalRouteName, TextMessageTooltipModalRouteName, ThreadSettingsMemberTooltipModalRouteName, UserRelationshipTooltipModalRouteName, RobotextMessageTooltipModalRouteName, ChatCameraModalRouteName, VideoPlaybackModalRouteName, CommunityDrawerNavigatorRouteName, type ScreenParamList, type OverlayParamList, TogglePinModalRouteName, } from './route-names.js'; import MultimediaMessageTooltipModal from '../chat/multimedia-message-tooltip-modal.react.js'; import RobotextMessageTooltipModal from '../chat/robotext-message-tooltip-modal.react.js'; import ThreadSettingsMemberTooltipModal from '../chat/settings/thread-settings-member-tooltip-modal.react.js'; import TextMessageTooltipModal from '../chat/text-message-tooltip-modal.react.js'; import TogglePinModal from '../chat/toggle-pin-modal.react.js'; import KeyboardStateContainer from '../keyboard/keyboard-state-container.react.js'; import ChatCameraModal from '../media/chat-camera-modal.react.js'; import ImageModal from '../media/image-modal.react.js'; import ThreadAvatarCameraModal from '../media/thread-avatar-camera-modal.react.js'; import UserAvatarCameraModal from '../media/user-avatar-camera-modal.react.js'; import VideoPlaybackModal from '../media/video-playback-modal.react.js'; import UserRelationshipTooltipModal from '../profile/user-relationship-tooltip-modal.react.js'; import PushHandler from '../push/push-handler.react.js'; import { getPersistor } from '../redux/persist.js'; import { useSelector } from '../redux/redux-utils.js'; import { RootContext } from '../root-context.js'; import { useLoadCommFonts } from '../themes/fonts.js'; import { waitForInteractions } from '../utils/timers.js'; let splashScreenHasHidden = false; export type AppNavigationProp< RouteName: $Keys = $Keys, > = OverlayNavigationProp; const App = createOverlayNavigator< ScreenParamList, OverlayParamList, OverlayNavigationHelpers, >(); type AppNavigatorProps = { navigation: RootNavigationProp<'App'>, ... }; function AppNavigator(props: AppNavigatorProps): React.Node { const { navigation } = props; const fontsLoaded = useLoadCommFonts(); const rootContext = React.useContext(RootContext); - const storeLoadedFromLocalDatabase = useSelector(state => state.storeLoaded); + const storeLoadedFromLocalDatabase = useSelector( + state => state.initialStateLoaded, + ); const setNavStateInitialized = rootContext && rootContext.setNavStateInitialized; React.useEffect(() => { setNavStateInitialized && setNavStateInitialized(); }, [setNavStateInitialized]); const [localSplashScreenHasHidden, setLocalSplashScreenHasHidden] = React.useState(splashScreenHasHidden); React.useEffect(() => { if (localSplashScreenHasHidden || !fontsLoaded) { return; } splashScreenHasHidden = true; void (async () => { await waitForInteractions(); try { await SplashScreen.hideAsync(); } finally { setLocalSplashScreenHasHidden(true); } })(); }, [localSplashScreenHasHidden, fontsLoaded]); let pushHandler; if (localSplashScreenHasHidden) { pushHandler = ( ); } if (!storeLoadedFromLocalDatabase) { return null; } return ( {pushHandler} ); } export default AppNavigator; diff --git a/native/navigation/navigation-handler.react.js b/native/navigation/navigation-handler.react.js index 083ab5569..bb2d39f31 100644 --- a/native/navigation/navigation-handler.react.js +++ b/native/navigation/navigation-handler.react.js @@ -1,82 +1,82 @@ // @flow import * as React from 'react'; import { useIsLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/hooks/account-hooks.js'; +import { usePersistedStateLoaded } from 'lib/selectors/app-state-selectors.js'; import { logInActionType, logOutActionType } from './action-types.js'; import ModalPruner from './modal-pruner.react.js'; import NavFromReduxHandler from './nav-from-redux-handler.react.js'; import { useIsAppLoggedIn } from './nav-selectors.js'; import { NavContext, type NavAction } from './navigation-context.js'; import PolicyAcknowledgmentHandler from './policy-acknowledgment-handler.react.js'; import ThreadScreenTracker from './thread-screen-tracker.react.js'; import { MissingRegistrationDataHandler } from '../account/registration/missing-registration-data/missing-registration-data-handler.react.js'; import DevTools from '../redux/dev-tools.react.js'; -import { usePersistedStateLoaded } from '../selectors/app-state-selectors.js'; const NavigationHandler: React.ComponentType<{}> = React.memo<{}>( function NavigationHandler() { const navContext = React.useContext(NavContext); const persistedStateLoaded = usePersistedStateLoaded(); const devTools = __DEV__ ? : null; if (!navContext || !persistedStateLoaded) { if (__DEV__) { return ( <> {devTools} ); } else { return null; } } const { dispatch } = navContext; return ( <> {devTools} ); }, ); NavigationHandler.displayName = 'NavigationHandler'; type LogInHandlerProps = { +dispatch: (action: NavAction) => void, }; const LogInHandler = React.memo(function LogInHandler( props: LogInHandlerProps, ) { const { dispatch } = props; const loggedIn = useIsLoggedInToIdentityAndAuthoritativeKeyserver(); const navLoggedIn = useIsAppLoggedIn(); const prevLoggedInRef = React.useRef(); React.useEffect(() => { if (loggedIn === prevLoggedInRef.current) { return; } prevLoggedInRef.current = loggedIn; if (loggedIn && !navLoggedIn) { dispatch({ type: (logInActionType: 'LOG_IN') }); } else if (!loggedIn && navLoggedIn) { dispatch({ type: (logOutActionType: 'LOG_OUT') }); } }, [navLoggedIn, loggedIn, dispatch]); return null; }); LogInHandler.displayName = 'LogInHandler'; export default NavigationHandler; diff --git a/native/redux/default-state.js b/native/redux/default-state.js index 7cd5c107e..3a91b6491 100644 --- a/native/redux/default-state.js +++ b/native/redux/default-state.js @@ -1,112 +1,112 @@ // @flow import { Platform } from 'react-native'; import Orientation from 'react-native-orientation-locker'; import { defaultAlertInfos } from 'lib/types/alert-types.js'; import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { defaultKeyserverInfo } from 'lib/types/keyserver-types.js'; import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js'; import { defaultDimensionsInfo } from './dimensions-updater.react.js'; import type { AppState } from './state-types.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { defaultNavInfo } from '../navigation/default-state.js'; import { defaultDeviceCameraInfo } from '../types/camera.js'; import { defaultConnectivityInfo } from '../types/connectivity.js'; import { defaultURLPrefix, natNodeServer } from '../utils/url-utils.js'; const defaultState = ({ navInfo: defaultNavInfo, currentUserInfo: null, draftStore: { drafts: {} }, entryStore: { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }, threadStore: { threadInfos: {}, }, userStore: { userInfos: {}, }, messageStore: { messages: {}, threads: {}, local: {}, currentAsOf: { [authoritativeKeyserverID]: 0 }, }, - storeLoaded: false, + initialStateLoaded: false, loadingStatuses: {}, calendarFilters: defaultCalendarFilters, dataLoaded: false, customServer: natNodeServer, alertStore: { alertInfos: defaultAlertInfos, }, watchedThreadIDs: [], lifecycleState: 'active', enabledApps: defaultEnabledApps, reportStore: { enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, queuedReports: [], }, _persist: null, dimensions: defaultDimensionsInfo, connectivity: defaultConnectivityInfo, globalThemeInfo: defaultGlobalThemeInfo, deviceCameraInfo: defaultDeviceCameraInfo, deviceOrientation: Orientation.getInitialOrientation(), frozen: false, userPolicies: {}, commServicesAccessToken: null, inviteLinksStore: { links: {}, }, keyserverStore: { keyserverInfos: { [authoritativeKeyserverID]: defaultKeyserverInfo( defaultURLPrefix, Platform.OS, ), }, }, localSettings: { isBackupEnabled: false, }, threadActivityStore: {}, integrityStore: { threadHashes: {}, threadHashingStatus: 'starting' }, communityStore: { communityInfos: {}, }, auxUserStore: { auxUserInfos: {}, }, dbOpsStore: { queuedOps: [], }, syncedMetadataStore: { syncedMetadata: {}, }, tunnelbrokerDeviceToken: { localToken: null, tunnelbrokerToken: null, }, queuedDMOperations: { threadQueue: {}, messageQueue: {}, entryQueue: {}, membershipQueue: {}, }, holderStore: { storedHolders: {}, }, }: AppState); export { defaultState }; diff --git a/native/redux/handle-redux-migration-failure.js b/native/redux/handle-redux-migration-failure.js index 4ab11df2a..eba47b8c1 100644 --- a/native/redux/handle-redux-migration-failure.js +++ b/native/redux/handle-redux-migration-failure.js @@ -1,43 +1,43 @@ // @flow import { wipeKeyserverStore } from 'lib/utils/keyserver-store-utils.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import { defaultState } from './default-state.js'; import { nonUserSpecificFieldsNative, type AppState } from './state-types.js'; const persistBlacklist = [ 'loadingStatuses', 'lifecycleState', 'dimensions', 'draftStore', 'connectivity', 'deviceOrientation', 'frozen', 'threadStore', - 'storeLoaded', + 'initialStateLoaded', 'dbOpsStore', 'syncedMetadataStore', 'userStore', 'auxUserStore', 'commServicesAccessToken', 'inviteLinksStore', 'integrityStore', ]; function handleReduxMigrationFailure(oldState: AppState): AppState { const persistedNonUserSpecificFields = nonUserSpecificFieldsNative.filter( field => !persistBlacklist.includes(field) || field === '_persist', ); const stateAfterReset = resetUserSpecificState( oldState, defaultState, persistedNonUserSpecificFields, ); return { ...stateAfterReset, keyserverStore: wipeKeyserverStore(stateAfterReset.keyserverStore), }; } export { persistBlacklist, handleReduxMigrationFailure }; diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js index 9565a05db..428d0eb26 100644 --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -1,463 +1,463 @@ // @flow import { AppState as NativeAppState, Alert } from 'react-native'; 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 { legacySiweAuthActionTypes } from 'lib/actions/siwe-actions.js'; import { logOutActionTypes, deleteAccountActionTypes, legacyLogInActionTypes, keyserverAuthActionTypes, deleteKeyserverAccountActionTypes, } from 'lib/actions/user-actions.js'; import { setNewSessionActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; import type { ThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { queueDBOps } from 'lib/reducers/db-ops-reducer.js'; import { reduceLoadingStatuses } from 'lib/reducers/loading-reducer.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { reduceCurrentUserInfo } from 'lib/reducers/user-reducer.js'; import { shouldClearData } from 'lib/shared/data-utils.js'; import { invalidSessionDowngrade, invalidSessionRecovery, identityInvalidSessionDowngrade, } from 'lib/shared/session-utils.js'; import { isStaff } from 'lib/shared/staff-utils.js'; import { processDMOpsActionType } from 'lib/types/dm-ops.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 { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import { updateDimensionsActiveType, updateConnectivityActiveType, updateDeviceCameraInfoActionType, updateDeviceOrientationActionType, backgroundActionTypes, setReduxStateActionType, setStoreLoadedActionType, type Action, setLocalSettingsActionType, } from './action-types.js'; import { setAccessTokenActionType } from './action-types.js'; import { defaultState } from './default-state.js'; import { remoteReduxDevServerConfig } from './dev-tools.js'; import { persistConfig, setPersistor } from './persist.js'; import { onStateDifference } from './redux-debug-utils.js'; import type { AppState } from './state-types.js'; import { nonUserSpecificFieldsNative } from './state-types.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; import { activeMessageListSelector } from '../navigation/nav-selectors.js'; import reactotron from '../reactotron.js'; import { appOutOfDateAlertDetails } from '../utils/alert-messages.js'; import { isStaffRelease } from '../utils/staff-utils.js'; import { getDevServerHostname } from '../utils/url-utils.js'; function reducer(state: AppState = defaultState, inputAction: Action) { let action = inputAction; 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: $ReadOnlyArray = 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: $ReadOnlyArray = 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 === logOutActionTypes.success || action.type === deleteAccountActionTypes.success) && identityInvalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, ) ) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } if ( action.type === setNewSessionActionType && invalidSessionDowngrade( state, action.payload.sessionChange.currentUserInfo, action.payload.preRequestUserState, action.payload.keyserverID, ) ) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const { currentUserInfo, preRequestUserState } = action.payload; const newKeyserverIDs = []; for (const keyserverID of action.payload.keyserverIDs) { if ( invalidSessionDowngrade( state, currentUserInfo, preRequestUserState, keyserverID, ) ) { continue; } newKeyserverIDs.push(keyserverID); } if (newKeyserverIDs.length === 0) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } action = { ...action, payload: { ...action.payload, keyserverIDs: newKeyserverIDs, }, }; } if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.preRequestUserState?.currentUserInfo, action.payload.authActionSource, )) || ((action.type === legacyLogInActionTypes.success || action.type === legacySiweAuthActionTypes.success) && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) || (action.type === keyserverAuthActionTypes.success && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) ) { return state; } 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 === updateDeviceCameraInfoActionType) { return { ...state, deviceCameraInfo: { ...state.deviceCameraInfo, ...action.payload, }, }; } else if (action.type === updateDeviceOrientationActionType) { return { ...state, deviceOrientation: action.payload, }; } else if (action.type === setLocalSettingsActionType) { return { ...state, localSettings: { ...state.localSettings, ...action.payload }, }; } else if (action.type === setAccessTokenActionType) { return { ...state, commServicesAccessToken: action.payload }; } if (action.type === setNewSessionActionType) { sessionInvalidationAlert(action.payload); } if (action.type === setStoreLoadedActionType) { return { ...state, - storeLoaded: true, + initialStateLoaded: true, }; } if (action.type === setClientDBStoreActionType) { state = { ...state, - storeLoaded: true, + initialStateLoaded: 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; } } // We're calling this reducer twice: here and in the baseReducer. This call // is only used to determine the new current user ID. We don't want to use // the remaining part of the current user info, because it is possible that // the reducer returned a modified ID without cleared remaining parts of // the current user info - this would be a bug, but we want to be extra // careful when clearing the state. // When newCurrentUserInfo has the same ID as state.currentUserInfo the state // won't be cleared and the current user info determined in baseReducer will // be equal to the newCurrentUserInfo. // When newCurrentUserInfo has different ID than state.currentUserInfo, we // reset the state and pass it to the baseReducer. Then, in baseReducer, // reduceCurrentUserInfo acts on the cleared state and may return a different // result than newCurrentUserInfo. // Overall, the solution is a little wasteful, but makes us sure that we never // keep the info of the user when the current user ID changes. const newCurrentUserInfo = reduceCurrentUserInfo( state.currentUserInfo, action, ); if (shouldClearData(state.currentUserInfo?.id, newCurrentUserInfo?.id)) { state = resetUserSpecificState( state, defaultState, nonUserSpecificFieldsNative, ); } const baseReducerResult = baseReducer( state, (action: BaseAction), onStateDifference, ); state = baseReducerResult.state; const { storeOperations } = baseReducerResult; const fixUnreadActiveThreadResult = fixUnreadActiveThread(state, action); state = fixUnreadActiveThreadResult.state; const threadStoreOperationsWithUnreadFix = [ ...(storeOperations.threadStoreOperations ?? []), ...fixUnreadActiveThreadResult.threadStoreOperations, ]; const ops = { ...storeOperations, threadStoreOperations: threadStoreOperationsWithUnreadFix, }; let notificationsCreationData = null; if (action.type === processDMOpsActionType) { notificationsCreationData = action.payload.notificationsCreationData; } state = { ...state, dbOpsStore: queueDBOps( state.dbOpsStore, action.dispatchMetadata, ops, notificationsCreationData, ), }; 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') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ 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 activeThreadInfo = state.threadStore.threadInfos[activeThread]; const updatedActiveThreadInfo = { ...activeThreadInfo, currentUser: { ...activeThreadInfo.currentUser, unread: false, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: activeThread, threadInfo: updatedActiveThreadInfo, }, }, ]; const updatedThreadStore = threadStoreOpsHandlers.processStoreOperations( 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({ maxAge: 200, 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 636d0a7de..9650cfe2e 100644 --- a/native/redux/state-types.js +++ b/native/redux/state-types.js @@ -1,93 +1,93 @@ // @flow import type { Orientations } from 'react-native-orientation-locker'; import type { PersistState } from 'redux-persist/es/types.js'; import type { AlertStore } from 'lib/types/alert-types.js'; import type { AuxUserStore } from 'lib/types/aux-user-types.js'; import type { CommunityStore } from 'lib/types/community-types.js'; import type { DBOpsStore } from 'lib/types/db-ops-types'; import type { QueuedDMOperations } from 'lib/types/dm-ops'; 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 { HolderStore } from 'lib/types/holder-types.js'; import type { IntegrityStore } from 'lib/types/integrity-types.js'; import type { KeyserverStore } from 'lib/types/keyserver-types.js'; import type { LifecycleState } from 'lib/types/lifecycle-state-types.js'; import type { InviteLinksStore } from 'lib/types/link-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 { SyncedMetadataStore } from 'lib/types/synced-metadata-types.js'; import type { GlobalThemeInfo } from 'lib/types/theme-types.js'; import type { ThreadActivityStore } from 'lib/types/thread-activity-types'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { TunnelbrokerDeviceToken } from 'lib/types/tunnelbroker-device-token-types'; import type { CurrentUserInfo, UserStore } from 'lib/types/user-types.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 { LocalSettings } from '../types/local-settings-types.js'; const nonUserSpecificFieldsNative = [ - 'storeLoaded', + 'initialStateLoaded', 'loadingStatuses', 'customServer', 'lifecycleState', 'dimensions', 'connectivity', 'deviceCameraInfo', 'deviceOrientation', 'frozen', 'keyserverStore', '_persist', 'commServicesAccessToken', ]; export type AppState = { +navInfo: NavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, - +storeLoaded: boolean, + +initialStateLoaded: boolean, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +dataLoaded: boolean, +customServer: ?string, +alertStore: AlertStore, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +_persist: ?PersistState, +dimensions: DimensionsInfo, +connectivity: ConnectivityInfo, +globalThemeInfo: GlobalThemeInfo, +deviceCameraInfo: DeviceCameraInfo, +deviceOrientation: Orientations, +frozen: boolean, +userPolicies: UserPolicies, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +localSettings: LocalSettings, +integrityStore: IntegrityStore, +communityStore: CommunityStore, +dbOpsStore: DBOpsStore, +syncedMetadataStore: SyncedMetadataStore, +auxUserStore: AuxUserStore, +tunnelbrokerDeviceToken: TunnelbrokerDeviceToken, +queuedDMOperations: QueuedDMOperations, +holderStore: HolderStore, }; export { nonUserSpecificFieldsNative }; diff --git a/native/selectors/app-state-selectors.js b/native/selectors/app-state-selectors.js deleted file mode 100644 index 5bfe91fef..000000000 --- a/native/selectors/app-state-selectors.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow - -import { useSelector } from '../redux/redux-utils.js'; - -function usePersistedStateLoaded(): boolean { - const rehydrateConcluded = useSelector(state => !!state._persist?.rehydrated); - const storeLoaded = useSelector(state => state.storeLoaded); - - return rehydrateConcluded && storeLoaded; -} - -export { usePersistedStateLoaded };