diff --git a/lib/actions/synced-metadata-actions.js b/lib/actions/synced-metadata-actions.js --- a/lib/actions/synced-metadata-actions.js +++ b/lib/actions/synced-metadata-actions.js @@ -2,7 +2,14 @@ const setSyncedMetadataEntryActionType = 'SET_SYNCED_METADATA_ENTRY' as const; +const setSyncedMetadataEntriesActionType = + 'SET_SYNCED_METADATA_ENTRIES' as const; + const clearSyncedMetadataEntryActionType = 'CLEAR_SYNCED_METADATA_ENTRY' as const; -export { setSyncedMetadataEntryActionType, clearSyncedMetadataEntryActionType }; +export { + setSyncedMetadataEntryActionType, + setSyncedMetadataEntriesActionType, + clearSyncedMetadataEntryActionType, +}; diff --git a/lib/reducers/synced-metadata-reducer.js b/lib/reducers/synced-metadata-reducer.js --- a/lib/reducers/synced-metadata-reducer.js +++ b/lib/reducers/synced-metadata-reducer.js @@ -3,6 +3,7 @@ import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; import { setSyncedMetadataEntryActionType, + setSyncedMetadataEntriesActionType, clearSyncedMetadataEntryActionType, } from '../actions/synced-metadata-actions.js'; import { @@ -34,6 +35,17 @@ syncedMetadataStore: processStoreOps(state, [replaceOperation]), syncedMetadataStoreOperations: [replaceOperation], }; + } else if (action.type === setSyncedMetadataEntriesActionType) { + const replaceOperations: Array = + action.payload.entries.map(entry => ({ + type: 'replace_synced_metadata_entry', + payload: entry, + })); + + return { + syncedMetadataStore: processStoreOps(state, replaceOperations), + syncedMetadataStoreOperations: replaceOperations, + }; } else if (action.type === clearSyncedMetadataEntryActionType) { const removeOperation: RemoveSyncedMetadataOperation = { type: 'remove_synced_metadata', diff --git a/lib/shared/farcaster/farcaster-hooks.js b/lib/shared/farcaster/farcaster-hooks.js --- a/lib/shared/farcaster/farcaster-hooks.js +++ b/lib/shared/farcaster/farcaster-hooks.js @@ -67,7 +67,9 @@ useCurrentUserFID, useCurrentUserSupportsDCs, useFarcasterDCsLoaded, - useSetFarcasterDCsLoaded, + useFarcasterDCsSyncCanceled, + useFarcasterDCsSyncCancellationRef, + useSetFarcasterDCsSyncStatus, } from '../../utils/farcaster-utils.js'; import { values } from '../../utils/objects.js'; import { useDispatch, useSelector } from '../../utils/redux-utils.js'; @@ -640,6 +642,7 @@ conversationID: string, messagesLimit: number, batchedUpdates: BatchedUpdates, + canBeCanceled: boolean, increaseMessagesCount?: (number) => mixed, ) => Promise { const fetchUsersByFIDs = useGetCommFCUsersForFIDs(); @@ -648,14 +651,19 @@ useFetchFarcasterConversationInvites(); const fetchFarcasterMessages = useFetchMessagesForConversation(); const { addLog } = useDebugLogs(); + const canceledRef = useFarcasterDCsSyncCancellationRef(); return React.useCallback( async ( conversationID: string, messagesLimit: number, batchedUpdates: BatchedUpdates, + canBeCanceled: boolean, increaseMessagesCount?: number => mixed, ): Promise => { + if (canBeCanceled && canceledRef.current) { + return null; + } try { const result = await fetchAndProcessConversation( conversationID, @@ -686,12 +694,13 @@ threadMembers.forEach(member => batchedUpdates.addUserID(member.id)); } - const messagesResult = await fetchFarcasterMessages( + const messagesResult = await fetchFarcasterMessages({ conversationID, - messagesLimit, - undefined, - increaseMessagesCount, - ); + messagesNumberLimit: messagesLimit, + cursor: undefined, + canBeCanceled, + increaseMessageCount: increaseMessagesCount, + }); if (messagesResult.messages.length === 0) { addLog( @@ -742,20 +751,26 @@ } }, [ - addLog, + canceledRef, fetchFarcasterConversation, - fetchFarcasterMessages, fetchFarcasterConversationInvites, fetchUsersByFIDs, + addLog, + fetchFarcasterMessages, ], ); } +type FetchMessagesForConversationInput = { + +conversationID: string, + +messagesNumberLimit?: number, + +cursor?: ?string, + +canBeCanceled?: boolean, + +increaseMessageCount?: number => mixed, +}; + function useFetchMessagesForConversation(): ( - conversationID: string, - messagesNumberLimit?: number, - cursor?: ?string, - increaseMessageCount?: (number) => mixed, + input: FetchMessagesForConversationInput, ) => Promise<{ +messages: Array, +userIDs: Array, @@ -765,21 +780,34 @@ const fetchUsersByFIDs = useGetCommFCUsersForFIDs(); const { addLog } = useDebugLogs(); + const canceledRef = useFarcasterDCsSyncCancellationRef(); + return React.useCallback( async ( - conversationID: string, - messagesNumberLimit: number = 20, - cursor?: ?string, - increaseMessageCount?: number => mixed, + input: FetchMessagesForConversationInput, ): Promise<{ +messages: Array, +userIDs: Array, +newCursor?: ?string, }> => { + const { + conversationID, + messagesNumberLimit = 20, + cursor: initialCursor, + canBeCanceled = false, + increaseMessageCount, + } = input; + if (canBeCanceled && canceledRef.current) { + return { + messages: [], + userIDs: [], + }; + } const result: Array = []; const messageIDs = new Set(); const identityCache = new Map(); let batchNumber = 0; + let cursor = initialCursor; try { let totalMessagesFetched = 0; @@ -892,7 +920,11 @@ } // This should help with the app responsiveness await sleep(0); - } while (cursor && totalMessagesFetched < messagesNumberLimit); + } while ( + cursor && + totalMessagesFetched < messagesNumberLimit && + (!canBeCanceled || !canceledRef.current) + ); } catch (e) { addLog( 'Farcaster: Failed to fetch messages', @@ -916,7 +948,7 @@ newCursor: cursor, }; }, - [addLog, fetchFarcasterMessages, fetchUsersByFIDs], + [addLog, canceledRef, fetchFarcasterMessages, fetchUsersByFIDs], ); } @@ -935,6 +967,7 @@ conversationID, messagesLimit ?? 20, batchedUpdates, + false, ); if (!batchedUpdates.isEmpty()) { @@ -1098,7 +1131,7 @@ ) => Promise { const dispatch = useDispatch(); const fetchConversationWithMessages = useFetchConversationWithMessages(); - const setFarcasterDCsLoaded = useSetFarcasterDCsLoaded(); + const { setLoaded } = useSetFarcasterDCsSyncStatus(); const { addLog } = useDebugLogs(); const fetchInboxes = useFetchInboxIDs(); const removeDeadThreads = useRemoveDeadThreads(); @@ -1128,7 +1161,7 @@ JSON.stringify({}), new Set([logTypes.FARCASTER]), ); - setFarcasterDCsLoaded(true); + setLoaded(true); return; } @@ -1154,6 +1187,7 @@ conversationID, Number.POSITIVE_INFINITY, batchedUpdates, + true, increaseMessagesCount, ), dispatch, @@ -1161,7 +1195,7 @@ addLog, ); - setFarcasterDCsLoaded(true); + setLoaded(true); } catch (e) { addLog( 'Farcaster: Failed to sync conversations (full sync)', @@ -1179,12 +1213,14 @@ fetchConversationWithMessages, fetchInboxes, removeDeadThreads, - setFarcasterDCsLoaded, + setLoaded, ], ); } -function useLightweightFarcasterConversationsSync(): () => Promise { +function useLightweightFarcasterConversationsSync(): ( + withMessages: boolean, +) => Promise { const dispatch = useDispatch(); const fetchConversationWithMessages = useFetchConversationWithMessages(); const { addLog } = useDebugLogs(); @@ -1194,132 +1230,140 @@ const currentUserFID = useCurrentUserFID(); const auxUserStore = useSelector(state => state.auxUserStore); - return React.useCallback(async () => { - try { - invariant(currentUserFID, 'currentUserFID is not defined'); - const inboxResults = await Promise.all([ - fetchInboxes(), - fetchInboxes('request'), - fetchInboxes('archived'), - ]); - const allFetchSuccessful = inboxResults.every( - result => result.fetchSuccessful, - ); - const conversations = inboxResults.flatMap( - result => result.conversations, - ); - const conversationIDs = conversations.map( - conversation => conversation.conversationId, - ); + return React.useCallback( + async (withMessages: boolean) => { + try { + invariant(currentUserFID, 'currentUserFID is not defined'); + const inboxResults = await Promise.all([ + fetchInboxes(), + fetchInboxes('request'), + fetchInboxes('archived'), + ]); + const allFetchSuccessful = inboxResults.every( + result => result.fetchSuccessful, + ); + const conversations = inboxResults.flatMap( + result => result.conversations, + ); + const conversationIDs = conversations.map( + conversation => conversation.conversationId, + ); - if (allFetchSuccessful) { - removeDeadThreads(conversationIDs); - } + if (allFetchSuccessful) { + removeDeadThreads(conversationIDs); + } - const threadIDs = new Set(Object.keys(threadInfos)); - const newConversationIDs = []; - const existingConversationIDs = []; + const threadIDs = new Set(Object.keys(threadInfos)); + const newConversationIDs = []; - for (const conversationID of conversationIDs) { - if ( - threadIDs.has(farcasterThreadIDFromConversationID(conversationID)) - ) { - existingConversationIDs.push(conversationID); - } else { - newConversationIDs.push(conversationID); + for (const conversationID of conversationIDs) { + if ( + !threadIDs.has(farcasterThreadIDFromConversationID(conversationID)) + ) { + newConversationIDs.push(conversationID); + } } - } - const updateResults = conversations - .map(conversation => { - const threadID = farcasterThreadIDFromConversationID( - conversation.conversationId, - ); - const thread = threadInfos[threadID]; - if (thread && thread.farcaster) { - return createUpdatedThread( - thread, - conversation, - currentUserFID, - auxUserStore, + const updateResults = conversations + .map(conversation => { + const threadID = farcasterThreadIDFromConversationID( + conversation.conversationId, ); - } - return null; - }) - .filter(Boolean); - - const updates = updateResults - .map(result => - result && result.result === 'updated' ? result.threadInfo : null, - ) - .filter(Boolean) - .map(thread => ({ - type: updateTypes.UPDATE_THREAD, - time: thread.creationTime, - threadInfo: thread, - id: uuid.v4(), - })); - dispatch({ - type: processFarcasterOpsActionType, - payload: { - rawMessageInfos: [], - updateInfos: updates, - }, - }); + const thread = threadInfos[threadID]; + if (thread && thread.farcaster) { + return createUpdatedThread( + thread, + conversation, + currentUserFID, + auxUserStore, + ); + } + return null; + }) + .filter(Boolean); + + const updates = updateResults + .map(result => + result && result.result === 'updated' ? result.threadInfo : null, + ) + .filter(Boolean) + .map(thread => ({ + type: updateTypes.UPDATE_THREAD, + time: thread.creationTime, + threadInfo: thread, + id: uuid.v4(), + })); + dispatch({ + type: processFarcasterOpsActionType, + payload: { + rawMessageInfos: [], + updateInfos: updates, + }, + }); - const needRefetch = updateResults - .map(result => - result && result.result === 'refetch' ? result.conversationID : null, - ) - .filter(Boolean); + const needRefetch = updateResults + .map(result => + result && result.result === 'refetch' + ? result.conversationID + : null, + ) + .filter(Boolean); - if (needRefetch.length > 0) { - await processInBatchesWithReduxBatching( - needRefetch, - FARCASTER_DATA_BATCH_SIZE, - (conversationID, batchedUpdates) => - fetchConversationWithMessages(conversationID, 0, batchedUpdates), - dispatch, - undefined, - addLog, - ); - } + if (needRefetch.length > 0) { + await processInBatchesWithReduxBatching( + needRefetch, + FARCASTER_DATA_BATCH_SIZE, + (conversationID, batchedUpdates) => + fetchConversationWithMessages( + conversationID, + 0, + batchedUpdates, + false, + ), + dispatch, + undefined, + addLog, + ); + } - if (newConversationIDs.length > 0) { - await processInBatchesWithReduxBatching( - newConversationIDs, - FARCASTER_DATA_BATCH_SIZE, - (conversationID, batchedUpdates) => - fetchConversationWithMessages( - conversationID, - Number.POSITIVE_INFINITY, - batchedUpdates, - ), - dispatch, - undefined, - addLog, + if (newConversationIDs.length > 0) { + await processInBatchesWithReduxBatching( + newConversationIDs, + FARCASTER_DATA_BATCH_SIZE, + (conversationID, batchedUpdates) => + fetchConversationWithMessages( + conversationID, + withMessages ? Number.POSITIVE_INFINITY : 20, + batchedUpdates, + false, + ), + dispatch, + undefined, + addLog, + ); + } + } catch (e) { + addLog( + 'Farcaster: Failed to sync conversations (lightweight)', + JSON.stringify({ + error: getMessageForException(e), + }), + new Set([logTypes.FARCASTER]), ); + throw e; } - } catch (e) { - addLog( - 'Farcaster: Failed to sync conversations (lightweight)', - JSON.stringify({ - error: getMessageForException(e), - }), - new Set([logTypes.FARCASTER]), - ); - throw e; - } - }, [ - currentUserFID, - fetchInboxes, - removeDeadThreads, - threadInfos, - dispatch, - auxUserStore, - addLog, - fetchConversationWithMessages, - ]); + }, + [ + currentUserFID, + fetchInboxes, + removeDeadThreads, + threadInfos, + dispatch, + auxUserStore, + addLog, + fetchConversationWithMessages, + ], + ); } function useAddNewFarcasterMessage(): FarcasterMessage => Promise { @@ -1337,6 +1381,7 @@ farcasterMessage.conversationId, withMessages ? Number.POSITIVE_INFINITY : 0, updates, + true, ); dispatch({ type: processFarcasterOpsActionType, @@ -1358,6 +1403,7 @@ const { addLog } = useDebugLogs(); const currentUserSupportsDCs = useCurrentUserSupportsDCs(); const farcasterDCsLoaded = useFarcasterDCsLoaded(); + const isFarcasterSyncCanceled = useFarcasterDCsSyncCanceled(); return React.useCallback( async (farcasterMessage: FarcasterMessage) => { @@ -1375,7 +1421,7 @@ farcasterMessage.conversationId, ) ) { - if (farcasterDCsLoaded) { + if (farcasterDCsLoaded && !isFarcasterSyncCanceled) { // If DCs have already been synced, then this missing conversation is // unexpected, and we should fetch all messages for it. No need to // proceed afterwards since this message will be fetched below @@ -1448,15 +1494,16 @@ }); }, [ + currentUserSupportsDCs, + threadInfos, + fetchUsersByFIDs, addLog, + viewerID, dispatch, + farcasterDCsLoaded, + isFarcasterSyncCanceled, fetchConversation, fetchMessage, - fetchUsersByFIDs, - threadInfos, - viewerID, - currentUserSupportsDCs, - farcasterDCsLoaded, ], ); } @@ -1487,16 +1534,16 @@ } function useFarcasterSync(onComplete?: () => void): { - +inProgress: boolean, +progress: ?FarcasterSyncProgress, } { const syncFarcasterConversations = useFarcasterConversationsSync(); const currentUserSupportsDCs = useCurrentUserSupportsDCs(); const farcasterDCsLoaded = useFarcasterDCsLoaded(); + const isFarcasterSyncCanceled = useFarcasterDCsSyncCanceled(); const isUserLoggedIn = useSelector(isLoggedIn); const userDataReady = useIsUserDataReady(); const fullyLoggedIn = isUserLoggedIn && userDataReady; - const [inProgress, setInProgress] = React.useState(false); + const inProgress = React.useRef(false); const [progress, setProgress] = React.useState(null); const { socketState } = useTunnelbroker(); @@ -1519,23 +1566,26 @@ React.useEffect(() => { if ( - inProgress || + inProgress.current || farcasterDCsLoaded !== false || !fullyLoggedIn || !currentUserSupportsDCs || - !socketState.isAuthorized + !socketState.isAuthorized || + isFarcasterSyncCanceled ) { return; } - setInProgress(true); + inProgress.current = true; setProgress(null); void (async () => { try { await syncFarcasterConversations(handleProgress); } finally { - setInProgress(false); + if (inProgress.current) { + onComplete?.(); + } + inProgress.current = false; setProgress(null); - onComplete?.(); } })(); }, [ @@ -1547,9 +1597,17 @@ handleProgress, syncFarcasterConversations, socketState.isAuthorized, + isFarcasterSyncCanceled, ]); - return { inProgress, progress }; + React.useEffect(() => { + if (isFarcasterSyncCanceled && inProgress.current) { + onComplete?.(); + inProgress.current = false; + } + }, [isFarcasterSyncCanceled, onComplete]); + + return { progress }; } function useFarcasterThreadRefresher( @@ -1594,6 +1652,7 @@ const fullyLoggedIn = isUserLoggedIn && userDataReady; const currentUserSupportsDCs = useCurrentUserSupportsDCs(); const farcasterDCsLoaded = useFarcasterDCsLoaded(); + const isFarcasterSyncCanceled = useFarcasterDCsSyncCanceled(); const lightweightSync = useLightweightFarcasterConversationsSync(); const { socketState } = useTunnelbroker(); const started = React.useRef(false); @@ -1612,14 +1671,15 @@ // If we're here, it means that the full sync is not yet done. In that // case, we don't want to perform the lightweight sync during this run // of the app. - if (!farcasterDCsLoaded) { + if (!farcasterDCsLoaded && !isFarcasterSyncCanceled) { return; } - void lightweightSync(); + void lightweightSync(!isFarcasterSyncCanceled); }, [ currentUserSupportsDCs, farcasterDCsLoaded, fullyLoggedIn, + isFarcasterSyncCanceled, lightweightSync, socketState.isAuthorized, ]); @@ -1631,10 +1691,11 @@ ) => Promise { const dispatch = useDispatch(); const threadInfos = useSelector(state => state.threadStore.threadInfos); - const fetchConversationWithBatching = useFetchConversationWithBatching(); + const fetchConversationWithMessages = useFetchConversationWithMessages(); const { addLog } = useDebugLogs(); const currentUserSupportsDCs = useCurrentUserSupportsDCs(); const farcasterDCsLoaded = useFarcasterDCsLoaded(); + const isFarcasterSyncCanceled = useFarcasterDCsSyncCanceled(); return React.useCallback( async ( @@ -1678,12 +1739,22 @@ } } + const shouldFetchMessages = + farcasterDCsLoaded && !isFarcasterSyncCanceled; + const messagesNumberLimit = shouldFetchMessages + ? Number.POSITIVE_INFINITY + : 20; if (farcasterDCsLoaded && unknownConversationIds.length > 0) { await processInBatchesWithReduxBatching( unknownConversationIds, FARCASTER_DATA_BATCH_SIZE, (conversationID, batchedUpdates) => - fetchConversationWithBatching(conversationID, batchedUpdates), + fetchConversationWithMessages( + conversationID, + messagesNumberLimit, + batchedUpdates, + false, + ), dispatch, undefined, addLog, @@ -1715,10 +1786,11 @@ [ currentUserSupportsDCs, farcasterDCsLoaded, - addLog, - dispatch, - fetchConversationWithBatching, + isFarcasterSyncCanceled, threadInfos, + dispatch, + addLog, + fetchConversationWithMessages, ], ); } diff --git a/lib/shared/farcaster/farcaster-message-fetching-context.js b/lib/shared/farcaster/farcaster-message-fetching-context.js --- a/lib/shared/farcaster/farcaster-message-fetching-context.js +++ b/lib/shared/farcaster/farcaster-message-fetching-context.js @@ -149,11 +149,12 @@ : Promise.resolve(null); const farcasterPromise = !state.farcasterExhausted - ? fetchFarcasterMessagesForConversation( + ? fetchFarcasterMessagesForConversation({ conversationID, - numMessages, - state.farcasterCursor ?? null, - ) + messagesNumberLimit: numMessages, + cursor: state.farcasterCursor ?? null, + canBeCanceled: false, + }) : Promise.resolve(null); const [dbResult, farcasterResult] = await Promise.allSettled([ diff --git a/lib/tunnelbroker/use-peer-to-peer-message-handler.js b/lib/tunnelbroker/use-peer-to-peer-message-handler.js --- a/lib/tunnelbroker/use-peer-to-peer-message-handler.js +++ b/lib/tunnelbroker/use-peer-to-peer-message-handler.js @@ -61,7 +61,7 @@ import { getMessageForException } from '../utils/errors.js'; import { useClearFarcasterThreads, - useSetFarcasterDCsLoaded, + useSetFarcasterDCsSyncStatus, useSetLocalCurrentUserSupportsDCs, useSetLocalFID, } from '../utils/farcaster-utils.js'; @@ -134,7 +134,7 @@ ); const fullBackupSupport = useFullBackupSupportEnabled(); - const setFarcasterDCsLoaded = useSetFarcasterDCsLoaded(); + const { setLoaded } = useSetFarcasterDCsSyncStatus(); return React.useCallback( async ( @@ -267,7 +267,7 @@ if (!userActionMessage.hasDCsToken) { clearFarcasterThreads(); } - setFarcasterDCsLoaded(false); + setLoaded(false); } finally { await removeAndConfirmMessage(messageID, senderInfo.deviceID); } @@ -293,7 +293,7 @@ userDataRestore, setLocalFID, setLocalDCsSupport, - setFarcasterDCsLoaded, + setLoaded, clearFarcasterThreads, ], ); diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -143,6 +143,7 @@ import type { SyncedMetadataStore, SetSyncedMetadataEntryPayload, + SetSyncedMetadataEntriesPayload, ClearSyncedMetadataEntryPayload, } from './synced-metadata-types.js'; import type { GlobalThemeInfo } from './theme-types.js'; @@ -1388,6 +1389,10 @@ +type: 'SET_SYNCED_METADATA_ENTRY', +payload: SetSyncedMetadataEntryPayload, } + | { + +type: 'SET_SYNCED_METADATA_ENTRIES', + +payload: SetSyncedMetadataEntriesPayload, + } | { +type: 'CLEAR_SYNCED_METADATA_ENTRY', +payload: ClearSyncedMetadataEntryPayload, diff --git a/lib/types/synced-metadata-types.js b/lib/types/synced-metadata-types.js --- a/lib/types/synced-metadata-types.js +++ b/lib/types/synced-metadata-types.js @@ -10,6 +10,7 @@ CURRENT_USER_FID: 'current_user_fid', CURRENT_USER_SUPPORTS_DCS: 'current_user_supports_dcs', FARCASTER_DCS_LOADED: 'farcaster_dcs_loaded', + FARCASTER_DCS_SYNC_CANCELED: 'farcaster_dcs_sync_canceled', STORE_VERSION: 'store_version', ENABLED_APPS: 'enabled_apps', GLOBAL_THEME_INFO: 'global_theme_info', @@ -23,6 +24,13 @@ +data: string, }; +export type SetSyncedMetadataEntriesPayload = { + +entries: $ReadOnlyArray<{ + +name: SyncedMetadataName, + +data: string, + }>, +}; + export type ClearSyncedMetadataEntryPayload = { +name: SyncedMetadataName, }; diff --git a/lib/utils/farcaster-utils.js b/lib/utils/farcaster-utils.js --- a/lib/utils/farcaster-utils.js +++ b/lib/utils/farcaster-utils.js @@ -7,7 +7,10 @@ import { getConfig } from './config.js'; import { getContentSigningKey } from './crypto-utils.js'; import { useDispatch, useSelector } from './redux-utils.js'; -import { setSyncedMetadataEntryActionType } from '../actions/synced-metadata-actions.js'; +import { + setSyncedMetadataEntryActionType, + setSyncedMetadataEntriesActionType, +} from '../actions/synced-metadata-actions.js'; import { useUserIdentityCache } from '../components/user-identity-cache.react.js'; import { getOwnPeerDevices } from '../selectors/user-selectors.js'; import { processFarcasterOpsActionType } from '../shared/farcaster/farcaster-actions.js'; @@ -68,6 +71,17 @@ return undefined; } +function useFarcasterDCsSyncCanceled(): boolean { + const farcasterDCsSyncCanceled = useSelector( + state => + state.syncedMetadataStore.syncedMetadata[ + syncedMetadataNames.FARCASTER_DCS_SYNC_CANCELED + ], + ); + + return farcasterDCsSyncCanceled === 'true'; +} + function useSetLocalFID(): (fid: ?string) => void { const dispatch = useDispatch(); const { invalidateCacheForUser } = useUserIdentityCache(); @@ -114,20 +128,69 @@ ); } -function useSetFarcasterDCsLoaded(): (loaded: boolean) => void { +function useSetFarcasterDCsSyncStatus(): { + +setLoaded: (loaded: boolean) => void, + +setCanceled: (canceled: boolean) => void, +} { const dispatch = useDispatch(); - return React.useCallback( + + const setLoaded = React.useCallback( (loaded: boolean) => { - dispatch({ - type: setSyncedMetadataEntryActionType, - payload: { + const entries: Array<{ +name: string, +data: string }> = [ + { name: syncedMetadataNames.FARCASTER_DCS_LOADED, data: String(loaded), }, + ]; + if (!loaded) { + entries.push({ + name: syncedMetadataNames.FARCASTER_DCS_SYNC_CANCELED, + data: String(false), + }); + } + dispatch({ + type: setSyncedMetadataEntriesActionType, + payload: { entries }, }); }, [dispatch], ); + + const setCanceled = React.useCallback( + (canceled: boolean) => { + const entries: Array<{ +name: string, +data: string }> = [ + { + name: syncedMetadataNames.FARCASTER_DCS_SYNC_CANCELED, + data: String(canceled), + }, + ]; + if (canceled) { + entries.push({ + name: syncedMetadataNames.FARCASTER_DCS_LOADED, + data: String(true), + }); + } + dispatch({ + type: setSyncedMetadataEntriesActionType, + payload: { entries }, + }); + }, + [dispatch], + ); + + return React.useMemo( + () => ({ setLoaded, setCanceled }), + [setLoaded, setCanceled], + ); +} + +function useFarcasterDCsSyncCancellationRef(): { +current: boolean } { + const isCanceled = useFarcasterDCsSyncCanceled(); + const canceledRef = React.useRef(isCanceled); + React.useEffect(() => { + canceledRef.current = isCanceled; + }, [isCanceled]); + return canceledRef; } function useLinkFID(): (fid: string) => Promise { @@ -154,9 +217,9 @@ function useClearFarcasterThreads(): () => void { const threads = useSelector(state => state.threadStore.threadInfos); const dispatch = useDispatch(); - const setFarcasterDCsLoaded = useSetFarcasterDCsLoaded(); + const { setLoaded } = useSetFarcasterDCsSyncStatus(); return React.useCallback(() => { - setFarcasterDCsLoaded(false); + setLoaded(false); const farcasterThreadIDs = Object.values(threads) .filter(thread => thread.farcaster) .map(thread => thread.id); @@ -174,7 +237,7 @@ updateInfos: updates, }, }); - }, [dispatch, setFarcasterDCsLoaded, threads]); + }, [dispatch, setLoaded, threads]); } function useUnlinkFID(): () => Promise { @@ -218,19 +281,19 @@ const setLocalDCsSupport = useSetLocalCurrentUserSupportsDCs(); const broadcastConnectionStatus = useBroadcastUpdateFarcasterConnectionStatus(); - const setFarcasterDCsLoaded = useSetFarcasterDCsLoaded(); + const { setLoaded } = useSetFarcasterDCsSyncStatus(); return React.useCallback( async (fid: string, farcasterDCsToken: string) => { await linkFarcasterDCsAccount(fid, farcasterDCsToken); setLocalDCsSupport(true); - setFarcasterDCsLoaded(false); + setLoaded(false); await broadcastConnectionStatus(fid, true); }, [ linkFarcasterDCsAccount, setLocalDCsSupport, - setFarcasterDCsLoaded, + setLoaded, broadcastConnectionStatus, ], ); @@ -301,18 +364,48 @@ ); } +function useCancelFarcasterDCsSync(): () => void { + const { setCanceled } = useSetFarcasterDCsSyncStatus(); + const { showAlert } = getConfig(); + + return React.useCallback(() => { + showAlert( + 'Cancel Farcaster fetching', + 'Are you sure you want to cancel the Farcaster conversations fetching?' + + ' You can always restart it later from your profile screen.', + [ + { + text: 'No', + style: 'cancel', + }, + { + text: 'Yes', + style: 'destructive', + onPress: () => setCanceled(true), + }, + ], + { + cancelable: true, + }, + ); + }, [setCanceled, showAlert]); +} + export { DISABLE_CONNECT_FARCASTER_ALERT, NO_FID_METADATA, useCurrentUserFID, useCurrentUserSupportsDCs, useFarcasterDCsLoaded, + useFarcasterDCsSyncCanceled, useSetLocalFID, useSetLocalCurrentUserSupportsDCs, - useSetFarcasterDCsLoaded, + useSetFarcasterDCsSyncStatus, useLinkFID, useUnlinkFID, useLinkFarcasterDCs, createFarcasterDCsAuthMessage, useClearFarcasterThreads, + useFarcasterDCsSyncCancellationRef, + useCancelFarcasterDCsSync, }; diff --git a/native/account/registration/registration-terms.react.js b/native/account/registration/registration-terms.react.js --- a/native/account/registration/registration-terms.react.js +++ b/native/account/registration/registration-terms.react.js @@ -5,7 +5,7 @@ import { Text, View, Image, Linking } from 'react-native'; import type { SignedMessage } from 'lib/types/siwe-types.js'; -import { useSetFarcasterDCsLoaded } from 'lib/utils/farcaster-utils.js'; +import { useSetFarcasterDCsSyncStatus } from 'lib/utils/farcaster-utils.js'; import type { AuthNavigationProp } from './auth-navigator.react.js'; import { RegistrationContext } from './registration-context.js'; @@ -105,7 +105,7 @@ ); }, [setCachedSelections, navigateToConnectEthereum]); - const setFarcasterDCsLoaded = useSetFarcasterDCsLoaded(); + const { setLoaded } = useSetFarcasterDCsSyncStatus(); const onProceed = React.useCallback(async () => { setRegistrationInProgress(true); try { @@ -115,7 +115,7 @@ onNonceExpired, }); if (userSelections.farcasterDCsToken) { - setFarcasterDCsLoaded(false); + setLoaded(false); } } finally { setRegistrationInProgress(false); @@ -125,7 +125,7 @@ userSelections, clearCachedSelections, onNonceExpired, - setFarcasterDCsLoaded, + setLoaded, ]); usePreventUserFromLeavingScreen(registrationInProgress); diff --git a/native/components/farcaster-sync-handler.react.js b/native/components/farcaster-sync-handler.react.js --- a/native/components/farcaster-sync-handler.react.js +++ b/native/components/farcaster-sync-handler.react.js @@ -9,6 +9,7 @@ import { useCurrentUserSupportsDCs, useFarcasterDCsLoaded, + useFarcasterDCsSyncCanceled, } from 'lib/utils/farcaster-utils.js'; import { useSelector } from 'lib/utils/redux-utils.js'; @@ -25,6 +26,7 @@ const currentUserSupportsDCs = useCurrentUserSupportsDCs(); const farcasterDCsLoaded = useFarcasterDCsLoaded(); + const isFarcasterSyncCanceled = useFarcasterDCsSyncCanceled(); const currentRouteName = useCurrentLeafRouteName(); useLightweightSyncOnAppStart(); @@ -40,7 +42,8 @@ fullyLoggedIn && currentUserSupportsDCs && farcasterDCsLoaded === false && - currentRouteName !== FarcasterSyncScreenRouteName + currentRouteName !== FarcasterSyncScreenRouteName && + !isFarcasterSyncCanceled ) { dispatch( CommonActions.navigate({ @@ -55,6 +58,7 @@ farcasterDCsLoaded, currentRouteName, fullyLoggedIn, + isFarcasterSyncCanceled, ]); return null; diff --git a/native/farcaster/farcaster-sync-loading-screen.react.js b/native/farcaster/farcaster-sync-loading-screen.react.js --- a/native/farcaster/farcaster-sync-loading-screen.react.js +++ b/native/farcaster/farcaster-sync-loading-screen.react.js @@ -6,7 +6,9 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { useFarcasterSync } from 'lib/shared/farcaster/farcaster-hooks.js'; +import { useCancelFarcasterDCsSync } from 'lib/utils/farcaster-utils.js'; +import PrimaryButton from '../components/primary-button.react.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useColors, useStyles } from '../themes/colors.js'; @@ -25,6 +27,7 @@ }, [props.navigation]); const { progress } = useFarcasterSync(handleComplete); + const cancelSync = useCancelFarcasterDCsSync(); const progressValue = progress ? progress.completedConversations / progress.totalNumberOfConversations @@ -32,67 +35,76 @@ return ( - Fetching Farcaster conversations - - - 1. - - Fetching in progress: Comm is - fetching all of your Farcaster messages so they can be backed up. - This can take a while, depending on how many chats you have. - + + Fetching Farcaster conversations + + + 1. + + Fetching in progress: Comm is + fetching all of your Farcaster messages so they can be backed up. + This can take a while, depending on how many chats you have. + + + + 2. + + No E2E encryption: Please note + that Farcaster messages are not end-to-end encrypted, which means + the Farcaster team can see them. For better security, consider + using Comm DMs. + + + + 3. + + Manual refresh: If you ever + notice any missing messages, you can manually refresh all + Farcaster chats from your profile screen, or refresh an individual + chat from its settings. + + - - 2. - - No E2E encryption: Please note that - Farcaster messages are not end-to-end encrypted, which means the - Farcaster team can see them. For better security, consider using - Comm DMs. - - - - 3. - - Manual refresh: If you ever notice - any missing messages, you can manually refresh all Farcaster chats - from your profile screen, or refresh an individual chat from its - settings. - - - - - {progress ? ( - <> - + {progress ? ( + <> + + + + {progress.completedConversations} of{' '} + {progress.totalNumberOfConversations} conversations fetched + + + + + {progress.completedMessages + ? `${progress.completedMessages.toLocaleString()} messages fetched` + : null} + + + + ) : ( + - - - {progress.completedConversations} of{' '} - {progress.totalNumberOfConversations} conversations fetched - - - - - {progress.completedMessages - ? `${progress.completedMessages.toLocaleString()} messages fetched` - : null} - - - - ) : ( - - )} + )} + + + + ); @@ -104,8 +116,11 @@ container: { flex: 1, backgroundColor: 'panelBackground', - justifyContent: 'space-between', + }, + contentContainer: { + flex: 1, padding: 16, + justifyContent: 'space-between', }, header: { fontSize: 24, @@ -148,6 +163,10 @@ color: 'panelForegroundSecondaryLabel', textAlign: 'center', }, + buttonContainer: { + marginVertical: 8, + marginHorizontal: 16, + }, }; export default FarcasterSyncLoadingScreen; diff --git a/native/profile/profile-screen.react.js b/native/profile/profile-screen.react.js --- a/native/profile/profile-screen.react.js +++ b/native/profile/profile-screen.react.js @@ -26,7 +26,7 @@ import { type CurrentUserInfo } from 'lib/types/user-types.js'; import { useCurrentUserFID, - useSetFarcasterDCsLoaded, + useSetFarcasterDCsSyncStatus, } from 'lib/utils/farcaster-utils.js'; import { useDispatchActionPromise, @@ -640,10 +640,10 @@ }, [checkIfPrimaryDevice]); const usingRestoreFlow = useIsRestoreFlowEnabled(); - const setDCsLoaded = useSetFarcasterDCsLoaded(); + const { setLoaded } = useSetFarcasterDCsSyncStatus(); const syncFarcasterConversations = React.useCallback(() => { - setDCsLoaded(false); - }, [setDCsLoaded]); + setLoaded(false); + }, [setLoaded]); const supportsFarcasterDCs = useIsFarcasterDCsIntegrationEnabled(); return ( diff --git a/web/components/farcaster-sync-overlay.react.js b/web/components/farcaster-sync-overlay.react.js --- a/web/components/farcaster-sync-overlay.react.js +++ b/web/components/farcaster-sync-overlay.react.js @@ -6,6 +6,7 @@ import { useCurrentUserSupportsDCs, useFarcasterDCsLoaded, + useFarcasterDCsSyncCanceled, } from 'lib/utils/farcaster-utils.js'; import FarcasterSyncLoadingScreen from '../farcaster/farcaster-sync-loading-screen.react.js'; @@ -17,10 +18,13 @@ function FarcasterSyncOverlay(props: Props): React.Node { const { children } = props; const farcasterDCsLoaded = useFarcasterDCsLoaded(); + const isFarcasterSyncCanceled = useFarcasterDCsSyncCanceled(); const currentUserSupportsDCs = useCurrentUserSupportsDCs(); const isFullSyncInProgress = - currentUserSupportsDCs && farcasterDCsLoaded === false; + currentUserSupportsDCs && + farcasterDCsLoaded === false && + !isFarcasterSyncCanceled; useLightweightSyncOnAppStart(); diff --git a/web/settings/account-settings.react.js b/web/settings/account-settings.react.js --- a/web/settings/account-settings.react.js +++ b/web/settings/account-settings.react.js @@ -26,7 +26,7 @@ createOlmSessionsWithOwnDevices, getContentSigningKey, } from 'lib/utils/crypto-utils.js'; -import { useSetFarcasterDCsLoaded } from 'lib/utils/farcaster-utils.js'; +import { useSetFarcasterDCsSyncStatus } from 'lib/utils/farcaster-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useIsFarcasterDCsIntegrationEnabled, @@ -183,10 +183,10 @@ [pushModal], ); - const setDCsLoaded = useSetFarcasterDCsLoaded(); + const { setLoaded } = useSetFarcasterDCsSyncStatus(); const syncFarcasterConversations = React.useCallback(() => { - setDCsLoaded(false); - }, [setDCsLoaded]); + setLoaded(false); + }, [setLoaded]); if (!currentUserInfo || currentUserInfo.anonymous) { return null;