diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js index 72bc169c9..bb48829c7 100644 --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,590 +1,606 @@ // @flow import t, { type TInterface, type TUnion } from 'tcomb'; import { clientAvatarValidator, type ClientAvatar } from './avatar-types.js'; import type { BlobOperation } from './holder-types.js'; import { type Media, mediaValidator } from './media-types.js'; import type { RawMessageInfo } from './message-types.js'; import type { RelationshipOperation } from './messages/update-relationship.js'; import type { NotificationsCreationData } from './notif-types.js'; import type { OutboundP2PMessage } from './sqlite-types.js'; import { type ThreadSubscription, threadSubscriptionValidator, } from './subscription-types.js'; import { type NonSidebarThickThreadType, nonSidebarThickThreadTypes, type ThickThreadType, thickThreadTypeValidator, } from './thread-types-enum.js'; import { threadTimestampsValidator, type ThreadTimestamps, } from './thread-types.js'; import type { ClientUpdateInfo } from './update-types.js'; import { values } from '../utils/objects.js'; -import { tColor, tShape, tString, tUserID } from '../utils/validation-utils.js'; +import { + tColor, + thickIDRegex, + tRegex, + tShape, + tString, + tUserID, + uuidRegex, +} from '../utils/validation-utils.js'; export const dmOperationTypes = Object.freeze({ CREATE_THREAD: 'create_thread', CREATE_SIDEBAR: 'create_sidebar', SEND_TEXT_MESSAGE: 'send_text_message', SEND_MULTIMEDIA_MESSAGE: 'send_multimedia_message', SEND_REACTION_MESSAGE: 'send_reaction_message', SEND_EDIT_MESSAGE: 'send_edit_message', ADD_MEMBERS: 'add_members', ADD_VIEWER_TO_THREAD_MEMBERS: 'add_viewer_to_thread_members', JOIN_THREAD: 'join_thread', LEAVE_THREAD: 'leave_thread', CHANGE_THREAD_SETTINGS: 'change_thread_settings', CHANGE_THREAD_SUBSCRIPTION: 'change_thread_subscription', CHANGE_THREAD_READ_STATUS: 'change_thread_read_status', CREATE_ENTRY: 'create_entry', DELETE_ENTRY: 'delete_entry', EDIT_ENTRY: 'edit_entry', UPDATE_RELATIONSHIP: 'update_relationship', }); export type DMOperationType = $Values; +const tThickID = tRegex(thickIDRegex); + +// In CHANGE_THREAD_SETTINGS operation we're generating message IDs +// based on the prefix that is tThickID. A message with the generated ID +// can be used as a target message of the edit, reaction, or a sidebar. +const thickTargetMessageIDRegex = new RegExp(`^${uuidRegex}(?:/\\w+)?$`); +const tThickTargetMessageID = tRegex(thickTargetMessageIDRegex); + type MemberIDWithSubscription = { +id: string, +subscription: ThreadSubscription, }; export const memberIDWithSubscriptionValidator: TInterface = tShape({ id: tUserID, subscription: threadSubscriptionValidator, }); export type CreateThickRawThreadInfoInput = { +threadID: string, +threadType: ThickThreadType, +creationTime: number, +parentThreadID?: ?string, +allMemberIDsWithSubscriptions: $ReadOnlyArray, +roleID: string, +unread: boolean, +timestamps: ThreadTimestamps, +name?: ?string, +avatar?: ?ClientAvatar, +description?: ?string, +color?: ?string, +containingThreadID?: ?string, +sourceMessageID?: ?string, +repliesCount?: ?number, +pinnedCount?: ?number, }; export const createThickRawThreadInfoInputValidator: TInterface = tShape({ - threadID: t.String, + threadID: tThickID, threadType: thickThreadTypeValidator, creationTime: t.Number, - parentThreadID: t.maybe(t.String), + parentThreadID: t.maybe(tThickID), allMemberIDsWithSubscriptions: t.list(memberIDWithSubscriptionValidator), - roleID: t.String, + roleID: tThickID, unread: t.Boolean, timestamps: threadTimestampsValidator, name: t.maybe(t.String), avatar: t.maybe(clientAvatarValidator), description: t.maybe(t.String), color: t.maybe(t.String), - containingThreadID: t.maybe(t.String), - sourceMessageID: t.maybe(t.String), + containingThreadID: t.maybe(tThickID), + sourceMessageID: t.maybe(tThickTargetMessageID), repliesCount: t.maybe(t.Number), pinnedCount: t.maybe(t.Number), }); export type DMCreateThreadOperation = { +type: 'create_thread', +threadID: string, +creatorID: string, +time: number, +threadType: NonSidebarThickThreadType, +memberIDs: $ReadOnlyArray, +roleID: string, +newMessageID: string, }; export const dmCreateThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_THREAD), - threadID: t.String, + threadID: tThickID, creatorID: tUserID, time: t.Number, threadType: t.enums.of(values(nonSidebarThickThreadTypes)), memberIDs: t.list(tUserID), - roleID: t.String, - newMessageID: t.String, + roleID: tThickID, + newMessageID: tThickID, }); export type DMCreateSidebarOperation = { +type: 'create_sidebar', +threadID: string, +creatorID: string, +time: number, +parentThreadID: string, +memberIDs: $ReadOnlyArray, +sourceMessageID: string, +roleID: string, +newSidebarSourceMessageID: string, +newCreateSidebarMessageID: string, }; export const dmCreateSidebarOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_SIDEBAR), - threadID: t.String, + threadID: tThickID, creatorID: tUserID, time: t.Number, - parentThreadID: t.String, + parentThreadID: tThickID, memberIDs: t.list(tUserID), - sourceMessageID: t.String, - roleID: t.String, - newSidebarSourceMessageID: t.String, - newCreateSidebarMessageID: t.String, + sourceMessageID: tThickTargetMessageID, + roleID: tThickID, + newSidebarSourceMessageID: tThickID, + newCreateSidebarMessageID: tThickID, }); export type DMSendTextMessageOperation = { +type: 'send_text_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +text: string, }; export const dmSendTextMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_TEXT_MESSAGE), - threadID: t.String, + threadID: tThickID, creatorID: tUserID, time: t.Number, - messageID: t.String, + messageID: tThickID, text: t.String, }); export type DMSendMultimediaMessageOperation = { +type: 'send_multimedia_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +media: $ReadOnlyArray, }; export const dmSendMultimediaMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_MULTIMEDIA_MESSAGE), - threadID: t.String, + threadID: tThickID, creatorID: tUserID, time: t.Number, - messageID: t.String, + messageID: tThickID, media: t.list(mediaValidator), }); export type DMSendReactionMessageOperation = { +type: 'send_reaction_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +targetMessageID: string, +reaction: string, +action: 'add_reaction' | 'remove_reaction', }; export const dmSendReactionMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_REACTION_MESSAGE), - threadID: t.String, + threadID: tThickID, creatorID: tUserID, time: t.Number, - messageID: t.String, - targetMessageID: t.String, + messageID: tThickID, + targetMessageID: tThickTargetMessageID, reaction: t.String, action: t.enums.of(['add_reaction', 'remove_reaction']), }); export type DMSendEditMessageOperation = { +type: 'send_edit_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +targetMessageID: string, +text: string, }; export const dmSendEditMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_EDIT_MESSAGE), - threadID: t.String, + threadID: tThickID, creatorID: tUserID, time: t.Number, - messageID: t.String, - targetMessageID: t.String, + messageID: tThickID, + targetMessageID: tThickTargetMessageID, text: t.String, }); type DMAddMembersBase = { +editorID: string, +time: number, +addedUserIDs: $ReadOnlyArray, }; const dmAddMembersBaseValidatorShape = { editorID: tUserID, time: t.Number, addedUserIDs: t.list(tUserID), }; export type DMAddMembersOperation = $ReadOnly<{ +type: 'add_members', +threadID: string, +messageID: string, ...DMAddMembersBase, }>; export const dmAddMembersOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_MEMBERS), - threadID: t.String, - messageID: t.String, + threadID: tThickID, + messageID: tThickID, ...dmAddMembersBaseValidatorShape, }); export type DMAddViewerToThreadMembersOperation = $ReadOnly<{ +type: 'add_viewer_to_thread_members', +messageID: ?string, +existingThreadDetails: CreateThickRawThreadInfoInput, ...DMAddMembersBase, }>; export const dmAddViewerToThreadMembersValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS), - messageID: t.maybe(t.String), + messageID: t.maybe(tThickID), existingThreadDetails: createThickRawThreadInfoInputValidator, ...dmAddMembersBaseValidatorShape, }); export type DMJoinThreadOperation = { +type: 'join_thread', +joinerID: string, +time: number, +messageID: string, +existingThreadDetails: CreateThickRawThreadInfoInput, }; export const dmJoinThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.JOIN_THREAD), joinerID: tUserID, time: t.Number, - messageID: t.String, + messageID: tThickID, existingThreadDetails: createThickRawThreadInfoInputValidator, }); export type DMLeaveThreadOperation = { +type: 'leave_thread', +editorID: string, +time: number, +messageID: string, +threadID: string, }; export const dmLeaveThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.LEAVE_THREAD), editorID: tUserID, time: t.Number, - messageID: t.String, - threadID: t.String, + messageID: tThickID, + threadID: tThickID, }); export type DMThreadSettingsChanges = { +name?: string, +description?: string, +color?: string, +avatar?: ClientAvatar | null, }; export type DMChangeThreadSettingsOperation = $ReadOnly<{ +type: 'change_thread_settings', +threadID: string, +editorID: string, +time: number, +changes: DMThreadSettingsChanges, +messageIDsPrefix: string, }>; export const dmChangeThreadSettingsOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS), - threadID: t.String, + threadID: tThickID, editorID: tUserID, time: t.Number, changes: tShape({ name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), avatar: t.maybe(clientAvatarValidator), }), - messageIDsPrefix: t.String, + messageIDsPrefix: tThickID, }); export type DMChangeThreadSubscriptionOperation = { +type: 'change_thread_subscription', +time: number, +threadID: string, +creatorID: string, +subscription: ThreadSubscription, }; export const dmChangeThreadSubscriptionOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SUBSCRIPTION), time: t.Number, - threadID: t.String, + threadID: tThickID, creatorID: tUserID, subscription: threadSubscriptionValidator, }); export type DMChangeThreadReadStatusOperation = { +type: 'change_thread_read_status', +time: number, +threadID: string, +creatorID: string, +unread: boolean, }; export const dmChangeThreadReadStatusOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_READ_STATUS), time: t.Number, - threadID: t.String, + threadID: tThickID, creatorID: tUserID, unread: t.Boolean, }); export type ComposableDMOperation = | DMSendTextMessageOperation | DMSendMultimediaMessageOperation; export type DMCreateEntryOperation = { +type: 'create_entry', +threadID: string, +creatorID: string, +time: number, +entryID: string, +entryDate: string, +text: string, +messageID: string, }; export const dmCreateEntryOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_ENTRY), - threadID: t.String, + threadID: tThickID, creatorID: tUserID, time: t.Number, - entryID: t.String, + entryID: tThickID, entryDate: t.String, text: t.String, - messageID: t.String, + messageID: tThickID, }); export type DMDeleteEntryOperation = { +type: 'delete_entry', +threadID: string, +creatorID: string, +time: number, +creationTime: number, +entryID: string, +entryDate: string, +prevText: string, +messageID: string, }; export const dmDeleteEntryOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.DELETE_ENTRY), - threadID: t.String, + threadID: tThickID, creatorID: tUserID, time: t.Number, creationTime: t.Number, - entryID: t.String, + entryID: tThickID, entryDate: t.String, prevText: t.String, - messageID: t.String, + messageID: tThickID, }); export type DMEditEntryOperation = { +type: 'edit_entry', +threadID: string, +creatorID: string, +time: number, +creationTime: number, +entryID: string, +entryDate: string, +text: string, +messageID: string, }; export const dmEditEntryOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.EDIT_ENTRY), - threadID: t.String, + threadID: tThickID, creatorID: tUserID, creationTime: t.Number, time: t.Number, - entryID: t.String, + entryID: tThickID, entryDate: t.String, text: t.String, - messageID: t.String, + messageID: tThickID, }); export type DMUpdateRelationshipOperation = { +type: 'update_relationship', +threadID: string, +creatorID: string, +time: number, +operation: RelationshipOperation, +targetUserID: string, +messageID: string, }; export const dmUpdateRelationshipOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.UPDATE_RELATIONSHIP), - threadID: t.String, + threadID: tThickID, creatorID: tUserID, time: t.Number, operation: t.enums.of([ 'request_sent', 'request_accepted', 'farcaster_mutual', ]), targetUserID: tUserID, - messageID: t.String, + messageID: tThickID, }); export type DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation | DMSendTextMessageOperation | DMSendMultimediaMessageOperation | DMSendReactionMessageOperation | DMSendEditMessageOperation | DMAddMembersOperation | DMAddViewerToThreadMembersOperation | DMJoinThreadOperation | DMLeaveThreadOperation | DMChangeThreadSettingsOperation | DMChangeThreadSubscriptionOperation | DMChangeThreadReadStatusOperation | DMCreateEntryOperation | DMDeleteEntryOperation | DMEditEntryOperation | DMUpdateRelationshipOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, dmSendTextMessageOperationValidator, dmSendMultimediaMessageOperationValidator, dmSendReactionMessageOperationValidator, dmSendEditMessageOperationValidator, dmAddMembersOperationValidator, dmAddViewerToThreadMembersValidator, dmJoinThreadOperationValidator, dmLeaveThreadOperationValidator, dmChangeThreadSettingsOperationValidator, dmChangeThreadSubscriptionOperationValidator, dmChangeThreadReadStatusOperationValidator, dmCreateEntryOperationValidator, dmDeleteEntryOperationValidator, dmEditEntryOperationValidator, dmUpdateRelationshipOperationValidator, ]); export type DMBlobOperation = $ReadOnly<{ ...BlobOperation, +dmOpType: 'inbound_only' | 'outbound_only' | 'inbound_and_outbound', }>; export type DMOperationResult = { rawMessageInfos: Array, updateInfos: Array, blobOps: Array, notificationsCreationData: ?NotificationsCreationData, }; export const processDMOpsActionType = 'PROCESS_DM_OPS'; export type ProcessDMOpsPayload = { +rawMessageInfos: $ReadOnlyArray, +updateInfos: $ReadOnlyArray, +outboundP2PMessages: ?$ReadOnlyArray, // For messages that could be retried from UI, we need to bind DM `messageID` // with `outboundP2PMessages` to keep track of whether all P2P messages // were queued on Tunnelbroker. +composableMessageID: ?string, +notificationsCreationData: ?NotificationsCreationData, }; export const queueDMOpsActionType = 'QUEUE_DM_OPS'; export type QueueDMOpsPayload = { +operation: DMOperation, +timestamp: number, +condition: | { +type: 'thread', +threadID: string, } | { +type: 'entry', +entryID: string, } | { +type: 'message', +messageID: string, } | { +type: 'membership', +threadID: string, +userID: string, }, }; export const pruneDMOpsQueueActionType = 'PRUNE_DM_OPS_QUEUE'; export type PruneDMOpsQueuePayload = { +pruneMaxTimestamp: number, }; export const clearQueuedThreadDMOpsActionType = 'CLEAR_QUEUED_THREAD_DM_OPS'; export type ClearQueuedThreadDMOpsPayload = { +threadID: string, }; export const clearQueuedMessageDMOpsActionType = 'CLEAR_QUEUED_MESSAGE_DM_OPS'; export type ClearQueuedMessageDMOpsPayload = { +messageID: string, }; export const clearQueuedEntryDMOpsActionType = 'CLEAR_QUEUED_ENTRY_DM_OPS'; export type ClearQueuedEntryDMOpsPayload = { +entryID: string, }; export const clearQueuedMembershipDMOpsActionType = 'CLEAR_QUEUED_MEMBERSHIP_DM_OPS'; export type ClearQueuedMembershipDMOpsPayload = { +threadID: string, +userID: string, }; export type OperationsQueue = $ReadOnlyArray<{ +operation: DMOperation, +timestamp: number, }>; export type QueuedDMOperations = { +threadQueue: { +[threadID: string]: OperationsQueue, }, +messageQueue: { +[messageID: string]: OperationsQueue, }, +entryQueue: { +[entryID: string]: OperationsQueue, }, +membershipQueue: { +[threadID: string]: { +[memberID: string]: OperationsQueue, }, }, }; diff --git a/lib/utils/validation-utils.js b/lib/utils/validation-utils.js index 3472123f7..7d7182de0 100644 --- a/lib/utils/validation-utils.js +++ b/lib/utils/validation-utils.js @@ -1,158 +1,159 @@ // @flow import invariant from 'invariant'; import t from 'tcomb'; import type { TStructProps, TIrreducible, TRefinement, TEnums, TInterface, TUnion, TType, } from 'tcomb'; import { validEmailRegex, oldValidUsernameRegex, validHexColorRegex, } from '../shared/account-utils.js'; import type { PlatformDetails } from '../types/device-types'; import type { MediaMessageServerDBContent, PhotoMessageServerDBContent, VideoMessageServerDBContent, } from '../types/messages/media'; function tBool(value: boolean): TIrreducible { return t.irreducible(value.toString(), x => x === value); } function tString(value: string): TIrreducible { return t.irreducible(`'${value}'`, x => x === value); } function tNumber(value: number): TIrreducible { return t.irreducible(value.toString(), x => x === value); } function tShape(spec: TStructProps): TInterface { return t.interface(spec, { strict: true }); } export type TRegex = TRefinement; function tRegex(regex: RegExp): TRegex { return t.refinement(t.String, val => regex.test(val)); } function tNumEnum(nums: $ReadOnlyArray): TRefinement { return t.refinement(t.Number, (input: number) => { for (const num of nums) { if (input === num) { return true; } } return false; }); } const tNull: TIrreducible = t.irreducible('null', x => x === null); const tDate: TRegex = tRegex(/^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$/); const tColor: TRegex = tRegex(validHexColorRegex); // we don't include # char const tPlatform: TEnums = t.enums.of([ 'ios', 'android', 'web', 'windows', 'macos', ]); const tDeviceType: TEnums = t.enums.of(['ios', 'android']); const tPlatformDetails: TInterface = tShape({ platform: tPlatform, codeVersion: t.maybe(t.Number), stateVersion: t.maybe(t.Number), majorDesktopVersion: t.maybe(t.Number), }); const tPassword: TRefinement = t.refinement( t.String, (password: string) => !!password, ); const tCookie: TRegex = tRegex(/^(user|anonymous)=[0-9]+:[0-9a-f]+$/); const tEmail: TRegex = tRegex(validEmailRegex); const tOldValidUsername: TRegex = tRegex(oldValidUsernameRegex); const tID: TRefinement = t.refinement(t.String, (id: string) => !!id); const tUserID: TRefinement = t.refinement( t.String, (id: string) => !!id, ); const tMediaMessagePhoto: TInterface = tShape({ type: tString('photo'), uploadID: tID, }); const tMediaMessageVideo: TInterface = tShape({ type: tString('video'), uploadID: tID, thumbnailUploadID: tID, }); const tMediaMessageMedia: TUnion = t.union([ tMediaMessagePhoto, tMediaMessageVideo, ]); function assertWithValidator(data: mixed, validator: TType): T { invariant(validator.is(data), "data isn't of type T"); return (data: any); } const ashoatKeyserverID = '256'; const uuidRegex = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'; const idSchemaRegex = `(?:(?:[0-9]+|${uuidRegex})\\|)?(?:[0-9]+|${uuidRegex})`; const pendingSidebarURLPrefix = 'sidebar'; const pendingThickSidebarURLPrefix = 'dm_sidebar'; const pendingThreadIDRegex = `pending/(type[0-9]+/[0-9]+(\\+[0-9]+)*|(${pendingSidebarURLPrefix}|${pendingThickSidebarURLPrefix})/${idSchemaRegex})`; -const thickThreadIDRegex: RegExp = new RegExp(`^${uuidRegex}$`); +const thickIDRegex: RegExp = new RegExp(`^${uuidRegex}$`); const chatNameMaxLength = 191; const chatNameMinLength = 0; const secondCharRange = `{${chatNameMinLength},${chatNameMaxLength}}`; const validChatNameRegexString = `.${secondCharRange}`; const validChatNameRegex: RegExp = new RegExp(`^${validChatNameRegexString}$`); export { tBool, tString, tNumber, tShape, tRegex, tNumEnum, tNull, tDate, tColor, tPlatform, tDeviceType, tPlatformDetails, tPassword, tCookie, tEmail, tOldValidUsername, tID, tUserID, tMediaMessagePhoto, tMediaMessageVideo, tMediaMessageMedia, assertWithValidator, ashoatKeyserverID, + uuidRegex, idSchemaRegex, pendingSidebarURLPrefix, pendingThickSidebarURLPrefix, pendingThreadIDRegex, - thickThreadIDRegex, + thickIDRegex, validChatNameRegex, validChatNameRegexString, chatNameMaxLength, }; diff --git a/web/redux/initial-state-gate.js b/web/redux/initial-state-gate.js index a0cbd7b00..9e4baf518 100644 --- a/web/redux/initial-state-gate.js +++ b/web/redux/initial-state-gate.js @@ -1,220 +1,220 @@ // @flow import * as React from 'react'; import { PersistGate } from 'redux-persist/es/integration/react.js'; import type { Persistor } from 'redux-persist/es/types'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import type { EntryStoreOperation } from 'lib/ops/entries-store-ops.js'; import type { MessageStoreOperation } from 'lib/ops/message-store-ops.js'; import type { ThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import type { UserStoreOperation } from 'lib/ops/user-store-ops.js'; import { getMessageSearchStoreOps } from 'lib/reducers/db-ops-reducer.js'; import { allUpdatesCurrentAsOfSelector } from 'lib/selectors/keyserver-selectors.js'; import type { RawThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { getConfig } from 'lib/utils/config.js'; import { convertIDToNewSchema } from 'lib/utils/migration-utils.js'; import { entries, values } from 'lib/utils/objects.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; -import { thickThreadIDRegex } from 'lib/utils/validation-utils.js'; +import { thickIDRegex } from 'lib/utils/validation-utils.js'; import { setInitialReduxState, useGetInitialReduxState, } from './action-types.js'; import { useSelector } from './redux-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import Loading from '../loading.react.js'; import { getClientDBStore } from '../shared-worker/utils/store.js'; import type { InitialReduxStateActionPayload } from '../types/redux-types.js'; type Props = { +persistor: Persistor, +children: React.Node, }; function InitialReduxStateGate(props: Props): React.Node { const { children, persistor } = props; const callGetInitialReduxState = useGetInitialReduxState(); const dispatch = useDispatch(); const [initError, setInitError] = React.useState(null); React.useEffect(() => { if (initError) { throw initError; } }, [initError]); const isRehydrated = useSelector(state => !!state._persist?.rehydrated); const allUpdatesCurrentAsOf = useSelector(allUpdatesCurrentAsOfSelector); const prevIsRehydrated = React.useRef(false); React.useEffect(() => { if (prevIsRehydrated.current || !isRehydrated) { return; } prevIsRehydrated.current = isRehydrated; void (async () => { try { let urlInfo = infoFromURL(decodeURI(window.location.href)); const isThickThreadOpen = - urlInfo.thread && thickThreadIDRegex.test(urlInfo.thread); + urlInfo.thread && thickIDRegex.test(urlInfo.thread); // Handle older links if (urlInfo.thread && !isThickThreadOpen) { urlInfo = { ...urlInfo, thread: convertIDToNewSchema( urlInfo.thread, authoritativeKeyserverID, ), }; } const clientDBStore = await getClientDBStore(); dispatch({ type: setClientDBStoreActionType, payload: clientDBStore, }); const payload = await callGetInitialReduxState({ urlInfo, excludedData: { threadStore: !!clientDBStore.threadStore, messageStore: !!clientDBStore.messages, userStore: !!clientDBStore.users, entryStore: !!clientDBStore.entries, }, allUpdatesCurrentAsOf, }); if (isThickThreadOpen) { payload.navInfo.activeChatThreadID = urlInfo.thread; } const currentLoggedInUserID = payload.currentUserInfo?.anonymous ? null : payload.currentUserInfo?.id; if (!currentLoggedInUserID) { dispatch({ type: setInitialReduxState, payload }); return; } let initialReduxState: InitialReduxStateActionPayload = payload; let threadStoreOperations: ThreadStoreOperation[] = []; if (clientDBStore.threadStore) { const { threadStore, ...rest } = initialReduxState; initialReduxState = rest; } else { // When there is no data in the DB, it's necessary to migrate data // from the keyserver payload to the DB threadStoreOperations = entries(payload.threadStore.threadInfos).map( ([id, threadInfo]: [string, RawThreadInfo]) => ({ type: 'replace', payload: { id, threadInfo, }, }), ); } let userStoreOperations: UserStoreOperation[] = []; if (clientDBStore.users) { const { userInfos, ...rest } = initialReduxState; initialReduxState = rest; } else { userStoreOperations = values(payload.userInfos).map(userInfo => ({ type: 'replace_user', payload: userInfo, })); } let messageStoreOperations: MessageStoreOperation[] = []; if (clientDBStore.messages) { const { messageStore, ...rest } = initialReduxState; initialReduxState = rest; } else { const { messages, threads } = payload.messageStore; messageStoreOperations = [ ...entries(messages).map(([id, messageInfo]) => ({ type: 'replace', payload: { id, messageInfo }, })), { type: 'replace_threads', payload: { threads }, }, ]; } let entryStoreOperations: Array = []; if (clientDBStore.entries) { const { entryStore, ...rest } = initialReduxState; initialReduxState = rest; } else { entryStoreOperations = entries(payload.entryStore.entryInfos).map( ([id, entry]) => ({ type: 'replace_entry', payload: { id, entry }, }), ); } if ( threadStoreOperations.length > 0 || userStoreOperations.length > 0 || messageStoreOperations.length > 0 || entryStoreOperations.length > 0 ) { const messageSearchStoreOperations = getMessageSearchStoreOps( messageStoreOperations, ); const { sqliteAPI } = getConfig(); await sqliteAPI.processDBStoreOperations({ threadStoreOperations, draftStoreOperations: [], messageStoreOperations, reportStoreOperations: [], userStoreOperations, keyserverStoreOperations: [], communityStoreOperations: [], integrityStoreOperations: [], syncedMetadataStoreOperations: [], auxUserStoreOperations: [], threadActivityStoreOperations: [], entryStoreOperations, messageSearchStoreOperations, }); } dispatch({ type: setInitialReduxState, payload: initialReduxState, }); } catch (err) { setInitError(err); } })(); }, [callGetInitialReduxState, dispatch, isRehydrated, allUpdatesCurrentAsOf]); const initialStateLoaded = useSelector(state => state.initialStateLoaded); const childFunction = React.useCallback( // This argument is passed from `PersistGate`. It means that the state is // rehydrated and we can start fetching the initial info. (bootstrapped: boolean) => { if (bootstrapped && initialStateLoaded) { return children; } else { return ; } }, [children, initialStateLoaded], ); return {childFunction}; } export default InitialReduxStateGate;