diff --git a/lib/shared/farcaster/farcaster-hooks.js b/lib/shared/farcaster/farcaster-hooks.js new file mode 100644 --- /dev/null +++ b/lib/shared/farcaster/farcaster-hooks.js @@ -0,0 +1,128 @@ +// @flow + +import * as React from 'react'; + +import { + type FetchFarcasterConversationResult, + useFetchFarcasterConversation, + useFetchFarcasterInbox, + type FetchFarcasterMessageResult, + useFetchFarcasterMessages, + useSendFarcasterTextMessage, +} from './farcaster-api.js'; +import type { FarcasterConversation } from './farcaster-conversation-types.js'; +import type { FarcasterMessage } from './farcaster-messages-types.js'; +import { useDispatch } from '../../utils/redux-utils.js'; +import { useSendDMOperationUtils } from '../dm-ops/dm-op-utils.js'; + +function useFarcasterConversationsSync(): () => Promise { + const [fullInbox, setFullInbox] = React.useState(false); + const [conversations, setConversations] = React.useState< + $ReadOnlyArray, + >([]); + + const fetchFarcasterInbox = useFetchFarcasterInbox(); + const fetchFarcasterConversation = useFetchFarcasterConversation(); + const fetchFarcasterMessages = useFetchFarcasterMessages(); + const sendFarcasterTextMessage = useSendFarcasterTextMessage(); + const dispatch = useDispatch(); + const utils = useSendDMOperationUtils(); + + const fetchInboxes: (cursor: ?string) => Promise = React.useCallback( + async (cursor: ?string) => { + try { + let input = { limit: 20 }; + if (cursor) { + input = { + ...input, + cursor, + }; + } + const { result, next } = await fetchFarcasterInbox(input); + setConversations(prev => { + const ids = result.conversations.map( + conversation => conversation.conversationId, + ); + return [...prev, ...ids]; + }); + if (next?.cursor) { + void fetchInboxes(next.cursor); + } else { + setFullInbox(true); + } + } catch (e) { + console.error('Error fetching inbox', e); + setFullInbox(true); + } + }, + [fetchFarcasterInbox], + ); + + React.useEffect(() => { + if (!fullInbox || conversations.length === 0) { + return; + } + + void (async () => { + const conversationPromises: $ReadOnlyArray< + Promise, + > = conversations.map(async conversationId => { + try { + return await fetchFarcasterConversation({ conversationId }); + } catch (e) { + console.error(`Failed fetching conversation ${conversationId}:`, e); + return null; + } + }); + + const farcasterConversationsResults: $ReadOnlyArray = + await Promise.all(conversationPromises); + + const farcasterConversations: $ReadOnlyArray = + farcasterConversationsResults + .filter(Boolean) + .map(conversationResult => conversationResult.result.conversation); + + console.log('Farcaster conversations:', farcasterConversations); + + const messagePromises: $ReadOnlyArray< + Promise, + > = farcasterConversations.map(async ({ conversationId }) => { + try { + return await fetchFarcasterMessages({ conversationId, limit: 30 }); + } catch (e) { + console.error(`Failed fetching messages for ${conversationId}:`, e); + return null; + } + }); + + const farcasterMessagesResult: $ReadOnlyArray = + await Promise.all(messagePromises); + + const farcasterMessages: $ReadOnlyArray = + farcasterMessagesResult + .filter(Boolean) + .flatMap(messagesResult => messagesResult.result.messages); + + console.log('Farcaster messages:', farcasterMessages); + + //TODO: dispatch threads and messages + setConversations([]); + })(); + }, [ + conversations, + dispatch, + fetchFarcasterConversation, + fetchFarcasterMessages, + fullInbox, + sendFarcasterTextMessage, + utils, + ]); + + return React.useCallback(async () => { + setFullInbox(false); + void fetchInboxes(null); + }, [fetchInboxes]); +} + +export { useFarcasterConversationsSync }; 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 @@ -20,6 +20,7 @@ dmOperationSpecificationTypes, } from 'lib/shared/dm-ops/dm-op-types.js'; import { useProcessAndSendDMOperation } from 'lib/shared/dm-ops/process-dm-ops.js'; +import { useFarcasterConversationsSync } from 'lib/shared/farcaster/farcaster-hooks.js'; import type { LogOutResult } from 'lib/types/account-types.js'; import type { DMCreateThreadOperation } from 'lib/types/dm-ops'; import { thickThreadTypes } from 'lib/types/thread-types-enum.js'; @@ -29,7 +30,10 @@ useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; -import { useIsRestoreFlowEnabled } from 'lib/utils/services-utils.js'; +import { + supportsFarcasterDCs, + useIsRestoreFlowEnabled, +} from 'lib/utils/services-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import { deleteNativeCredentialsFor } from '../account/native-credentials.js'; @@ -182,6 +186,7 @@ +onCreateDMThread: () => Promise, +currentUserFID: ?string, +usingRestoreFlow: boolean, + +farcasterConversationsSync: () => Promise, }; class ProfileScreen extends React.PureComponent { @@ -279,6 +284,17 @@ ); } + let farcaster; + if (staffCanSee && supportsFarcasterDCs) { + farcaster = ( + <> + + + ); + } return ( @@ -338,6 +354,7 @@ {developerTools} {dmActions} + {farcaster} {debugLogs} @@ -544,6 +561,10 @@ void this.props.onCreateDMThread(); }; + onPressFarcasterConversationsSync = () => { + void this.props.farcasterConversationsSync(); + }; + onPressDebugLogs = () => { this.props.navigation.navigate({ name: DebugLogsScreenRouteName }); }; @@ -616,6 +637,7 @@ }, [checkIfPrimaryDevice]); const usingRestoreFlow = useIsRestoreFlowEnabled(); + const farcasterConversationsSync = useFarcasterConversationsSync(); return ( ); }); 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 @@ -18,6 +18,7 @@ type OutboundDMOperationSpecification, } from 'lib/shared/dm-ops/dm-op-types.js'; import { useProcessAndSendDMOperation } from 'lib/shared/dm-ops/process-dm-ops.js'; +import { useFarcasterConversationsSync } from 'lib/shared/farcaster/farcaster-hooks.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { DMCreateThreadOperation } from 'lib/types/dm-ops.js'; @@ -27,7 +28,10 @@ getContentSigningKey, } from 'lib/utils/crypto-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; -import { useIsRestoreFlowEnabled } from 'lib/utils/services-utils.js'; +import { + supportsFarcasterDCs, + useIsRestoreFlowEnabled, +} from 'lib/utils/services-utils.js'; import css from './account-settings.css'; import AppearanceChangeModal from './appearance-change-modal.react.js'; @@ -178,6 +182,8 @@ [pushModal], ); + const farcasterConversationsSync = useFarcasterConversationsSync(); + if (!currentUserInfo || currentUserInfo.anonymous) { return null; } @@ -276,6 +282,25 @@ ); } + let farcaster; + if (staffCanSee && supportsFarcasterDCs) { + farcaster = ( +
+

Farcaster menu

+
+
    +
  • + Farcaster DCs integration + +
  • +
+
+
+ ); + } + let debugLogs; if (staffCanSee) { debugLogs = ( @@ -329,6 +354,7 @@ {tunnelbroker} {deviceData} {dms} + {farcaster} {debugLogs}