PinnedMessagesPanel: Add footer with Unpin all messages button

This commit is contained in:
Jamie
2025-11-21 07:13:05 -08:00
committed by GitHub
parent 025d5d5011
commit 954bb8591b
4 changed files with 72 additions and 30 deletions

View File

@@ -1682,6 +1682,10 @@
"messageformat": "Pinned messages", "messageformat": "Pinned messages",
"description": "Conversation > Pinned messages panel (view all) > Title" "description": "Conversation > Pinned messages panel (view all) > Title"
}, },
"icu:PinnedMessagesPanel__UnpinAllMessages": {
"messageformat": "Unpin all messages",
"description": "Conversation > Pinned messages panel (view all) > Unpin all messages button"
},
"icu:sessionEnded": { "icu:sessionEnded": {
"messageformat": "Secure session reset", "messageformat": "Secure session reset",
"description": "This is a past tense, informational message. In other words, your secure session has been reset." "description": "This is a past tense, informational message. In other words, your secure session has been reset."

View File

@@ -10,7 +10,6 @@
height: 100%; height: 100%;
inset-inline-start: 0; inset-inline-start: 0;
overflow-y: auto;
position: absolute; position: absolute;
top: 0; top: 0;
width: 100%; width: 100%;
@@ -28,11 +27,12 @@
// Used for centering EmptyState in All Media view // Used for centering EmptyState in All Media view
position: relative; position: relative;
overflow-y: auto;
flex-grow: 1; flex-grow: 1;
padding-top: calc(
#{variables.$header-height} + var(--title-bar-drag-area-height) &--padding {
); padding-inline: 24px;
padding-inline: 24px; }
} }
&__header { &__header {
@@ -44,7 +44,6 @@
#{variables.$header-height} + var(--title-bar-drag-area-height) #{variables.$header-height} + var(--title-bar-drag-area-height)
); );
padding-top: var(--title-bar-drag-area-height); padding-top: var(--title-bar-drag-area-height);
position: fixed;
width: 100%; width: 100%;
z-index: variables.$z-index-base; z-index: variables.$z-index-base;

View File

@@ -1,6 +1,14 @@
// Copyright 2025 Signal Messenger, LLC // Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { Fragment, memo, useMemo, useRef, useState } from 'react'; import type { ForwardedRef, ReactNode } from 'react';
import React, {
forwardRef,
Fragment,
memo,
useMemo,
useRef,
useState,
} from 'react';
import { useLayoutEffect } from '@react-aria/utils'; import { useLayoutEffect } from '@react-aria/utils';
import type { LocalizerType } from '../../../types/I18N.std.js'; import type { LocalizerType } from '../../../types/I18N.std.js';
import type { ConversationType } from '../../../state/ducks/conversations.preload.js'; import type { ConversationType } from '../../../state/ducks/conversations.preload.js';
@@ -16,6 +24,8 @@ import { getWidthBreakpoint } from '../../../util/timelineUtil.std.js';
import { strictAssert } from '../../../util/assert.std.js'; import { strictAssert } from '../../../util/assert.std.js';
import { useSizeObserver } from '../../../hooks/useSizeObserver.dom.js'; import { useSizeObserver } from '../../../hooks/useSizeObserver.dom.js';
import { MessageInteractivity } from '../Message.dom.js'; import { MessageInteractivity } from '../Message.dom.js';
import { tw } from '../../../axo/tw.dom.js';
import { AxoButton } from '../../../axo/AxoButton.dom.js';
export type PinnedMessagesPanelProps = Readonly<{ export type PinnedMessagesPanelProps = Readonly<{
i18n: LocalizerType; i18n: LocalizerType;
@@ -27,6 +37,7 @@ export type PinnedMessagesPanelProps = Readonly<{
export const PinnedMessagesPanel = memo(function PinnedMessagesPanel( export const PinnedMessagesPanel = memo(function PinnedMessagesPanel(
props: PinnedMessagesPanelProps props: PinnedMessagesPanelProps
) { ) {
const { i18n } = props;
const containerElementRef = useRef<HTMLDivElement>(null); const containerElementRef = useRef<HTMLDivElement>(null);
const [containerWidthBreakpoint, setContainerWidthBreakpoint] = useState( const [containerWidthBreakpoint, setContainerWidthBreakpoint] = useState(
WidthBreakpoint.Wide WidthBreakpoint.Wide
@@ -42,6 +53,44 @@ export const PinnedMessagesPanel = memo(function PinnedMessagesPanel(
setContainerWidthBreakpoint(getWidthBreakpoint(size.width)); setContainerWidthBreakpoint(getWidthBreakpoint(size.width));
}); });
return (
<div className={tw('flex h-full flex-col')}>
<ScrollArea ref={containerElementRef}>
{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>
);
})}
</ScrollArea>
<div className={tw('flex items-center justify-center p-2.5')}>
<AxoButton.Root variant="borderless-primary" size="lg">
{i18n('icu:PinnedMessagesPanel__UnpinAllMessages')}
</AxoButton.Root>
</div>
</div>
);
});
const ScrollArea = forwardRef(function ScrollArea(
props: { children: ReactNode },
ref: ForwardedRef<HTMLDivElement>
) {
const scrollerLock = useMemo(() => { const scrollerLock = useMemo(() => {
return createScrollerLock('PinnedMessagesPanel', () => { return createScrollerLock('PinnedMessagesPanel', () => {
// noop - we probably don't need to do anything here because the only // noop - we probably don't need to do anything here because the only
@@ -51,31 +100,13 @@ export const PinnedMessagesPanel = memo(function PinnedMessagesPanel(
return ( return (
<AxoScrollArea.Root scrollbarWidth="wide"> <AxoScrollArea.Root scrollbarWidth="wide">
<AxoScrollArea.Hint edge="top" />
<AxoScrollArea.Hint edge="bottom" />
<AxoScrollArea.Viewport> <AxoScrollArea.Viewport>
<AxoScrollArea.Content> <AxoScrollArea.Content>
<div ref={containerElementRef}> <div ref={ref}>
<ScrollerLockContext.Provider value={scrollerLock}> <ScrollerLockContext.Provider value={scrollerLock}>
{props.pinnedMessages.map((pinnedMessage, pinnedMessageIndex) => { {props.children}
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> </ScrollerLockContext.Provider>
</div> </div>
</AxoScrollArea.Content> </AxoScrollArea.Content>

View File

@@ -11,6 +11,7 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import classNames from 'classnames';
import type { PanelRenderType } from '../../types/Panels.std.js'; import type { PanelRenderType } from '../../types/Panels.std.js';
import { createLogger } from '../../logging/log.std.js'; import { createLogger } from '../../logging/log.std.js';
import { PanelType } from '../../types/Panels.std.js'; import { PanelType } from '../../types/Panels.std.js';
@@ -322,7 +323,14 @@ const PanelContainer = forwardRef<
</div> </div>
)} )}
</div> </div>
<div className="ConversationPanel__body" ref={focusRef}> <div
className={classNames(
'ConversationPanel__body',
panel.type !== PanelType.PinnedMessages &&
'ConversationPanel__body--padding'
)}
ref={focusRef}
>
<PanelElement conversationId={conversationId} panel={panel} /> <PanelElement conversationId={conversationId} panel={panel} />
</div> </div>
</div> </div>