diff --git a/lib/shared/sidebar-utils.js b/lib/shared/sidebar-utils.js new file mode 100644 --- /dev/null +++ b/lib/shared/sidebar-utils.js @@ -0,0 +1,245 @@ +// @flow + +import invariant from 'invariant'; + +import type { ParserRules } from './markdown.js'; +import { getMessageTitle, isInvalidSidebarSource } from './message-utils.js'; +import { relationshipBlockedInEitherDirection } from './relationship-utils.js'; +import { + createPendingThread, + getSingleOtherUser, + extractMentionedMembers, + useThreadHasPermission, + userIsMember, +} from './thread-utils.js'; +import type { ChatMessageInfoItem } from '../selectors/chat-selectors.js'; +import { messageTypes } from '../types/message-types-enum.js'; +import type { + RobotextMessageInfo, + ComposableMessageInfo, +} from '../types/message-types.js'; +import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; +import { threadPermissions } from '../types/thread-permission-types.js'; +import { threadTypes } from '../types/thread-types-enum.js'; +import type { LoggedInUserInfo } from '../types/user-types.js'; +import type { GetENSNames } from '../utils/ens-helpers.js'; +import { + entityTextToRawString, + getEntityTextAsString, +} from '../utils/entity-text.js'; +import type { GetFCNames } from '../utils/farcaster-helpers.js'; +import { useSelector } from '../utils/redux-utils.js'; +import { trimText } from '../utils/text-utils.js'; + +type SharedCreatePendingSidebarInput = { + +sourceMessageInfo: ComposableMessageInfo | RobotextMessageInfo, + +parentThreadInfo: ThreadInfo, + +loggedInUserInfo: LoggedInUserInfo, +}; + +type BaseCreatePendingSidebarInput = { + ...SharedCreatePendingSidebarInput, + +messageTitle: string, +}; + +type UserIDAndUsername = { + +id: string, + +username: string, + ... +}; + +function baseCreatePendingSidebar( + input: BaseCreatePendingSidebarInput, +): ThreadInfo { + const { + sourceMessageInfo, + parentThreadInfo, + loggedInUserInfo, + messageTitle, + } = input; + const { color, type: parentThreadType } = parentThreadInfo; + const threadName = trimText(messageTitle, 30); + + const initialMembers = new Map(); + + const { id: viewerID, username: viewerUsername } = loggedInUserInfo; + initialMembers.set(viewerID, { id: viewerID, username: viewerUsername }); + + if (userIsMember(parentThreadInfo, sourceMessageInfo.creator.id)) { + const { id: sourceAuthorID, username: sourceAuthorUsername } = + sourceMessageInfo.creator; + invariant( + sourceAuthorUsername, + 'sourceAuthorUsername should be set in createPendingSidebar', + ); + const initialMemberUserInfo = { + id: sourceAuthorID, + username: sourceAuthorUsername, + }; + initialMembers.set(sourceAuthorID, initialMemberUserInfo); + } + + const singleOtherUser = getSingleOtherUser(parentThreadInfo, viewerID); + if (parentThreadType === threadTypes.PERSONAL && singleOtherUser) { + const singleOtherUsername = parentThreadInfo.members.find( + member => member.id === singleOtherUser, + )?.username; + invariant( + singleOtherUsername, + 'singleOtherUsername should be set in createPendingSidebar', + ); + const singleOtherUserInfo = { + id: singleOtherUser, + username: singleOtherUsername, + }; + initialMembers.set(singleOtherUser, singleOtherUserInfo); + } + + if (sourceMessageInfo.type === messageTypes.TEXT) { + const mentionedMembersOfParent = extractMentionedMembers( + sourceMessageInfo.text, + parentThreadInfo, + ); + for (const [memberID, member] of mentionedMembersOfParent) { + initialMembers.set(memberID, member); + } + } + + return createPendingThread({ + viewerID, + threadType: threadTypes.SIDEBAR, + members: [...initialMembers.values()], + parentThreadInfo, + threadColor: color, + name: threadName, + sourceMessageID: sourceMessageInfo.id, + }); +} + +// The message title here may have ETH addresses that aren't resolved to ENS +// names. This function should only be used in cases where we're sure that we +// don't care about the thread title. We should prefer createPendingSidebar +// wherever possible +type CreateUnresolvedPendingSidebarInput = { + ...SharedCreatePendingSidebarInput, + +markdownRules: ParserRules, +}; + +function createUnresolvedPendingSidebar( + input: CreateUnresolvedPendingSidebarInput, +): ThreadInfo { + const { + sourceMessageInfo, + parentThreadInfo, + loggedInUserInfo, + markdownRules, + } = input; + + const messageTitleEntityText = getMessageTitle( + sourceMessageInfo, + parentThreadInfo, + parentThreadInfo, + markdownRules, + ); + const messageTitle = entityTextToRawString(messageTitleEntityText, { + ignoreViewer: true, + }); + + return baseCreatePendingSidebar({ + sourceMessageInfo, + parentThreadInfo, + messageTitle, + loggedInUserInfo, + }); +} + +type CreatePendingSidebarInput = { + ...SharedCreatePendingSidebarInput, + +markdownRules: ParserRules, + +getENSNames: ?GetENSNames, + +getFCNames: ?GetFCNames, +}; + +async function createPendingSidebar( + input: CreatePendingSidebarInput, +): Promise { + const { + sourceMessageInfo, + parentThreadInfo, + loggedInUserInfo, + markdownRules, + getENSNames, + getFCNames, + } = input; + + const messageTitleEntityText = getMessageTitle( + sourceMessageInfo, + parentThreadInfo, + parentThreadInfo, + markdownRules, + ); + const messageTitle = await getEntityTextAsString( + messageTitleEntityText, + { getENSNames, getFCNames }, + { ignoreViewer: true }, + ); + invariant( + messageTitle !== null && messageTitle !== undefined, + 'getEntityTextAsString only returns falsey when passed falsey', + ); + + return baseCreatePendingSidebar({ + sourceMessageInfo, + parentThreadInfo, + messageTitle, + loggedInUserInfo, + }); +} + +function useCanCreateSidebarFromMessage( + threadInfo: ThreadInfo, + messageInfo: ComposableMessageInfo | RobotextMessageInfo, +): boolean { + const messageCreatorUserInfo = useSelector( + state => state.userStore.userInfos[messageInfo.creator.id], + ); + const hasCreateSidebarsPermission = useThreadHasPermission( + threadInfo, + threadPermissions.CREATE_SIDEBARS, + ); + if (!hasCreateSidebarsPermission) { + return false; + } + + if ( + !messageInfo.id || + threadInfo.sourceMessageID === messageInfo.id || + isInvalidSidebarSource(messageInfo) + ) { + return false; + } + + const messageCreatorRelationship = messageCreatorUserInfo?.relationshipStatus; + return ( + !messageCreatorRelationship || + !relationshipBlockedInEitherDirection(messageCreatorRelationship) + ); +} + +function useSidebarExistsOrCanBeCreated( + threadInfo: ThreadInfo, + messageItem: ChatMessageInfoItem, +): boolean { + const canCreateSidebarFromMessage = useCanCreateSidebarFromMessage( + threadInfo, + messageItem.messageInfo, + ); + return !!messageItem.threadCreatedFromMessage || canCreateSidebarFromMessage; +} + +export { + createUnresolvedPendingSidebar, + createPendingSidebar, + useCanCreateSidebarFromMessage, + useSidebarExistsOrCanBeCreated, +}; diff --git a/lib/shared/thread-utils.js b/lib/shared/thread-utils.js --- a/lib/shared/thread-utils.js +++ b/lib/shared/thread-utils.js @@ -9,9 +9,7 @@ import { getUserAvatarForThread } from './avatar-utils.js'; import { generatePendingThreadColor } from './color-utils.js'; -import { type ParserRules } from './markdown.js'; import { extractUserMentionsFromText } from './mention-utils.js'; -import { getMessageTitle, isInvalidSidebarSource } from './message-utils.js'; import { relationshipBlockedInEitherDirection } from './relationship-utils.js'; import ashoat from '../facts/ashoat.js'; import genesis from '../facts/genesis.js'; @@ -29,10 +27,7 @@ getAllThreadPermissions, makePermissionsBlob, } from '../permissions/thread-permissions.js'; -import type { - ChatThreadItem, - ChatMessageInfoItem, -} from '../selectors/chat-selectors.js'; +import type { ChatThreadItem } from '../selectors/chat-selectors.js'; import { threadInfoSelector, pendingToRealizedThreadIDsSelector, @@ -43,11 +38,6 @@ getRelativeMemberInfos, usersWithPersonalThreadSelector, } from '../selectors/user-selectors.js'; -import { messageTypes } from '../types/message-types-enum.js'; -import { - type RobotextMessageInfo, - type ComposableMessageInfo, -} from '../types/message-types.js'; import type { RelativeMemberInfo, RawThreadInfo, @@ -98,19 +88,14 @@ LoggedInUserInfo, UserInfo, } from '../types/user-types.js'; -import type { GetENSNames } from '../utils/ens-helpers.js'; import { ET, - entityTextToRawString, - getEntityTextAsString, type ThreadEntity, type UserEntity, } from '../utils/entity-text.js'; -import type { GetFCNames } from '../utils/farcaster-helpers.js'; import { entries, values } from '../utils/objects.js'; import { useSelector } from '../utils/redux-utils.js'; import { firstLine } from '../utils/string-utils.js'; -import { trimText } from '../utils/text-utils.js'; import { pendingThreadIDRegex } from '../utils/validation-utils.js'; function threadHasPermission( @@ -553,165 +538,6 @@ return [...mentionedMembersOfParent.values()]; } -type SharedCreatePendingSidebarInput = { - +sourceMessageInfo: ComposableMessageInfo | RobotextMessageInfo, - +parentThreadInfo: ThreadInfo, - +loggedInUserInfo: LoggedInUserInfo, -}; - -type BaseCreatePendingSidebarInput = { - ...SharedCreatePendingSidebarInput, - +messageTitle: string, -}; - -function baseCreatePendingSidebar( - input: BaseCreatePendingSidebarInput, -): ThreadInfo { - const { - sourceMessageInfo, - parentThreadInfo, - loggedInUserInfo, - messageTitle, - } = input; - const { color, type: parentThreadType } = parentThreadInfo; - const threadName = trimText(messageTitle, 30); - - const initialMembers = new Map(); - - const { id: viewerID, username: viewerUsername } = loggedInUserInfo; - initialMembers.set(viewerID, { id: viewerID, username: viewerUsername }); - - if (userIsMember(parentThreadInfo, sourceMessageInfo.creator.id)) { - const { id: sourceAuthorID, username: sourceAuthorUsername } = - sourceMessageInfo.creator; - invariant( - sourceAuthorUsername, - 'sourceAuthorUsername should be set in createPendingSidebar', - ); - const initialMemberUserInfo = { - id: sourceAuthorID, - username: sourceAuthorUsername, - }; - initialMembers.set(sourceAuthorID, initialMemberUserInfo); - } - - const singleOtherUser = getSingleOtherUser(parentThreadInfo, viewerID); - if (parentThreadType === threadTypes.PERSONAL && singleOtherUser) { - const singleOtherUsername = parentThreadInfo.members.find( - member => member.id === singleOtherUser, - )?.username; - invariant( - singleOtherUsername, - 'singleOtherUsername should be set in createPendingSidebar', - ); - const singleOtherUserInfo = { - id: singleOtherUser, - username: singleOtherUsername, - }; - initialMembers.set(singleOtherUser, singleOtherUserInfo); - } - - if (sourceMessageInfo.type === messageTypes.TEXT) { - const mentionedMembersOfParent = extractMentionedMembers( - sourceMessageInfo.text, - parentThreadInfo, - ); - for (const [memberID, member] of mentionedMembersOfParent) { - initialMembers.set(memberID, member); - } - } - - return createPendingThread({ - viewerID, - threadType: threadTypes.SIDEBAR, - members: [...initialMembers.values()], - parentThreadInfo, - threadColor: color, - name: threadName, - sourceMessageID: sourceMessageInfo.id, - }); -} - -// The message title here may have ETH addresses that aren't resolved to ENS -// names. This function should only be used in cases where we're sure that we -// don't care about the thread title. We should prefer createPendingSidebar -// wherever possible -type CreateUnresolvedPendingSidebarInput = { - ...SharedCreatePendingSidebarInput, - +markdownRules: ParserRules, -}; - -function createUnresolvedPendingSidebar( - input: CreateUnresolvedPendingSidebarInput, -): ThreadInfo { - const { - sourceMessageInfo, - parentThreadInfo, - loggedInUserInfo, - markdownRules, - } = input; - - const messageTitleEntityText = getMessageTitle( - sourceMessageInfo, - parentThreadInfo, - parentThreadInfo, - markdownRules, - ); - const messageTitle = entityTextToRawString(messageTitleEntityText, { - ignoreViewer: true, - }); - - return baseCreatePendingSidebar({ - sourceMessageInfo, - parentThreadInfo, - messageTitle, - loggedInUserInfo, - }); -} - -type CreatePendingSidebarInput = { - ...SharedCreatePendingSidebarInput, - +markdownRules: ParserRules, - +getENSNames: ?GetENSNames, - +getFCNames: ?GetFCNames, -}; - -async function createPendingSidebar( - input: CreatePendingSidebarInput, -): Promise { - const { - sourceMessageInfo, - parentThreadInfo, - loggedInUserInfo, - markdownRules, - getENSNames, - getFCNames, - } = input; - - const messageTitleEntityText = getMessageTitle( - sourceMessageInfo, - parentThreadInfo, - parentThreadInfo, - markdownRules, - ); - const messageTitle = await getEntityTextAsString( - messageTitleEntityText, - { getENSNames, getFCNames }, - { ignoreViewer: true }, - ); - invariant( - messageTitle !== null && messageTitle !== undefined, - 'getEntityTextAsString only returns falsey when passed falsey', - ); - - return baseCreatePendingSidebar({ - sourceMessageInfo, - parentThreadInfo, - messageTitle, - loggedInUserInfo, - }); -} - function pendingThreadType(numberOfOtherMembers: number): 4 | 6 | 7 { if (numberOfOtherMembers === 0) { return threadTypes.PRIVATE; @@ -1332,47 +1158,6 @@ return false; } -function useCanCreateSidebarFromMessage( - threadInfo: ThreadInfo, - messageInfo: ComposableMessageInfo | RobotextMessageInfo, -): boolean { - const messageCreatorUserInfo = useSelector( - state => state.userStore.userInfos[messageInfo.creator.id], - ); - const hasCreateSidebarsPermission = useThreadHasPermission( - threadInfo, - threadPermissions.CREATE_SIDEBARS, - ); - if (!hasCreateSidebarsPermission) { - return false; - } - - if ( - !messageInfo.id || - threadInfo.sourceMessageID === messageInfo.id || - isInvalidSidebarSource(messageInfo) - ) { - return false; - } - - const messageCreatorRelationship = messageCreatorUserInfo?.relationshipStatus; - return ( - !messageCreatorRelationship || - !relationshipBlockedInEitherDirection(messageCreatorRelationship) - ); -} - -function useSidebarExistsOrCanBeCreated( - threadInfo: ThreadInfo, - messageItem: ChatMessageInfoItem, -): boolean { - const canCreateSidebarFromMessage = useCanCreateSidebarFromMessage( - threadInfo, - messageItem.messageInfo, - ); - return !!messageItem.threadCreatedFromMessage || canCreateSidebarFromMessage; -} - function checkIfDefaultMembersAreVoiced(threadInfo: ThreadInfo): boolean { const defaultRoleID = Object.keys(threadInfo.roles).find(roleID => roleIsDefaultRole(threadInfo.roles[roleID]), @@ -1776,9 +1561,7 @@ getPendingThreadID, parsePendingThreadID, createPendingThread, - createUnresolvedPendingSidebar, extractNewMentionedParentMembers, - createPendingSidebar, pendingThreadType, filterOutDisabledPermissions, getCurrentUser, @@ -1800,8 +1583,6 @@ useExistingThreadInfoFinder, getThreadTypeParentRequirement, threadMemberHasPermission, - useCanCreateSidebarFromMessage, - useSidebarExistsOrCanBeCreated, checkIfDefaultMembersAreVoiced, draftKeySuffix, draftKeyFromThreadID, @@ -1820,4 +1601,5 @@ useUserProfileThreadInfo, assertAllThreadInfosAreLegacy, useOnScreenEntryEditableThreadInfos, + extractMentionedMembers, }; diff --git a/native/chat/multimedia-message.react.js b/native/chat/multimedia-message.react.js --- a/native/chat/multimedia-message.react.js +++ b/native/chat/multimedia-message.react.js @@ -10,7 +10,7 @@ import { View } from 'react-native'; import { messageKey } from 'lib/shared/message-utils.js'; -import { useCanCreateSidebarFromMessage } from 'lib/shared/thread-utils.js'; +import { useCanCreateSidebarFromMessage } from 'lib/shared/sidebar-utils.js'; import type { MediaInfo } from 'lib/types/media-types.js'; import ComposedMessage from './composed-message.react.js'; diff --git a/native/chat/robotext-message.react.js b/native/chat/robotext-message.react.js --- a/native/chat/robotext-message.react.js +++ b/native/chat/robotext-message.react.js @@ -5,7 +5,7 @@ import { View } from 'react-native'; import { messageKey } from 'lib/shared/message-utils.js'; -import { useCanCreateSidebarFromMessage } from 'lib/shared/thread-utils.js'; +import { useCanCreateSidebarFromMessage } from 'lib/shared/sidebar-utils.js'; import { inlineEngagementCenterStyle } from './chat-constants.js'; import type { ChatNavigationProp } from './chat.react.js'; diff --git a/native/chat/sidebar-navigation.js b/native/chat/sidebar-navigation.js --- a/native/chat/sidebar-navigation.js +++ b/native/chat/sidebar-navigation.js @@ -10,7 +10,7 @@ import { createPendingSidebar, createUnresolvedPendingSidebar, -} from 'lib/shared/thread-utils.js'; +} from 'lib/shared/sidebar-utils.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { ChatMentionCandidates } from 'lib/types/thread-types.js'; import type { LoggedInUserInfo } from 'lib/types/user-types.js'; diff --git a/native/chat/text-message.react.js b/native/chat/text-message.react.js --- a/native/chat/text-message.react.js +++ b/native/chat/text-message.react.js @@ -5,10 +5,8 @@ import { View } from 'react-native'; import { messageKey } from 'lib/shared/message-utils.js'; -import { - useCanCreateSidebarFromMessage, - useThreadHasPermission, -} from 'lib/shared/thread-utils.js'; +import { useCanCreateSidebarFromMessage } from 'lib/shared/sidebar-utils.js'; +import { useThreadHasPermission } from 'lib/shared/thread-utils.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import type { ChatNavigationProp } from './chat.react.js'; diff --git a/web/selectors/thread-selectors.js b/web/selectors/thread-selectors.js --- a/web/selectors/thread-selectors.js +++ b/web/selectors/thread-selectors.js @@ -8,10 +8,8 @@ import { NeynarClientContext } from 'lib/components/neynar-client-provider.react.js'; import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js'; import { useThreadChatMentionCandidates } from 'lib/hooks/chat-mention-hooks.js'; -import { - createPendingSidebar, - threadInHomeChatList, -} from 'lib/shared/thread-utils.js'; +import { createPendingSidebar } from 'lib/shared/sidebar-utils.js'; +import { threadInHomeChatList } from 'lib/shared/thread-utils.js'; import type { ComposableMessageInfo, RobotextMessageInfo, diff --git a/web/tooltips/tooltip-action-utils.js b/web/tooltips/tooltip-action-utils.js --- a/web/tooltips/tooltip-action-utils.js +++ b/web/tooltips/tooltip-action-utils.js @@ -13,10 +13,8 @@ import { useCanEditMessage } from 'lib/shared/edit-messages-utils.js'; import { createMessageReply } from 'lib/shared/message-utils.js'; import { useCanCreateReactionFromMessage } from 'lib/shared/reaction-utils.js'; -import { - useSidebarExistsOrCanBeCreated, - useThreadHasPermission, -} from 'lib/shared/thread-utils.js'; +import { useSidebarExistsOrCanBeCreated } from 'lib/shared/sidebar-utils.js'; +import { useThreadHasPermission } from 'lib/shared/thread-utils.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js';