Add pinned messages receive/send flags

This commit is contained in:
Jamie
2025-12-15 13:25:35 -08:00
committed by GitHub
parent acc9fd604f
commit b10b6624d9
11 changed files with 66 additions and 47 deletions

View File

@@ -25,6 +25,10 @@ const log = createLogger('RemoteConfig');
const SemverKeys = [ const SemverKeys = [
'desktop.callQualitySurvey.beta', 'desktop.callQualitySurvey.beta',
'desktop.callQualitySurvey.prod', 'desktop.callQualitySurvey.prod',
'desktop.pinnedMessages.receive.beta',
'desktop.pinnedMessages.receive.prod',
'desktop.pinnedMessages.send.beta',
'desktop.pinnedMessages.send.prod',
'desktop.plaintextExport.beta', 'desktop.plaintextExport.beta',
'desktop.plaintextExport.prod', 'desktop.plaintextExport.prod',
] as const; ] as const;

View File

@@ -283,7 +283,7 @@ import {
} from './types/Message2.preload.js'; } from './types/Message2.preload.js';
import { JobCancelReason } from './jobs/types.std.js'; import { JobCancelReason } from './jobs/types.std.js';
import { itemStorage } from './textsecure/Storage.preload.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; const { isNumber, throttle } = lodash;

View File

