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();
-}