mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 10:19:08 +00:00
Add pinned messages receive/send flags
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
{i18n('icu:PinnedMessagesBar__ActionsMenu__UnpinMessage')}
|
<AxoDropdownMenu.Item
|
||||||
</AxoDropdownMenu.Item>
|
symbol="pin-slash"
|
||||||
|
onSelect={handlePinRemove}
|
||||||
|
>
|
||||||
|
{i18n('icu:PinnedMessagesBar__ActionsMenu__UnpinMessage')}
|
||||||
|
</AxoDropdownMenu.Item>
|
||||||
|
)}
|
||||||
<AxoDropdownMenu.Item
|
<AxoDropdownMenu.Item
|
||||||
symbol="message-arrow"
|
symbol="message-arrow"
|
||||||
onSelect={handlePinGoTo}
|
onSelect={handlePinGoTo}
|
||||||
|
|||||||
@@ -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>
|
||||||
<div className={tw('flex items-center justify-center p-2.5')}>
|
{props.canPinMessages && (
|
||||||
<AxoButton.Root variant="borderless-primary" size="lg">
|
<div className={tw('flex items-center justify-center p-2.5')}>
|
||||||
{i18n('icu:PinnedMessagesPanel__UnpinAllMessages')}
|
<AxoButton.Root variant="borderless-primary" size="lg">
|
||||||
</AxoButton.Root>
|
{i18n('icu:PinnedMessagesPanel__UnpinAllMessages')}
|
||||||
</div>
|
</AxoButton.Root>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
18
ts/util/isPinnedMessagesEnabled.dom.ts
Normal file
18
ts/util/isPinnedMessagesEnabled.dom.ts
Normal 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user