Init PinnedMessagesPanel

This commit is contained in:
Jamie
2025-11-20 13:18:31 -08:00
committed by GitHub
parent 60bb04a4fc
commit 9f8c3cd765
25 changed files with 336 additions and 37 deletions

View File

@@ -1678,6 +1678,10 @@
"messageformat": "See all messages",
"description": "Conversation > With pinned message(s) > Pinned messages bar > More actions menu > See all messages"
},
"icu:PinnedMessagesPanel__Title": {
"messageformat": "Pinned messages",
"description": "Conversation > Pinned messages panel (view all) > Title"
},
"icu:sessionEnded": {
"messageformat": "Secure session reset",
"description": "This is a past tense, informational message. In other words, your secure session has been reset."

View File

@@ -9,7 +9,11 @@ import type { AttachmentType } from '../types/Attachment.std.js';
import type { LocalizerType } from '../types/Util.std.js';
import type { MessagePropsType } from '../state/selectors/message.preload.js';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges.preload.js';
import { Message, TextDirection } from './conversation/Message.dom.js';
import {
Message,
MessageInteractivity,
TextDirection,
} from './conversation/Message.dom.js';
import { Modal } from './Modal.dom.js';
import { WidthBreakpoint } from './_util.std.js';
import { shouldNeverBeCalled } from '../util/shouldNeverBeCalled.std.js';
@@ -132,6 +136,7 @@ export function EditHistoryMessagesModal({
displayLimit={displayLimitById[currentMessageId]}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
interactivity={MessageInteractivity.Static}
isEditedMessage
isSpoilerExpanded={revealedSpoilersById[currentMessageId] || {}}
key={currentMessage.timestamp}
@@ -204,6 +209,7 @@ export function EditHistoryMessagesModal({
{...MESSAGE_DEFAULT_PROPS}
{...messageAttributes}
id={syntheticId}
interactivity={MessageInteractivity.Static}
containerElementRef={containerElementRef}
displayLimit={displayLimitById[syntheticId]}
getPreferredBadge={getPreferredBadge}

View File

@@ -22,7 +22,11 @@ import { Avatar, AvatarSize } from './Avatar.dom.js';
import { CompositionInput } from './CompositionInput.dom.js';
import { ContactName } from './conversation/ContactName.dom.js';
import { Emojify } from './conversation/Emojify.dom.js';
import { Message, TextDirection } from './conversation/Message.dom.js';
import {
Message,
MessageInteractivity,
TextDirection,
} from './conversation/Message.dom.js';
import { MessageTimestamp } from './conversation/MessageTimestamp.dom.js';
import { Modal } from './Modal.dom.js';
import { ReactionPicker } from './conversation/ReactionPicker.dom.js';
@@ -673,6 +677,7 @@ function ReplyOrReactionMessage({
i18n={i18n}
platform={platform}
id={reply.id}
interactivity={MessageInteractivity.Normal}
interactionMode="mouse"
isSpoilerExpanded={isSpoilerExpanded}
isVoiceMessagePlayed={false}

View File

@@ -168,6 +168,15 @@ const TextDirectionToDirAttribute = {
export const Directions = ['incoming', 'outgoing'] as const;
export type DirectionType = (typeof Directions)[number];
export enum MessageInteractivity {
/** Enable all interactions for message type */
Normal = 'Normal',
/** Disable all interactions for message type */
Static = 'Static',
/** Enable some interactions for embedded messages (ex: PinnedMessagesPanel) */
Embed = 'Embed',
}
export type AudioAttachmentProps = {
renderingContext: string;
i18n: LocalizerType;
@@ -344,6 +353,7 @@ export type PropsHousekeeping = {
disableScroll?: boolean;
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
interactivity: MessageInteractivity;
interactionMode: InteractionModeType;
platform: string;
renderAudioAttachment: (props: AudioAttachmentProps) => JSX.Element;
@@ -3297,6 +3307,7 @@ export class Message extends React.PureComponent<Props, State> {
attachments,
direction,
i18n,
interactivity,
isSticker,
isSelected,
isSelectMode,
@@ -3352,6 +3363,10 @@ export class Message extends React.PureComponent<Props, State> {
// prevent other click handlers from firing.
onClickCapture: event => {
if (isMacOS ? event.metaKey : event.ctrlKey) {
if (interactivity !== MessageInteractivity.Normal) {
return;
}
if (this.#hasSelectedTextRef.current) {
return;
}

View File

@@ -14,7 +14,7 @@ import type {
Props as MessagePropsType,
PropsData as MessagePropsDataType,
} from './Message.dom.js';
import { Message } from './Message.dom.js';
import { Message, MessageInteractivity } from './Message.dom.js';
import type { LocalizerType, ThemeType } from '../../types/Util.std.js';
import type { ConversationType } from '../../state/ducks/conversations.preload.js';
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges.preload.js';
@@ -351,6 +351,7 @@ export function MessageDetail({
endPoll={endPoll}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
interactivity={MessageInteractivity.Static}
interactionMode={interactionMode}
kickOffAttachmentDownload={kickOffAttachmentDownload}
markAttachmentAsCorrupted={markAttachmentAsCorrupted}

View File

@@ -10,7 +10,7 @@ import { ConversationColors } from '../../types/Colors.std.js';
import { pngUrl } from '../../storybook/Fixtures.std.js';
import type { Props as TimelineMessagesProps } from './TimelineMessage.dom.js';
import { TimelineMessage } from './TimelineMessage.dom.js';
import { TextDirection } from './Message.dom.js';
import { MessageInteractivity, TextDirection } from './Message.dom.js';
import {
AUDIO_MP3,
IMAGE_PNG,
@@ -101,6 +101,7 @@ const defaultMessageProps: TimelineMessagesProps = {
platform: 'darwin',
id: 'messageId',
// renderingContext: 'storybook',
interactivity: MessageInteractivity.Normal,
interactionMode: 'keyboard',
isBlocked: false,
isMessageRequestAccepted: true,

View File

@@ -19,7 +19,7 @@ import { ContactSpoofingType } from '../../util/contactSpoofing.std.js';
import { ReadStatus } from '../../messages/MessageReadStatus.std.js';
import type { WidthBreakpoint } from '../_util.std.js';
import { ThemeType } from '../../types/Util.std.js';
import { TextDirection } from './Message.dom.js';
import { MessageInteractivity, TextDirection } from './Message.dom.js';
import { PaymentEventKind } from '../../types/Payment.std.js';
import type { PropsData as TimelineMessageProps } from './TimelineMessage.dom.js';
import { CollidingAvatars } from '../CollidingAvatars.dom.js';
@@ -373,6 +373,7 @@ const renderItem = ({
isBlocked={false}
isGroup={false}
i18n={i18n}
interactivity={MessageInteractivity.Normal}
interactionMode="keyboard"
isNextItemCallingNotification={false}
theme={ThemeType.light}

View File

@@ -49,6 +49,7 @@ import {
createScrollerLock,
ScrollerLockContext,
} from '../../hooks/useScrollLock.dom.js';
import { MessageInteractivity } from './Message.dom.js';
const { first, get, isNumber, last, throttle } = lodash;
@@ -132,6 +133,7 @@ type PropsHousekeepingType = {
containerElementRef: RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint;
conversationId: string;
interactivity: MessageInteractivity;
isBlocked: boolean;
isGroup: boolean;
isOldestTimelineItem: boolean;
@@ -1069,6 +1071,7 @@ export class Timeline extends React.Component<
containerWidthBreakpoint: widthBreakpoint,
conversationId: id,
isBlocked,
interactivity: MessageInteractivity.Normal,
isGroup,
isOldestTimelineItem: haveOldest && itemIndex === 0,
messageId,

View File

@@ -15,6 +15,7 @@ import { WidthBreakpoint } from '../_util.std.js';
import { ThemeType } from '../../types/Util.std.js';
import { PaymentEventKind } from '../../types/Payment.std.js';
import { ErrorBoundary } from './ErrorBoundary.dom.js';
import { MessageInteractivity } from './Message.dom.js';
const { i18n } = window.SignalContext;
@@ -43,6 +44,7 @@ const getDefaultProps = () => ({
isTargeted: false,
isBlocked: false,
isGroup: false,
interactivity: MessageInteractivity.Normal,
interactionMode: 'keyboard' as const,
theme: ThemeType.light,
platform: 'darwin',

View File

@@ -67,6 +67,7 @@ import {
type MessageRequestResponseNotificationData,
} from './MessageRequestResponseNotification.dom.js';
import type { MessageRequestState } from './MessageRequestActionsConfirmation.dom.js';
import type { MessageInteractivity } from './Message.dom.js';
type CallHistoryType = {
type: 'callHistory';
@@ -200,6 +201,7 @@ type PropsLocalType = {
conversationId: string;
item?: TimelineItemType;
id: string;
interactivity: MessageInteractivity;
isBlocked: boolean;
isGroup: boolean;
isNextItemCallingNotification: boolean;
@@ -242,6 +244,7 @@ export const TimelineItem = memo(function TimelineItem({
getPreferredBadge,
i18n,
id,
interactivity,
isBlocked,
isGroup,
isNextItemCallingNotification,
@@ -281,6 +284,7 @@ export const TimelineItem = memo(function TimelineItem({
<TimelineMessage
{...reducedProps}
{...item.data}
interactivity={interactivity}
isTargeted={isTargeted}
targetMessage={targetMessage}
setMessageToEdit={setMessageToEdit}

View File

@@ -12,7 +12,7 @@ import { ConversationColors } from '../../types/Colors.std.js';
import type { AudioAttachmentProps } from './Message.dom.js';
import type { Props } from './TimelineMessage.dom.js';
import { TimelineMessage } from './TimelineMessage.dom.js';
import { TextDirection } from './Message.dom.js';
import { MessageInteractivity, TextDirection } from './Message.dom.js';
import {
AUDIO_MP3,
IMAGE_JPEG,
@@ -265,6 +265,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
platform: 'darwin',
id: overrideProps.id ?? 'random-message-id',
// renderingContext: 'storybook',
interactivity: MessageInteractivity.Normal,
interactionMode: overrideProps.interactionMode || 'keyboard',
isSticker: isBoolean(overrideProps.isSticker)
? overrideProps.isSticker

View File

@@ -13,7 +13,7 @@ import type { LocalizerType } from '../../types/I18N.std.js';
import { handleOutsideClick } from '../../util/handleOutsideClick.dom.js';
import { offsetDistanceModifier } from '../../util/popperUtil.std.js';
import { WidthBreakpoint } from '../_util.std.js';
import { Message } from './Message.dom.js';
import { Message, MessageInteractivity } from './Message.dom.js';
import type { SmartReactionPicker } from '../../state/smart/ReactionPicker.dom.js';
import type {
Props as MessageProps,
@@ -114,6 +114,7 @@ export function TimelineMessage(props: Props): JSX.Element {
direction,
i18n,
id,
interactivity,
isTargeted,
kickOffAttachmentDownload,
copyMessageText,
@@ -257,6 +258,8 @@ export function TimelineMessage(props: Props): JSX.Element {
const shouldShowAdditional =
doesMessageBodyOverflow(text || '') || !isWindowWidthNotNarrow;
const canSelect = interactivity === MessageInteractivity.Normal;
const handleDownload = canDownload ? openGenericAttachment : null;
const handleReplyToMessage = useCallback(() => {
@@ -317,7 +320,11 @@ export function TimelineMessage(props: Props): JSX.Element {
canRetryDeleteForEveryone ? () => retryDeleteForEveryone(id) : null
}
onCopy={canCopy ? () => copyMessageText(id) : null}
onSelect={() => toggleSelectMessage(conversationId, id, false, true)}
onSelect={
canSelect
? () => toggleSelectMessage(conversationId, id, false, true)
: null
}
onForward={
canForward
? () =>
@@ -350,6 +357,7 @@ export function TimelineMessage(props: Props): JSX.Element {
canEditMessage,
canForward,
canRetry,
canSelect,
canEndPoll,
canRetryDeleteForEveryone,
conversationId,

View File

@@ -63,6 +63,9 @@ import {
getTooltipContent,
} from '../InAnotherCallTooltip.dom.js';
import { BadgeSustainerInstructionsDialog } from '../../BadgeSustainerInstructionsDialog.dom.js';
import { AxoSymbol } from '../../../axo/AxoSymbol.dom.js';
import { isInternalFeaturesEnabled } from '../../../util/isInternalFeaturesEnabled.dom.js';
import { tw } from '../../../axo/tw.dom.js';
enum ModalState {
AddingGroupMembers,
@@ -725,6 +728,23 @@ export function ConversationDetails({
)}
</PanelSection>
)}
{isInternalFeaturesEnabled() && (
<PanelSection title="Internal">
<PanelRow
onClick={() =>
pushPanelForConversation({
type: PanelType.PinnedMessages,
})
}
icon={
<div className={tw('flex size-8 items-center justify-center')}>
<AxoSymbol.Icon symbol="pin" size={20} label={null} />
</div>
}
label="View all pinned messages"
/>
</PanelSection>
)}
{isGroup && (
<ConversationDetailsMembershipList
canAddNewMembers={canAddNewMembers}

View File

@@ -0,0 +1,85 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { Fragment, memo, useMemo, useRef, useState } from 'react';
import { useLayoutEffect } from '@react-aria/utils';
import type { LocalizerType } from '../../../types/I18N.std.js';
import type { ConversationType } from '../../../state/ducks/conversations.preload.js';
import type { PinnedMessage } from '../../../types/PinnedMessage.std.js';
import type { SmartTimelineItemProps } from '../../../state/smart/TimelineItem.preload.js';
import { WidthBreakpoint } from '../../_util.std.js';
import { AxoScrollArea } from '../../../axo/AxoScrollArea.dom.js';
import {
createScrollerLock,
ScrollerLockContext,
} from '../../../hooks/useScrollLock.dom.js';
import { getWidthBreakpoint } from '../../../util/timelineUtil.std.js';
import { strictAssert } from '../../../util/assert.std.js';
import { useSizeObserver } from '../../../hooks/useSizeObserver.dom.js';
import { MessageInteractivity } from '../Message.dom.js';
export type PinnedMessagesPanelProps = Readonly<{
i18n: LocalizerType;
conversation: ConversationType;
pinnedMessages: ReadonlyArray<PinnedMessage>;
renderTimelineItem: (props: SmartTimelineItemProps) => JSX.Element;
}>;
export const PinnedMessagesPanel = memo(function PinnedMessagesPanel(
props: PinnedMessagesPanelProps
) {
const containerElementRef = useRef<HTMLDivElement>(null);
const [containerWidthBreakpoint, setContainerWidthBreakpoint] = useState(
WidthBreakpoint.Wide
);
useLayoutEffect(() => {
strictAssert(containerElementRef.current, 'Missing container ref');
const container = containerElementRef.current;
setContainerWidthBreakpoint(getWidthBreakpoint(container.offsetWidth));
}, []);
useSizeObserver(containerElementRef, size => {
setContainerWidthBreakpoint(getWidthBreakpoint(size.width));
});
const scrollerLock = useMemo(() => {
return createScrollerLock('PinnedMessagesPanel', () => {
// noop - we probably don't need to do anything here because the only
// thing that can happen is the pinned messages getting removed/added
});
}, []);
return (
<AxoScrollArea.Root scrollbarWidth="wide">
<AxoScrollArea.Viewport>
<AxoScrollArea.Content>
<div ref={containerElementRef}>
<ScrollerLockContext.Provider value={scrollerLock}>
{props.pinnedMessages.map((pinnedMessage, pinnedMessageIndex) => {
const next = props.pinnedMessages[pinnedMessageIndex + 1];
const prev = props.pinnedMessages[pinnedMessageIndex - 1];
return (
<Fragment key={pinnedMessage.id}>
{props.renderTimelineItem({
containerElementRef,
containerWidthBreakpoint,
conversationId: props.conversation.id,
interactivity: MessageInteractivity.Embed,
isBlocked: props.conversation.isBlocked ?? false,
isGroup: props.conversation.type === 'group',
isOldestTimelineItem: pinnedMessageIndex === 0,
messageId: pinnedMessage.messageId,
nextMessageId: next?.messageId,
previousMessageId: prev?.messageId,
unreadIndicatorPlacement: undefined,
})}
</Fragment>
);
})}
</ScrollerLockContext.Provider>
</div>
</AxoScrollArea.Content>
</AxoScrollArea.Viewport>
</AxoScrollArea.Root>
);
});

View File

@@ -6,12 +6,6 @@ import { AxoContextMenu } from '../../axo/AxoContextMenu.dom.js';
import type { LocalizerType } from '../../types/I18N.std.js';
import type { ConversationType } from '../../state/ducks/conversations.preload.js';
import { isConversationUnread } from '../../util/isConversationUnread.std.js';
import {
Environment,
getEnvironment,
isMockEnvironment,
} from '../../environment.std.js';
import { isAlpha } from '../../util/version.std.js';
import { drop } from '../../util/drop.std.js';
import { DeleteMessagesConfirmationDialog } from '../DeleteMessagesConfirmationDialog.dom.js';
import { getMuteOptions } from '../../util/getMuteOptions.std.js';
@@ -29,28 +23,7 @@ import { CurrentChatFolders } from '../../types/CurrentChatFolders.std.js';
import { strictAssert } from '../../util/assert.std.js';
import { UserText } from '../UserText.dom.js';
import { isConversationMuted } from '../../util/isConversationMuted.std.js';
function isEnabled() {
const env = getEnvironment();
if (
env === Environment.Development ||
env === Environment.Test ||
isMockEnvironment()
) {
return true;
}
const version = window.getVersion?.();
if (version != null) {
if (isAlpha(version)) {
return true;
}
}
return false;
}
import { isInternalFeaturesEnabled } from '../../util/isInternalFeaturesEnabled.dom.js';
export type ChatFolderToggleChat = (
chatFolderId: ChatFolderId,
@@ -281,7 +254,7 @@ export const LeftPaneConversationListItemContextMenu: FC<LeftPaneConversationLis
>
{i18n('icu:deleteConversation')}
</AxoContextMenu.Item>
{isEnabled() && (
{isInternalFeaturesEnabled() && (
<>
<AxoContextMenu.Separator />
<AxoContextMenu.Group>

View File

@@ -76,6 +76,7 @@ import { getMessageById } from '../../messages/getMessageById.preload.js';
import { canReply, isNormalBubble } from '../selectors/message.preload.js';
import { getAuthorId } from '../../messages/sources.preload.js';
import {
getActivePanel,
getConversationSelector,
getSelectedConversationId,
} from '../selectors/conversations.dom.js';
@@ -835,6 +836,13 @@ export function setQuoteByMessageId(
throw new Error('setQuoteByMessageId: No conversation found');
}
const activePanel = getActivePanel(getState());
if (activePanel != null && messageId != null) {
// Reset the conversation panels and scroll to the message
// in case we're inside of a conversation panel like pinned messages
dispatch(scrollToMessage(conversationId, messageId));
}
const draftEditMessage = conversation.get('draftEditMessage');
// We can remove quotes, but we can't add them
if (draftEditMessage && messageId) {

View File

@@ -95,6 +95,7 @@ import {
getMessagesByConversation,
getPendingAvatarDownloadSelector,
getAllConversations,
getActivePanel,
} from '../selectors/conversations.dom.js';
import { getIntl } from '../selectors/user.std.js';
import type {
@@ -2102,6 +2103,13 @@ function setMessageToEdit(
return;
}
const activePanel = getActivePanel(getState());
if (activePanel != null) {
// Reset the conversation panels and scroll to the message
// in case we're inside of a conversation panel like pinned messages
dispatch(scrollToMessage(conversationId, messageId));
}
setQuoteByMessageId(conversationId, undefined)(
dispatch,
getState,

View File

@@ -37,6 +37,7 @@ import { missingCaseError } from '../../util/missingCaseError.std.js';
import { useConversationsActions } from '../ducks/conversations.preload.js';
import { useReducedMotion } from '../../hooks/useReducedMotion.dom.js';
import { itemStorage } from '../../textsecure/Storage.preload.js';
import { SmartPinnedMessagesPanel } from './PinnedMessagesPanel.preload.js';
const log = createLogger('ConversationPanel');
@@ -381,6 +382,10 @@ function PanelElement({
);
}
if (panel.type === PanelType.PinnedMessages) {
return <SmartPinnedMessagesPanel conversationId={conversationId} />;
}
if (panel.type === PanelType.StickerManager) {
return <SmartStickerManager />;
}
@@ -399,6 +404,7 @@ function getPanelKey(panel: PanelRenderType): string {
case PanelType.GroupPermissions:
case PanelType.GroupV1Members:
case PanelType.NotificationSettings:
case PanelType.PinnedMessages:
case PanelType.StickerManager:
return panel.type;
case PanelType.MessageDetails:

View File

@@ -0,0 +1,102 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import type { AciString } from '@signalapp/mock-server/src/types.js';
import { getIntl } from '../selectors/user.std.js';
import { getConversationByIdSelector } from '../selectors/conversations.dom.js';
import { strictAssert } from '../../util/assert.std.js';
import { PinnedMessagesPanel } from '../../components/conversation/pinned-messages/PinnedMessagesPanel.dom.js';
import type { SmartTimelineItemProps } from './TimelineItem.preload.js';
import { SmartTimelineItem } from './TimelineItem.preload.js';
import type { StateSelector } from '../types.std.js';
import type {
PinnedMessage,
PinnedMessageId,
} from '../../types/PinnedMessage.std.js';
import type { StateType } from '../reducer.preload.js';
import { itemStorage } from '../../textsecure/Storage.preload.js';
import { isAciString } from '../../util/isAciString.std.js';
export type SmartPinnedMessagesPanelProps = Readonly<{
conversationId: string;
}>;
function renderTimelineItem(props: SmartTimelineItemProps) {
return <SmartTimelineItem {...props} />;
}
const mockSelectPinnedMessages: StateSelector<ReadonlyArray<PinnedMessage>> =
createSelector(
(state: StateType) => state.conversations,
conversations => {
const selectedConversationId =
conversations.selectedConversationId ?? null;
if (selectedConversationId == null) {
throw new Error();
}
const messageIds =
conversations.messagesByConversation[selectedConversationId]
?.messageIds ?? [];
const ourAci = itemStorage.user.getCheckedAci();
return messageIds
.map(messageId => {
return conversations.messagesLookup[messageId] ?? null;
})
.filter(message => {
return message.type === 'incoming' || message.type === 'outgoing';
})
.slice(-10)
.map((message, messageIndex): PinnedMessage => {
let messageSenderAci: AciString;
if (message.type === 'outgoing') {
messageSenderAci = ourAci;
} else {
strictAssert(
isAciString(message.sourceServiceId),
'sourceServiceId must be aci string for incoming message'
);
messageSenderAci = message.sourceServiceId;
}
return {
id: messageIndex as PinnedMessageId,
conversationId: selectedConversationId,
messageId: message.id,
messageSentAt: message.sent_at,
messageSenderAci,
pinnedByAci: ourAci,
pinnedAt: Date.now(),
expiresAt: null,
};
});
}
);
export const SmartPinnedMessagesPanel = memo(function SmartPinnedMessagesPanel(
props: SmartPinnedMessagesPanelProps
) {
const i18n = useSelector(getIntl);
const conversationSelector = useSelector(getConversationByIdSelector);
const conversation = conversationSelector(props.conversationId);
strictAssert(
conversation,
'<SmartPinnedMessagesPanel> expected a conversation to be found'
);
const mockPinnedMessages = useSelector(mockSelectPinnedMessages);
return (
<PinnedMessagesPanel
i18n={i18n}
conversation={conversation}
pinnedMessages={mockPinnedMessages}
renderTimelineItem={renderTimelineItem}
/>
);
});

View File

@@ -58,6 +58,7 @@ function renderItem({
containerElementRef,
containerWidthBreakpoint,
conversationId,
interactivity,
isBlocked,
isGroup,
isOldestTimelineItem,
@@ -71,6 +72,7 @@ function renderItem({
containerElementRef={containerElementRef}
containerWidthBreakpoint={containerWidthBreakpoint}
conversationId={conversationId}
interactivity={interactivity}
isBlocked={isBlocked}
isGroup={isGroup}
isOldestTimelineItem={isOldestTimelineItem}

View File

@@ -39,11 +39,13 @@ import { renderAudioAttachment } from './renderAudioAttachment.preload.js';
import { renderReactionPicker } from './renderReactionPicker.dom.js';
import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation.dom.js';
import { TargetedMessageSource } from '../ducks/conversationsEnums.std.js';
import type { MessageInteractivity } from '../../components/conversation/Message.dom.js';
export type SmartTimelineItemProps = {
containerElementRef: RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint;
conversationId: string;
interactivity: MessageInteractivity;
isBlocked: boolean;
isGroup: boolean;
isOldestTimelineItem: boolean;
@@ -67,6 +69,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
containerElementRef,
containerWidthBreakpoint,
conversationId,
interactivity,
isBlocked,
isGroup,
isOldestTimelineItem,
@@ -209,6 +212,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
shouldRenderDateHeader={shouldRenderDateHeader}
showEditHistoryModal={showEditHistoryModal}
i18n={i18n}
interactivity={interactivity}
interactionMode={interactionMode}
isBlocked={isBlocked}
isGroup={isGroup}

View File

@@ -16,6 +16,7 @@ export enum PanelType {
GroupV1Members = 'GroupV1Members',
MessageDetails = 'MessageDetails',
NotificationSettings = 'NotificationSettings',
PinnedMessages = 'PinnedMessages',
StickerManager = 'StickerManager',
}
@@ -35,6 +36,7 @@ export type PanelRequestType = ReadonlyDeep<
| { type: PanelType.GroupV1Members }
| { type: PanelType.MessageDetails; args: { messageId: string } }
| { type: PanelType.NotificationSettings }
| { type: PanelType.PinnedMessages }
| { type: PanelType.StickerManager }
>;
@@ -57,5 +59,6 @@ export type PanelRenderType = ReadonlyDeep<
args: { message: ReadonlyMessageAttributesType };
}
| { type: PanelType.NotificationSettings }
| { type: PanelType.PinnedMessages }
| { type: PanelType.StickerManager }
>;

View File

@@ -47,6 +47,10 @@ export function getConversationTitleForPanelType(
return i18n('icu:ConversationDetails--notifications');
}
if (panelType === PanelType.PinnedMessages) {
return i18n('icu:PinnedMessagesPanel__Title');
}
if (panelType === PanelType.StickerManager) {
return '';
}

View File

@@ -0,0 +1,26 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import {
Environment,
getEnvironment,
isMockEnvironment,
} from '../environment.std.js';
import * as RemoteConfig from '../RemoteConfig.dom.js';
/**
* This should be reserved for internal-only features that are focused on
* debugging and development, and will never be enabled in production.
*/
export function isInternalFeaturesEnabled(): boolean {
const env = getEnvironment();
if (
env === Environment.Development ||
env === Environment.Test ||
isMockEnvironment()
) {
return true;
}
return RemoteConfig.isEnabled('desktop.internalUser');
}

View File

@@ -1950,6 +1950,13 @@
"updated": "2025-11-06T20:28:00.760Z",
"reasonDetail": "Ref for timer"
},
{
"rule": "React-useRef",
"path": "ts/components/conversation/pinned-messages/PinnedMessagesPanel.dom.tsx",
"line": " const containerElementRef = useRef<HTMLDivElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2025-11-20T18:33:59.075Z"
},
{
"rule": "React-useRef",
"path": "ts/components/fun/FunGif.dom.tsx",