@@ -4,7 +4,6 @@
import React, { type ReactNode } from 'react'; import React, { type ReactNode } from 'react';
import type { LocalizerType } from '../../types/I18N.std.js'; import type { LocalizerType } from '../../types/I18N.std.js';
import { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.js'; import { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.js';
import { isPinnedMessagesReceiveEnabled } from '../../util/isPinnedMessagesEnabled.std.js';
type MessageContextMenuProps = Readonly<{ type MessageContextMenuProps = Readonly<{
i18n: LocalizerType; i18n: LocalizerType;
@@ -101,12 +100,12 @@ export function MessageContextMenu({
{i18n('icu:copy')} {i18n('icu:copy')}
</AxoMenuBuilder.Item> </AxoMenuBuilder.Item>
)} )}
{isPinnedMessagesReceiveEnabled() && onPinMessage && ( {onPinMessage && (
<AxoMenuBuilder.Item symbol="pin" onSelect={onPinMessage}> <AxoMenuBuilder.Item symbol="pin" onSelect={onPinMessage}>
{i18n('icu:MessageContextMenu__PinMessage')} {i18n('icu:MessageContextMenu__PinMessage')}
</AxoMenuBuilder.Item> </AxoMenuBuilder.Item>
)} )}
{isPinnedMessagesReceiveEnabled() && onUnpinMessage && ( {onUnpinMessage && (
<AxoMenuBuilder.Item symbol="pin-slash" onSelect={onUnpinMessage}> <AxoMenuBuilder.Item symbol="pin-slash" onSelect={onUnpinMessage}>
{i18n('icu:MessageContextMenu__UnpinMessage')} {i18n('icu:MessageContextMenu__UnpinMessage')}
</AxoMenuBuilder.Item> </AxoMenuBuilder.Item>

View File

@@ -84,6 +84,7 @@ function Template(props: {
onPinGoTo={action('onPinGoTo')} onPinGoTo={action('onPinGoTo')}
onPinRemove={action('onPinRemove')} onPinRemove={action('onPinRemove')}
onPinsShowAll={action('onPinsShowAll')} onPinsShowAll={action('onPinsShowAll')}
canPinMessages
/> />
); );
} }

View File

@@ -70,6 +70,7 @@ export type PinnedMessagesBarProps = Readonly<{
onPinGoTo: (messageId: string) => void; onPinGoTo: (messageId: string) => void;
onPinRemove: (messageId: string) => void; onPinRemove: (messageId: string) => void;
onPinsShowAll: () => void; onPinsShowAll: () => void;
canPinMessages: boolean;
}>; }>;
export const PinnedMessagesBar = memo(function PinnedMessagesBar( export const PinnedMessagesBar = memo(function PinnedMessagesBar(
@@ -97,6 +98,7 @@ export const PinnedMessagesBar = memo(function PinnedMessagesBar(
onPinGoTo={props.onPinGoTo} onPinGoTo={props.onPinGoTo}
onPinRemove={props.onPinRemove} onPinRemove={props.onPinRemove}
onPinsShowAll={props.onPinsShowAll} onPinsShowAll={props.onPinsShowAll}
canPinMessages={props.canPinMessages}
/> />
</Container> </Container>
); );
@@ -131,6 +133,7 @@ export const PinnedMessagesBar = memo(function PinnedMessagesBar(
onPinGoTo={props.onPinGoTo} onPinGoTo={props.onPinGoTo}
onPinRemove={props.onPinRemove} onPinRemove={props.onPinRemove}
onPinsShowAll={props.onPinsShowAll} onPinsShowAll={props.onPinsShowAll}
canPinMessages={props.canPinMessages}
/> />
</Tabs.Content> </Tabs.Content>
); );
@@ -236,6 +239,7 @@ const Content = forwardRef(function Content(
onPinGoTo: (messageId: string) => void; onPinGoTo: (messageId: string) => void;
onPinRemove: (messageId: string) => void; onPinRemove: (messageId: string) => void;
onPinsShowAll: () => void; onPinsShowAll: () => void;
canPinMessages: boolean;
}, },
ref: ForwardedRef<HTMLDivElement> ref: ForwardedRef<HTMLDivElement>
): JSX.Element { ): JSX.Element {
@@ -298,9 +302,14 @@ const Content = forwardRef(function Content(
/> />
</AxoDropdownMenu.Trigger> </AxoDropdownMenu.Trigger>
<AxoDropdownMenu.Content> <AxoDropdownMenu.Content>
<AxoDropdownMenu.Item symbol="pin-slash" onSelect={handlePinRemove}> {props.canPinMessages && (
<AxoDropdownMenu.Item
symbol="pin-slash"
onSelect={handlePinRemove}
>
{i18n('icu:PinnedMessagesBar__ActionsMenu__UnpinMessage')} {i18n('icu:PinnedMessagesBar__ActionsMenu__UnpinMessage')}
</AxoDropdownMenu.Item> </AxoDropdownMenu.Item>
)}
<AxoDropdownMenu.Item <AxoDropdownMenu.Item
symbol="message-arrow" symbol="message-arrow"
onSelect={handlePinGoTo} onSelect={handlePinGoTo}

View File

@@ -31,6 +31,7 @@ export type PinnedMessagesPanelProps = Readonly<{
i18n: LocalizerType; i18n: LocalizerType;
conversation: ConversationType; conversation: ConversationType;
pinnedMessages: ReadonlyArray<PinnedMessageRenderData>; pinnedMessages: ReadonlyArray<PinnedMessageRenderData>;
canPinMessages: boolean;
renderTimelineItem: (props: SmartTimelineItemProps) => JSX.Element; renderTimelineItem: (props: SmartTimelineItemProps) => JSX.Element;
}>; }>;
@@ -78,11 +79,13 @@ export const PinnedMessagesPanel = memo(function PinnedMessagesPanel(
); );
})} })}
</ScrollArea> </ScrollArea>
{props.canPinMessages && (
<div className={tw('flex items-center justify-center p-2.5')}> <div className={tw('flex items-center justify-center p-2.5')}>
<AxoButton.Root variant="borderless-primary" size="lg"> <AxoButton.Root variant="borderless-primary" size="lg">
{i18n('icu:PinnedMessagesPanel__UnpinAllMessages')} {i18n('icu:PinnedMessagesPanel__UnpinAllMessages')}
</AxoButton.Root> </AxoButton.Root>
</div> </div>
)}
</div> </div>
); );
}); });

View File

@@ -169,6 +169,7 @@ import type { MessageRequestResponseNotificationData } from '../../components/co
import type { PinnedMessageNotificationData } from '../../components/conversation/pinned-messages/PinnedMessageNotification.dom.js'; import type { PinnedMessageNotificationData } from '../../components/conversation/pinned-messages/PinnedMessageNotification.dom.js';
import { canEditGroupInfo } from '../../util/canEditGroupInfo.preload.js'; import { canEditGroupInfo } from '../../util/canEditGroupInfo.preload.js';
import { getPinnedMessagesMessageIds } from './pinnedMessages.dom.js'; import { getPinnedMessagesMessageIds } from './pinnedMessages.dom.js';
import { isPinnedMessagesSendEnabled } from '../../util/isPinnedMessagesEnabled.dom.js';
const { groupBy, isEmpty, isNumber, isObject, map } = lodash; const { groupBy, isEmpty, isNumber, isObject, map } = lodash;
@@ -2403,6 +2404,9 @@ export function canForward(message: ReadonlyMessageAttributesType): boolean {
} }
export function canPinMessages(conversation: ConversationType): boolean { export function canPinMessages(conversation: ConversationType): boolean {
if (!isPinnedMessagesSendEnabled()) {
return false;
}
return conversation.type === 'direct' || canEditGroupInfo(conversation); return conversation.type === 'direct' || canEditGroupInfo(conversation);
} }

