diff --git a/lib/shared/message-utils.js b/lib/shared/message-utils.js --- a/lib/shared/message-utils.js +++ b/lib/shared/message-utils.js @@ -41,7 +41,7 @@ RawReactionMessageInfo, ReactionMessageInfo, } from '../types/messages/reaction.js'; -import { type ThreadInfo } from '../types/thread-types.js'; +import type { ThreadInfo } from '../types/thread-types.js'; import type { UserInfos } from '../types/user-types.js'; import { type EntityText, @@ -665,8 +665,24 @@ ); } -function isInvalidPinSource(message: RawMessageInfo | MessageInfo): boolean { - return !messageSpecs[message.type].canBePinned; +// Prefer checking isInvalidPinSourceForThread below. This function doesn't +// check whether the user is attempting to pin a SIDEBAR_SOURCE in the context +// of its parent thread, so it's not suitable for permission checks. We only +// use it in the message-fetchers.js code where we don't have access to the +// RawThreadInfo and don't need to do permission checks. +function isInvalidPinSource( + messageInfo: RawMessageInfo | MessageInfo, +): boolean { + return !messageSpecs[messageInfo.type].canBePinned; +} + +function isInvalidPinSourceForThread( + messageInfo: RawMessageInfo | MessageInfo, + threadInfo: ThreadInfo, +): boolean { + const isValidPinSource = !isInvalidPinSource(messageInfo); + const isFirstMessageInSidebar = threadInfo.sourceMessageID === messageInfo.id; + return !isValidPinSource || isFirstMessageInSidebar; } function isUnableToBeRenderedIndependently( @@ -706,5 +722,6 @@ useNextLocalID, isInvalidSidebarSource, isInvalidPinSource, + isInvalidPinSourceForThread, isUnableToBeRenderedIndependently, }; diff --git a/lib/utils/toggle-pin-utils.js b/lib/utils/toggle-pin-utils.js new file mode 100644 --- /dev/null +++ b/lib/utils/toggle-pin-utils.js @@ -0,0 +1,25 @@ +// @flow + +import { isInvalidPinSourceForThread } from '../shared/message-utils.js'; +import { threadHasPermission } from '../shared/thread-utils.js'; +import type { + ComposableMessageInfo, + RobotextMessageInfo, +} from '../types/message-types.js'; +import { threadPermissions } from '../types/thread-permission-types.js'; +import type { ThreadInfo } from '../types/thread-types.js'; + +function canToggleMessagePin( + messageInfo: ComposableMessageInfo | RobotextMessageInfo, + threadInfo: ThreadInfo, +): boolean { + const isValidMessage = !isInvalidPinSourceForThread(messageInfo, threadInfo); + const hasManagePinsPermission = threadHasPermission( + threadInfo, + threadPermissions.MANAGE_PINS, + ); + + return isValidMessage && hasManagePinsPermission; +} + +export { canToggleMessagePin }; diff --git a/native/chat/message-results-screen.react.js b/native/chat/message-results-screen.react.js --- a/native/chat/message-results-screen.react.js +++ b/native/chat/message-results-screen.react.js @@ -9,7 +9,7 @@ import { messageListData } from 'lib/selectors/chat-selectors.js'; import { createMessageInfo, - isInvalidPinSource, + isInvalidPinSourceForThread, } from 'lib/shared/message-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { useServerCall } from 'lib/utils/action-utils.js'; @@ -77,7 +77,7 @@ item => item.itemType === 'message' && item.isPinned && - !isInvalidPinSource(item.messageInfo), + !isInvalidPinSourceForThread(item.messageInfo, threadInfo), ); // By the nature of using messageListData and passing in @@ -102,7 +102,7 @@ } return sortedChatMessageInfoItems.filter(Boolean); - }, [chatMessageInfos, rawMessageResults]); + }, [chatMessageInfos, rawMessageResults, threadInfo]); const measureCallback = React.useCallback( (listDataWithHeights: $ReadOnlyArray) => { diff --git a/native/chat/message.react.js b/native/chat/message.react.js --- a/native/chat/message.react.js +++ b/native/chat/message.react.js @@ -8,6 +8,7 @@ } from 'react-native'; import { messageKey } from 'lib/shared/message-utils.js'; +import { canToggleMessagePin } from 'lib/utils/toggle-pin-utils.js'; import type { ChatNavigationProp } from './chat.react.js'; import MultimediaMessage from './multimedia-message.react.js'; @@ -91,6 +92,11 @@ [focused, item], ); + const canTogglePins = React.useMemo( + () => canToggleMessagePin(props.item.messageInfo, props.item.threadInfo), + [props.item.messageInfo, props.item.threadInfo], + ); + const innerMessageNode = React.useMemo(() => { if (item.messageShapeType === 'text') { return ( @@ -101,6 +107,7 @@ focused={focused} toggleFocus={toggleFocus} verticalBounds={verticalBounds} + canTogglePins={canTogglePins} shouldDisplayPinIndicator={shouldDisplayPinIndicator} /> ); @@ -111,6 +118,7 @@ focused={focused} toggleFocus={toggleFocus} verticalBounds={verticalBounds} + canTogglePins={canTogglePins} shouldDisplayPinIndicator={shouldDisplayPinIndicator} /> ); @@ -134,6 +142,7 @@ shouldDisplayPinIndicator, toggleFocus, verticalBounds, + canTogglePins, ]); const message = React.useMemo( 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 @@ -9,13 +9,9 @@ import * as React from 'react'; import { View } from 'react-native'; -import { isInvalidPinSource, messageKey } from 'lib/shared/message-utils.js'; -import { - threadHasPermission, - useCanCreateSidebarFromMessage, -} from 'lib/shared/thread-utils.js'; +import { messageKey } from 'lib/shared/message-utils.js'; +import { useCanCreateSidebarFromMessage } from 'lib/shared/thread-utils.js'; import type { MediaInfo } from 'lib/types/media-types.js'; -import { threadPermissions } from 'lib/types/thread-permission-types.js'; import ComposedMessage from './composed-message.react.js'; import { InnerMultimediaMessage } from './inner-multimedia-message.react.js'; @@ -45,6 +41,7 @@ +focused: boolean, +toggleFocus: (messageKey: string) => void, +verticalBounds: ?VerticalBounds, + +canTogglePins: boolean, +shouldDisplayPinIndicator: boolean, }; type Props = { @@ -54,7 +51,6 @@ +overlayContext: ?OverlayContextType, +chatContext: ?ChatContextType, +canCreateSidebarFromMessage: boolean, - +canTogglePins: boolean, }; type State = { +clickable: boolean, @@ -242,13 +238,6 @@ props.item.threadInfo, props.item.messageInfo, ); - const canTogglePins = - !isInvalidPinSource(props.item.messageInfo) && - threadHasPermission( - props.item.threadInfo, - threadPermissions.MANAGE_PINS, - ) && - props.item.threadInfo.sourceMessageID !== props.item.messageInfo.id; return ( ); }); 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 @@ -4,7 +4,7 @@ import * as React from 'react'; import { View } from 'react-native'; -import { isInvalidPinSource, messageKey } from 'lib/shared/message-utils.js'; +import { messageKey } from 'lib/shared/message-utils.js'; import { threadHasPermission, useCanCreateSidebarFromMessage, @@ -51,6 +51,7 @@ +focused: boolean, +toggleFocus: (messageKey: string) => void, +verticalBounds: ?VerticalBounds, + +canTogglePins: boolean, +shouldDisplayPinIndicator: boolean, }; type Props = { @@ -65,7 +66,6 @@ +isLinkModalActive: boolean, +isUserProfileBottomSheetActive: boolean, +canEditMessage: boolean, - +canTogglePins: boolean, }; class TextMessage extends React.PureComponent { message: ?React.ElementRef; @@ -291,14 +291,6 @@ useCanEditMessageNative(props.item.threadInfo, props.item.messageInfo) && !isThisMessageEdited; - const canTogglePins = - !isInvalidPinSource(props.item.messageInfo) && - threadHasPermission( - props.item.threadInfo, - threadPermissions.MANAGE_PINS, - ) && - props.item.threadInfo.sourceMessageID !== props.item.messageInfo.id; - React.useEffect(() => clearMarkdownContextData, [clearMarkdownContextData]); return ( @@ -310,7 +302,6 @@ isLinkModalActive={isLinkModalActive} isUserProfileBottomSheetActive={isUserProfileBottomSheetActive} canEditMessage={canEditMessage} - canTogglePins={canTogglePins} /> ); }); diff --git a/web/modals/chat/message-results-modal.react.js b/web/modals/chat/message-results-modal.react.js --- a/web/modals/chat/message-results-modal.react.js +++ b/web/modals/chat/message-results-modal.react.js @@ -11,7 +11,7 @@ import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { createMessageInfo, - isInvalidPinSource, + isInvalidPinSourceForThread, modifyItemForResultScreen, } from 'lib/shared/message-utils.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; @@ -80,7 +80,7 @@ item => item.itemType === 'message' && item.isPinned && - !isInvalidPinSource(item.messageInfo), + !isInvalidPinSourceForThread(item.messageInfo, threadInfo), ); // By the nature of using messageListData and passing in @@ -105,7 +105,7 @@ } return sortedChatMessageInfoItems; - }, [chatMessageInfos, rawMessageResults]); + }, [chatMessageInfos, rawMessageResults, threadInfo]); const modifiedItems = React.useMemo( () => diff --git a/web/utils/tooltip-action-utils.js b/web/utils/tooltip-action-utils.js --- a/web/utils/tooltip-action-utils.js +++ b/web/utils/tooltip-action-utils.js @@ -10,10 +10,7 @@ ChatMessageInfoItem, } from 'lib/selectors/chat-selectors.js'; import { useCanEditMessage } from 'lib/shared/edit-messages-utils.js'; -import { - createMessageReply, - isInvalidPinSource, -} from 'lib/shared/message-utils.js'; +import { createMessageReply } from 'lib/shared/message-utils.js'; import { useCanCreateReactionFromMessage } from 'lib/shared/reaction-utils.js'; import { threadHasPermission, @@ -23,6 +20,7 @@ import { threadPermissions } from 'lib/types/thread-permission-types.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { longAbsoluteDate } from 'lib/utils/date-utils.js'; +import { canToggleMessagePin } from 'lib/utils/toggle-pin-utils.js'; import { type MessageTooltipAction, @@ -274,10 +272,7 @@ const { pushModal } = useModalContext(); const { messageInfo, isPinned } = item; - const canTogglePin = - !isInvalidPinSource(messageInfo) && - threadHasPermission(threadInfo, threadPermissions.MANAGE_PINS) && - threadInfo.sourceMessageID !== item.messageInfo.id; + const canTogglePin = canToggleMessagePin(messageInfo, threadInfo); const inputState = React.useContext(InputStateContext);