diff --git a/lib/components/farcaster-data-handler.react.js b/lib/components/farcaster-data-handler.react.js index e98f24b66..efe08e7e9 100644 --- a/lib/components/farcaster-data-handler.react.js +++ b/lib/components/farcaster-data-handler.react.js @@ -1,149 +1,147 @@ // @flow import * as React from 'react'; import { setAuxUserFIDsActionType } from '../actions/aux-user-actions.js'; import { updateRelationships as serverUpdateRelationships, updateRelationshipsActionTypes, } from '../actions/relationship-actions.js'; import { NeynarClientContext } from '../components/neynar-client-provider.react.js'; import { useLegacyAshoatKeyserverCall } from '../keyserver-conn/legacy-keyserver-call.js'; -import { isLoggedInToAuthoritativeKeyserver } from '../selectors/user-selectors.js'; +import { isLoggedInToIdentityAndAuthoritativeKeyserver } from '../selectors/user-selectors.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { relationshipActions } from '../types/relationship-types.js'; import { useCurrentUserFID } from '../utils/farcaster-utils.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector, useDispatch } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; function FarcasterDataHandler(): React.Node { const isActive = useSelector(state => state.lifecycleState !== 'background'); - const isLoggedInToAuthKeyserver = useSelector( - isLoggedInToAuthoritativeKeyserver, - ); const currentUserID = useSelector(state => state.currentUserInfo?.id); - const loggedIn = !!currentUserID && isLoggedInToAuthKeyserver; + + const loggedIn = useSelector(isLoggedInToIdentityAndAuthoritativeKeyserver); const neynarClient = React.useContext(NeynarClientContext)?.client; const identityServiceClient = React.useContext(IdentityClientContext); const getFarcasterUsers = identityServiceClient?.identityClient.getFarcasterUsers; const findUserIdentities = identityServiceClient?.identityClient.findUserIdentities; const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const updateRelationships = useLegacyAshoatKeyserverCall( serverUpdateRelationships, ); const createThreadsAndRobotextForFarcasterMutuals = React.useCallback( (userIDsToFID: { +[userID: string]: string }) => updateRelationships({ action: relationshipActions.FARCASTER_MUTUAL, userIDsToFID, }), [updateRelationships], ); const userInfos = useSelector(state => state.userStore.userInfos); const fid = useCurrentUserFID(); const prevCanQueryRef = React.useRef(); const handleFarcasterMutuals = React.useCallback(async () => { const canQuery = isActive && !!fid && loggedIn; if (canQuery === prevCanQueryRef.current) { return; } prevCanQueryRef.current = canQuery; if ( !loggedIn || !isActive || !fid || !neynarClient || !getFarcasterUsers || !currentUserID ) { return; } const followerFIDs = await neynarClient.fetchFriendFIDs(fid); const commFCUsers = await getFarcasterUsers(followerFIDs); const newCommUsers = commFCUsers.filter(({ userID }) => !userInfos[userID]); if (newCommUsers.length === 0) { return; } const userIDsToFID: { +[userID: string]: string } = Object.fromEntries( newCommUsers.map(({ userID, farcasterID }) => [userID, farcasterID]), ); const userIDsToFIDIncludingCurrentUser: { +[userID: string]: string } = { ...userIDsToFID, [(currentUserID: string)]: fid, }; void dispatchActionPromise( updateRelationshipsActionTypes, createThreadsAndRobotextForFarcasterMutuals( userIDsToFIDIncludingCurrentUser, ), ); }, [ isActive, fid, loggedIn, neynarClient, getFarcasterUsers, userInfos, dispatchActionPromise, createThreadsAndRobotextForFarcasterMutuals, currentUserID, ]); const handleUserStoreFIDs = React.useCallback(async () => { if (!loggedIn || !isActive || !findUserIdentities) { return; } const userStoreIDs = Object.keys(userInfos); const userIdentities = await findUserIdentities(userStoreIDs); const userStoreFarcasterUsers = Object.entries(userIdentities) .filter(([, identity]) => identity.farcasterID !== null) .map(([userID, identity]) => ({ userID, username: identity.username, farcasterID: identity.farcasterID, })); dispatch({ type: setAuxUserFIDsActionType, payload: { farcasterUsers: userStoreFarcasterUsers, }, }); }, [loggedIn, isActive, findUserIdentities, userInfos, dispatch]); React.useEffect(() => { if (!usingCommServicesAccessToken) { return; } void handleFarcasterMutuals(); void handleUserStoreFIDs(); }, [handleFarcasterMutuals, handleUserStoreFIDs]); return null; } export { FarcasterDataHandler }; diff --git a/lib/selectors/user-selectors.js b/lib/selectors/user-selectors.js index 9091b8afc..900e5b2ef 100644 --- a/lib/selectors/user-selectors.js +++ b/lib/selectors/user-selectors.js @@ -1,281 +1,287 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; import bots from '../facts/bots.js'; import { getAvatarForUser, getRandomDefaultEmojiAvatar, } from '../shared/avatar-utils.js'; import { getSingleOtherUser } from '../shared/thread-utils.js'; import { type AuxUserInfos, type AuxUserInfo, } from '../types/aux-user-types.js'; import type { ClientEmojiAvatar } from '../types/avatar-types'; import type { RelativeMemberInfo, RawThreadInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import type { BaseAppState } from '../types/redux-types.js'; import { userRelationshipStatus } from '../types/relationship-types.js'; import { threadTypes } from '../types/thread-types-enum.js'; import type { RawThreadInfos } from '../types/thread-types.js'; import type { UserInfos, RelativeUserInfo, AccountUserInfo, CurrentUserInfo, } from '../types/user-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { entries, values } from '../utils/objects.js'; // Used for specific message payloads that include an array of user IDs, ie. // array of initial users, array of added users function userIDsToRelativeUserInfos( userIDs: $ReadOnlyArray, viewerID: ?string, userInfos: UserInfos, ): RelativeUserInfo[] { const relativeUserInfos: RelativeUserInfo[] = []; for (const userID of userIDs) { const username = userInfos[userID] ? userInfos[userID].username : null; const relativeUserInfo = { id: userID, username, isViewer: userID === viewerID, }; if (userID === viewerID) { relativeUserInfos.unshift(relativeUserInfo); } else { relativeUserInfos.push(relativeUserInfo); } } return relativeUserInfos; } function getRelativeMemberInfos( threadInfo: ?RawThreadInfo, currentUserID: ?string, userInfos: UserInfos, ): $ReadOnlyArray { const relativeMemberInfos: RelativeMemberInfo[] = []; if (!threadInfo) { return relativeMemberInfos; } const memberInfos = threadInfo.members; for (const memberInfo of memberInfos) { if (!memberInfo.role) { continue; } const username = userInfos[memberInfo.id] ? userInfos[memberInfo.id].username : null; const { id, role, isSender, minimallyEncoded } = memberInfo; if (memberInfo.id === currentUserID) { relativeMemberInfos.unshift({ id, role, isSender, minimallyEncoded, username, isViewer: true, }); } else { relativeMemberInfos.push({ id, role, isSender, minimallyEncoded, username, isViewer: false, }); } } return relativeMemberInfos; } const emptyArray: $ReadOnlyArray = []; // Includes current user at the start const baseRelativeMemberInfoSelectorForMembersOfThread: ( threadID: ?string, ) => (state: BaseAppState<>) => $ReadOnlyArray = ( threadID: ?string, ) => { if (!threadID) { return () => emptyArray; } return createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos[threadID], (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, (state: BaseAppState<>) => state.userStore.userInfos, getRelativeMemberInfos, ); }; const relativeMemberInfoSelectorForMembersOfThread: ( threadID: ?string, ) => (state: BaseAppState<>) => $ReadOnlyArray = _memoize( baseRelativeMemberInfoSelectorForMembersOfThread, ); const userInfoSelectorForPotentialMembers: (state: BaseAppState<>) => { [id: string]: AccountUserInfo, } = createSelector( (state: BaseAppState<>) => state.userStore.userInfos, (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, ( userInfos: UserInfos, currentUserID: ?string, ): { [id: string]: AccountUserInfo } => { const availableUsers: { [id: string]: AccountUserInfo } = {}; for (const id in userInfos) { const { username, relationshipStatus } = userInfos[id]; if (id === currentUserID || !username) { continue; } if ( relationshipStatus !== userRelationshipStatus.BLOCKED_VIEWER && relationshipStatus !== userRelationshipStatus.BOTH_BLOCKED ) { availableUsers[id] = { id, username, relationshipStatus }; } } return availableUsers; }, ); const isLoggedIn = (state: BaseAppState<>): boolean => !!( state.currentUserInfo && !state.currentUserInfo.anonymous && state.dataLoaded ); const isLoggedInToKeyserver: ( keyserverID: ?string, ) => (state: BaseAppState<>) => boolean = _memoize( (keyserverID: ?string) => (state: BaseAppState<>) => { if (!keyserverID) { return false; } const cookie = state.keyserverStore.keyserverInfos[keyserverID]?.cookie; return !!cookie && cookie.startsWith('user='); }, ); const isLoggedInToAuthoritativeKeyserver: (state: BaseAppState<>) => boolean = isLoggedInToKeyserver(authoritativeKeyserverID()); +const isLoggedInToIdentityAndAuthoritativeKeyserver: ( + state: BaseAppState<>, +) => boolean = (state: BaseAppState<>) => + isLoggedInToAuthoritativeKeyserver(state) && isLoggedIn(state); + const usersWithPersonalThreadSelector: ( state: BaseAppState<>, ) => $ReadOnlySet = createSelector( (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, (state: BaseAppState<>) => state.threadStore.threadInfos, (viewerID: ?string, threadInfos: RawThreadInfos) => { const personalThreadMembers = new Set(); for (const threadID in threadInfos) { const thread = threadInfos[threadID]; if ( thread.type !== threadTypes.GENESIS_PERSONAL || !thread.members.find(member => member.id === viewerID) ) { continue; } const otherMemberID = getSingleOtherUser(thread, viewerID); if (otherMemberID) { personalThreadMembers.add(otherMemberID); } } return personalThreadMembers; }, ); const savedEmojiAvatarSelectorForCurrentUser: ( state: BaseAppState<>, ) => () => ClientEmojiAvatar = createSelector( (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo, (currentUser: ?CurrentUserInfo) => { return () => { let userAvatar = getAvatarForUser(currentUser); if (userAvatar.type !== 'emoji') { userAvatar = getRandomDefaultEmojiAvatar(); } return userAvatar; }; }, ); const getRelativeUserIDs: (state: BaseAppState<>) => $ReadOnlyArray = createSelector( (state: BaseAppState<>) => state.userStore.userInfos, (userInfos: UserInfos): $ReadOnlyArray => Object.keys(userInfos), ); const usersWithMissingDeviceListSelector: ( state: BaseAppState<>, ) => $ReadOnlyArray = createSelector( getRelativeUserIDs, (state: BaseAppState<>) => state.auxUserStore.auxUserInfos, ( userIDs: $ReadOnlyArray, auxUserInfos: AuxUserInfos, ): $ReadOnlyArray => userIDs.filter( userID => (!auxUserInfos[userID] || !auxUserInfos[userID].deviceList) && userID !== bots.commbot.userID, ), ); // Foreign Peer Devices are all devices of users we are aware of, // but not our own devices. const getForeignPeerDevices: (state: BaseAppState<>) => $ReadOnlyArray = createSelector( (state: BaseAppState<>) => state.auxUserStore.auxUserInfos, (state: BaseAppState<>) => state.currentUserInfo && state.currentUserInfo.id, ( auxUserInfos: AuxUserInfos, currentUserID: ?string, ): $ReadOnlyArray => entries(auxUserInfos) .map(([userID, auxUserInfo]: [string, AuxUserInfo]) => userID !== currentUserID && auxUserInfo.deviceList?.devices ? auxUserInfo.deviceList.devices : [], ) .flat(), ); const getAllPeerDevices: (state: BaseAppState<>) => $ReadOnlyArray = createSelector( (state: BaseAppState<>) => state.auxUserStore.auxUserInfos, (auxUserInfos: AuxUserInfos): $ReadOnlyArray => values(auxUserInfos) .map( (auxUserInfo: AuxUserInfo) => auxUserInfo.deviceList?.devices ?? [], ) .flat(), ); export { userIDsToRelativeUserInfos, getRelativeMemberInfos, relativeMemberInfoSelectorForMembersOfThread, userInfoSelectorForPotentialMembers, isLoggedIn, isLoggedInToKeyserver, isLoggedInToAuthoritativeKeyserver, + isLoggedInToIdentityAndAuthoritativeKeyserver, usersWithPersonalThreadSelector, savedEmojiAvatarSelectorForCurrentUser, getRelativeUserIDs, usersWithMissingDeviceListSelector, getForeignPeerDevices, getAllPeerDevices, }; diff --git a/native/components/auto-join-community-handler.react.js b/native/components/auto-join-community-handler.react.js index 1c2a71b0a..03f97971c 100644 --- a/native/components/auto-join-community-handler.react.js +++ b/native/components/auto-join-community-handler.react.js @@ -1,163 +1,159 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { joinThreadActionTypes, useJoinThread, } from 'lib/actions/thread-actions.js'; import { NeynarClientContext } from 'lib/components/neynar-client-provider.react.js'; import blobService from 'lib/facts/blob-service.js'; import { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js'; -import { isLoggedInToAuthoritativeKeyserver } from 'lib/selectors/user-selectors.js'; +import { isLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/selectors/user-selectors.js'; import { farcasterChannelTagBlobHash } from 'lib/shared/community-utils.js'; import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { defaultThreadSubscription } from 'lib/types/subscription-types.js'; import { getBlobFetchableURL } from 'lib/utils/blob-service.js'; import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken, createDefaultHTTPRequestHeaders, } from 'lib/utils/services-utils.js'; import { nonThreadCalendarQuery } from '../navigation/nav-selectors.js'; import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; function AutoJoinCommunityHandler(): React.Node { const isActive = useSelector(state => state.lifecycleState !== 'background'); - const isLoggedInToAuthKeyserver = useSelector( - isLoggedInToAuthoritativeKeyserver, - ); - const currentUserID = useSelector(state => state.currentUserInfo?.id); - const loggedIn = !!currentUserID && isLoggedInToAuthKeyserver; + const loggedIn = useSelector(isLoggedInToIdentityAndAuthoritativeKeyserver); const fid = useCurrentUserFID(); const neynarClient = React.useContext(NeynarClientContext)?.client; const navContext = React.useContext(NavContext); const identityClientContext = React.useContext(IdentityClientContext); invariant(identityClientContext, 'IdentityClientContext should be set'); const { getAuthMetadata } = identityClientContext; const calendarQuery = useSelector(state => nonThreadCalendarQuery({ redux: state, navContext, }), ); const joinThread = useJoinThread(); const joinThreadActionPromise = React.useCallback( async (communityID: string) => { const query = calendarQuery(); return await joinThread({ threadID: communityID, calendarQuery: { startDate: query.startDate, endDate: query.endDate, filters: [ ...query.filters, { type: 'threads', threadIDs: [communityID] }, ], }, defaultSubscription: defaultThreadSubscription, }); }, [calendarQuery, joinThread], ); const dispatchActionPromise = useDispatchActionPromise(); const threadInfos = useSelector(state => state.threadStore.threadInfos); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); React.useEffect(() => { if (!loggedIn || !isActive || !fid || !neynarClient || !threadInfos) { return; } void (async () => { const authMetadataPromise: Promise = (async () => { if (!usingCommServicesAccessToken) { return undefined; } return await getAuthMetadata(); })(); const followedFarcasterChannelsPromise = neynarClient.fetchFollowedFarcasterChannels(fid); const [authMetadata, followedFarcasterChannels] = await Promise.all([ authMetadataPromise, followedFarcasterChannelsPromise, ]); const headers = authMetadata ? createDefaultHTTPRequestHeaders(authMetadata) : {}; const followedFarcasterChannelIDs = followedFarcasterChannels.map( channel => channel.id, ); const promises = followedFarcasterChannelIDs.map(async channelID => { const blobHash = farcasterChannelTagBlobHash(channelID); const blobURL = getBlobFetchableURL(blobHash); const blobResult = await fetch(blobURL, { method: blobService.httpEndpoints.GET_BLOB.method, headers, }); if (blobResult.status !== 200) { return; } const { commCommunityID } = await blobResult.json(); const keyserverID = extractKeyserverIDFromID(commCommunityID); if (!keyserverInfos[keyserverID]) { return; } // The user is already in the community if (threadInfos[commCommunityID]) { return; } void dispatchActionPromise( joinThreadActionTypes, joinThreadActionPromise(commCommunityID), ); }); await Promise.all(promises); })(); }, [ threadInfos, dispatchActionPromise, fid, isActive, joinThreadActionPromise, loggedIn, neynarClient, getAuthMetadata, keyserverInfos, ]); return null; } export { AutoJoinCommunityHandler }; diff --git a/native/components/connect-farcaster-alert-handler.react.js b/native/components/connect-farcaster-alert-handler.react.js index 3655d426a..7158b798d 100644 --- a/native/components/connect-farcaster-alert-handler.react.js +++ b/native/components/connect-farcaster-alert-handler.react.js @@ -1,69 +1,65 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { recordAlertActionType } from 'lib/actions/alert-actions.js'; -import { isLoggedInToAuthoritativeKeyserver } from 'lib/selectors/user-selectors.js'; +import { isLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/selectors/user-selectors.js'; import { alertTypes, type RecordAlertActionPayload, } from 'lib/types/alert-types.js'; import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; import { shouldSkipConnectFarcasterAlert } from 'lib/utils/push-alerts.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import sleep from 'lib/utils/sleep.js'; import { ConnectFarcasterBottomSheetRouteName } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; function ConnectFarcasterAlertHandler(): React.Node { const { navigate } = useNavigation(); const isActive = useSelector(state => state.lifecycleState !== 'background'); - const isLoggedInToAuthKeyserver = useSelector( - isLoggedInToAuthoritativeKeyserver, - ); - const currentUserID = useSelector(state => state.currentUserInfo?.id); - const loggedIn = !!currentUserID && isLoggedInToAuthKeyserver; + const loggedIn = useSelector(isLoggedInToIdentityAndAuthoritativeKeyserver); const fid = useCurrentUserFID(); const connectFarcasterAlertInfo = useSelector( state => state.alertStore.alertInfos[alertTypes.CONNECT_FARCASTER], ); const dispatch = useDispatch(); React.useEffect(() => { if ( !loggedIn || !isActive || !!fid || shouldSkipConnectFarcasterAlert(connectFarcasterAlertInfo) ) { return; } void (async () => { await sleep(1000); navigate(ConnectFarcasterBottomSheetRouteName); const payload: RecordAlertActionPayload = { alertType: alertTypes.CONNECT_FARCASTER, time: Date.now(), }; dispatch({ type: recordAlertActionType, payload, }); })(); }, [connectFarcasterAlertInfo, dispatch, fid, isActive, loggedIn, navigate]); return null; } export default ConnectFarcasterAlertHandler; diff --git a/native/navigation/navigation-handler.react.js b/native/navigation/navigation-handler.react.js index 9bc92de40..b9f15bca8 100644 --- a/native/navigation/navigation-handler.react.js +++ b/native/navigation/navigation-handler.react.js @@ -1,91 +1,83 @@ // @flow import * as React from 'react'; -import { - isLoggedIn, - isLoggedInToAuthoritativeKeyserver, -} from 'lib/selectors/user-selectors.js'; +import { isLoggedInToIdentityAndAuthoritativeKeyserver } from 'lib/selectors/user-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 { useSelector } from '../redux/redux-utils.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 hasCurrentUserInfo = useSelector(isLoggedIn); + const loggedIn = useSelector(isLoggedInToIdentityAndAuthoritativeKeyserver); - const isLoggedInToAuthKeyserver = useSelector( - isLoggedInToAuthoritativeKeyserver, - ); - - const loggedIn = hasCurrentUserInfo && isLoggedInToAuthKeyserver; 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;