diff --git a/ts/RemoteConfig.dom.ts b/ts/RemoteConfig.dom.ts index f0eaaec6db..2f8f5c395d 100644 --- a/ts/RemoteConfig.dom.ts +++ b/ts/RemoteConfig.dom.ts @@ -25,6 +25,10 @@ const log = createLogger('RemoteConfig'); const SemverKeys = [ 'desktop.callQualitySurvey.beta', 'desktop.callQualitySurvey.prod', + 'desktop.pinnedMessages.receive.beta', + 'desktop.pinnedMessages.receive.prod', + 'desktop.pinnedMessages.send.beta', + 'desktop.pinnedMessages.send.prod', 'desktop.plaintextExport.beta', 'desktop.plaintextExport.prod', ] as const; diff --git a/ts/background.preload.ts b/ts/background.preload.ts index 45bd060754..9a81fc280e 100644 --- a/ts/background.preload.ts +++ b/ts/background.preload.ts @@ -283,7 +283,7 @@ import { } from './types/Message2.preload.js'; import { JobCancelReason } from './jobs/types.std.js'; import { itemStorage } from './textsecure/Storage.preload.js'; -import { isPinnedMessagesReceiveEnabled } from './util/isPinnedMessagesEnabled.std.js'; +import { isPinnedMessagesReceiveEnabled } from './util/isPinnedMessagesEnabled.dom.js'; const { isNumber, throttle } = lodash; diff --git a/ts/components/conversation/MessageContextMenu.dom.tsx b/ts/components/conversation/MessageContextMenu.dom.tsx index d7dad57744..041410a6a5 100644 --- a/ts/components/conversation/MessageContextMenu.dom.tsx +++ b/ts/components/conversation/MessageContextMenu.dom.tsx @@ -4,7 +4,6 @@ import React, { type ReactNode } from 'react'; import type { LocalizerType } from '../../types/I18N.std.js'; import { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.js'; -import { isPinnedMessagesReceiveEnabled } from '../../util/isPinnedMessagesEnabled.std.js'; type MessageContextMenuProps = Readonly<{ i18n: LocalizerType; @@ -101,12 +100,12 @@ export function MessageContextMenu({ {i18n('icu:copy')} )} - {isPinnedMessagesReceiveEnabled() && onPinMessage && ( + {onPinMessage && ( {i18n('icu:MessageContextMenu__PinMessage')} )} - {isPinnedMessagesReceiveEnabled() && onUnpinMessage && ( + {onUnpinMessage && ( {i18n('icu:MessageContextMenu__UnpinMessage')} diff --git a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.stories.tsx b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.stories.tsx index 9d40fd172b..401486fd74 100644 --- a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.stories.tsx +++ b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.stories.tsx @@ -84,6 +84,7 @@ function Template(props: { onPinGoTo={action('onPinGoTo')} onPinRemove={action('onPinRemove')} onPinsShowAll={action('onPinsShowAll')} + canPinMessages /> ); } diff --git a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx index df250fbed1..2434690798 100644 --- a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx +++ b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx @@ -70,6 +70,7 @@ export type PinnedMessagesBarProps = Readonly<{ onPinGoTo: (messageId: string) => void; onPinRemove: (messageId: string) => void; onPinsShowAll: () => void; + canPinMessages: boolean; }>; export const PinnedMessagesBar = memo(function PinnedMessagesBar( @@ -97,6 +98,7 @@ export const PinnedMessagesBar = memo(function PinnedMessagesBar( onPinGoTo={props.onPinGoTo} onPinRemove={props.onPinRemove} onPinsShowAll={props.onPinsShowAll} + canPinMessages={props.canPinMessages} /> ); @@ -131,6 +133,7 @@ export const PinnedMessagesBar = memo(function PinnedMessagesBar( onPinGoTo={props.onPinGoTo} onPinRemove={props.onPinRemove} onPinsShowAll={props.onPinsShowAll} + canPinMessages={props.canPinMessages} /> ); @@ -236,6 +239,7 @@ const Content = forwardRef(function Content( onPinGoTo: (messageId: string) => void; onPinRemove: (messageId: string) => void; onPinsShowAll: () => void; + canPinMessages: boolean; }, ref: ForwardedRef ): JSX.Element { @@ -298,9 +302,14 @@ const Content = forwardRef(function Content( /> - - {i18n('icu:PinnedMessagesBar__ActionsMenu__UnpinMessage')} - + {props.canPinMessages && ( + + {i18n('icu:PinnedMessagesBar__ActionsMenu__UnpinMessage')} + + )} ; + canPinMessages: boolean; renderTimelineItem: (props: SmartTimelineItemProps) => JSX.Element; }>; @@ -78,11 +79,13 @@ export const PinnedMessagesPanel = memo(function PinnedMessagesPanel( ); })} -
- - {i18n('icu:PinnedMessagesPanel__UnpinAllMessages')} - -
+ {props.canPinMessages && ( +
+ + {i18n('icu:PinnedMessagesPanel__UnpinAllMessages')} + +
+ )} ); }); diff --git a/ts/state/selectors/message.preload.ts b/ts/state/selectors/message.preload.ts index cb6ae6672e..4219ee7c9f 100644 --- a/ts/state/selectors/message.preload.ts +++ b/ts/state/selectors/message.preload.ts @@ -169,6 +169,7 @@ import type { MessageRequestResponseNotificationData } from '../../components/co import type { PinnedMessageNotificationData } from '../../components/conversation/pinned-messages/PinnedMessageNotification.dom.js'; import { canEditGroupInfo } from '../../util/canEditGroupInfo.preload.js'; import { getPinnedMessagesMessageIds } from './pinnedMessages.dom.js'; +import { isPinnedMessagesSendEnabled } from '../../util/isPinnedMessagesEnabled.dom.js'; const { groupBy, isEmpty, isNumber, isObject, map } = lodash; @@ -2403,6 +2404,9 @@ export function canForward(message: ReadonlyMessageAttributesType): boolean { } export function canPinMessages(conversation: ConversationType): boolean { + if (!isPinnedMessagesSendEnabled()) { + return false; + } return conversation.type === 'direct' || canEditGroupInfo(conversation); } diff --git a/ts/state/smart/PinnedMessagesBar.preload.tsx b/ts/state/smart/PinnedMessagesBar.preload.tsx index bbc62c8b8f..e0ce9baf30 100644 --- a/ts/state/smart/PinnedMessagesBar.preload.tsx +++ b/ts/state/smart/PinnedMessagesBar.preload.tsx @@ -4,7 +4,10 @@ import React, { memo, useCallback, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import { getIntl } from '../selectors/user.std.js'; -import { getSelectedConversationId } from '../selectors/conversations.dom.js'; +import { + getConversationSelector, + getSelectedConversationId, +} from '../selectors/conversations.dom.js'; import { strictAssert } from '../../util/assert.std.js'; import { useConversationsActions } from '../ducks/conversations.preload.js'; import type { @@ -20,6 +23,7 @@ import { PinnedMessagesBar } from '../../components/conversation/pinned-messages import { PanelType } from '../../types/Panels.std.js'; import type { PinnedMessageId } from '../../types/PinnedMessage.std.js'; import { + canPinMessages as getCanPinMessages, getMessagePropsSelector, type MessagePropsType, } from '../selectors/message.preload.js'; @@ -129,13 +133,18 @@ const selectPins: StateSelector> = createSelector( export const SmartPinnedMessagesBar = memo(function SmartPinnedMessagesBar() { const i18n = useSelector(getIntl); const conversationId = useSelector(getSelectedConversationId); - const pins = useSelector(selectPins); - strictAssert( conversationId != null, 'PinnedMessagesBar should only be rendered in selected conversation' ); + const conversationSelector = useSelector(getConversationSelector); + const conversation = conversationSelector(conversationId); + strictAssert(conversation != null, 'Missing conversation'); + + const pins = useSelector(selectPins); + const canPinMessages = getCanPinMessages(conversation); + const { pushPanelForConversation, scrollToMessage } = useConversationsActions(); const { onPinnedMessageRemove } = usePinnedMessagesActions(); @@ -203,6 +212,7 @@ export const SmartPinnedMessagesBar = memo(function SmartPinnedMessagesBar() { onPinGoTo={handlePinGoTo} onPinRemove={handlePinRemove} onPinsShowAll={handlePinsShowAll} + canPinMessages={canPinMessages} /> ); }); diff --git a/ts/state/smart/PinnedMessagesPanel.preload.tsx b/ts/state/smart/PinnedMessagesPanel.preload.tsx index e0b1205e09..5935cc2400 100644 --- a/ts/state/smart/PinnedMessagesPanel.preload.tsx +++ b/ts/state/smart/PinnedMessagesPanel.preload.tsx @@ -10,6 +10,7 @@ import { PinnedMessagesPanel } from '../../components/conversation/pinned-messag import type { SmartTimelineItemProps } from './TimelineItem.preload.js'; import { SmartTimelineItem } from './TimelineItem.preload.js'; import { getPinnedMessages } from '../selectors/pinnedMessages.dom.js'; +import { canPinMessages as getCanPinMessages } from '../selectors/message.preload.js'; export type SmartPinnedMessagesPanelProps = Readonly<{ conversationId: string; @@ -32,6 +33,7 @@ export const SmartPinnedMessagesPanel = memo(function SmartPinnedMessagesPanel( ); const pinnedMessages = useSelector(getPinnedMessages); + const canPinMessages = getCanPinMessages(conversation); return ( ); }); diff --git a/ts/util/isPinnedMessagesEnabled.dom.ts b/ts/util/isPinnedMessagesEnabled.dom.ts new file mode 100644 index 0000000000..8cb18405f9 --- /dev/null +++ b/ts/util/isPinnedMessagesEnabled.dom.ts @@ -0,0 +1,18 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { isFeaturedEnabledNoRedux } from './isFeatureEnabled.dom.js'; + +export function isPinnedMessagesReceiveEnabled(): boolean { + return isFeaturedEnabledNoRedux({ + betaKey: 'desktop.pinnedMessages.receive.beta', + prodKey: 'desktop.pinnedMessages.receive.prod', + }); +} + +export function isPinnedMessagesSendEnabled(): boolean { + return isFeaturedEnabledNoRedux({ + betaKey: 'desktop.pinnedMessages.send.beta', + prodKey: 'desktop.pinnedMessages.send.prod', + }); +} diff --git a/ts/util/isPinnedMessagesEnabled.std.ts b/ts/util/isPinnedMessagesEnabled.std.ts deleted file mode 100644 index 3ee4941adb..0000000000 --- a/ts/util/isPinnedMessagesEnabled.std.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2025 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -// Copyright 2025 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -import { - Environment, - getEnvironment, - isMockEnvironment, -} from '../environment.std.js'; - -function isDevEnv(): boolean { - const env = getEnvironment(); - - if ( - env === Environment.Development || - env === Environment.Test || - isMockEnvironment() - ) { - return true; - } - - return false; -} - -export function isPinnedMessagesReceiveEnabled(): boolean { - return isDevEnv(); -} - -export function isPinnedMessagesSendEnabled(): boolean { - return isDevEnv(); -}