View File

@@ -4,7 +4,10 @@ import React, { memo, useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { getIntl } from '../selectors/user.std.js'; 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 { strictAssert } from '../../util/assert.std.js';
import { useConversationsActions } from '../ducks/conversations.preload.js'; import { useConversationsActions } from '../ducks/conversations.preload.js';
import type { import type {
@@ -20,6 +23,7 @@ import { PinnedMessagesBar } from '../../components/conversation/pinned-messages
import { PanelType } from '../../types/Panels.std.js'; import { PanelType } from '../../types/Panels.std.js';
import type { PinnedMessageId } from '../../types/PinnedMessage.std.js'; import type { PinnedMessageId } from '../../types/PinnedMessage.std.js';
import { import {
canPinMessages as getCanPinMessages,
getMessagePropsSelector, getMessagePropsSelector,
type MessagePropsType, type MessagePropsType,
} from '../selectors/message.preload.js'; } from '../selectors/message.preload.js';
@@ -129,13 +133,18 @@ const selectPins: StateSelector<ReadonlyArray<Pin>> = createSelector(
export const SmartPinnedMessagesBar = memo(function SmartPinnedMessagesBar() { export const SmartPinnedMessagesBar = memo(function SmartPinnedMessagesBar() {
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const conversationId = useSelector(getSelectedConversationId); const conversationId = useSelector(getSelectedConversationId);
const pins = useSelector(selectPins);
strictAssert( strictAssert(
conversationId != null, conversationId != null,
'PinnedMessagesBar should only be rendered in selected conversation' '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 } = const { pushPanelForConversation, scrollToMessage } =
useConversationsActions(); useConversationsActions();
const { onPinnedMessageRemove } = usePinnedMessagesActions(); const { onPinnedMessageRemove } = usePinnedMessagesActions();
@@ -203,6 +212,7 @@ export const SmartPinnedMessagesBar = memo(function SmartPinnedMessagesBar() {
onPinGoTo={handlePinGoTo} onPinGoTo={handlePinGoTo}
onPinRemove={handlePinRemove} onPinRemove={handlePinRemove}
onPinsShowAll={handlePinsShowAll} onPinsShowAll={handlePinsShowAll}
canPinMessages={canPinMessages}
/> />
); );
}); });

View File

@@ -10,6 +10,7 @@ import { PinnedMessagesPanel } from '../../components/conversation/pinned-messag
import type { SmartTimelineItemProps } from './TimelineItem.preload.js'; import type { SmartTimelineItemProps } from './TimelineItem.preload.js';
import { SmartTimelineItem } from './TimelineItem.preload.js'; import { SmartTimelineItem } from './TimelineItem.preload.js';
import { getPinnedMessages } from '../selectors/pinnedMessages.dom.js'; import { getPinnedMessages } from '../selectors/pinnedMessages.dom.js';
import { canPinMessages as getCanPinMessages } from '../selectors/message.preload.js';
export type SmartPinnedMessagesPanelProps = Readonly<{ export type SmartPinnedMessagesPanelProps = Readonly<{
conversationId: string; conversationId: string;
@@ -32,6 +33,7 @@ export const SmartPinnedMessagesPanel = memo(function SmartPinnedMessagesPanel(
); );
const pinnedMessages = useSelector(getPinnedMessages); const pinnedMessages = useSelector(getPinnedMessages);
const canPinMessages = getCanPinMessages(conversation);
return ( return (
<PinnedMessagesPanel <PinnedMessagesPanel
@@ -39,6 +41,7 @@ export const SmartPinnedMessagesPanel = memo(function SmartPinnedMessagesPanel(
conversation={conversation} conversation={conversation}
pinnedMessages={pinnedMessages} pinnedMessages={pinnedMessages}
renderTimelineItem={renderTimelineItem} renderTimelineItem={renderTimelineItem}
canPinMessages={canPinMessages}
/> />
); );
}); });

View File

@@ -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',
});
}

View File

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