mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-19 17:58:48 +00:00
Init PinnedMessagesBar UI
This commit is contained in:
@@ -1650,6 +1650,34 @@
|
||||
"messageformat": "Pin",
|
||||
"description": "Message > Context Menu > Pin Message > Dialog > Pin Button"
|
||||
},
|
||||
"icu:PinnedMessagesBar__AccessibilityLabel": {
|
||||
"messageformat": "{pinsCount, plural, one {Pinned message} other {Pinned messages}}",
|
||||
"description": "Conversation > With pinned message(s) > Pinned messages bar > Accessibility label"
|
||||
},
|
||||
"icu:PinnedMessagesBar__Tab__AccessibilityLabel": {
|
||||
"messageformat": "Go to pin {pinNumber, number}",
|
||||
"description": "Conversation > With *multiple* pinned messages > Pinned messages bar > Vertical tabs > Tab item > Accessibility Label"
|
||||
},
|
||||
"icu:PinnedMessagesBar__GoToMessageClickableArea__AccessibilityLabel": {
|
||||
"messageformat": "Go to message",
|
||||
"description": "Conversation > With pinned message(s) > Pinned messages bar > Clickable area (Goes to pinned message) > Accessibility label"
|
||||
},
|
||||
"icu:PinnedMessagesBar__ActionsMenu__Button__AccessibilityLabel": {
|
||||
"messageformat": "More actions",
|
||||
"description": "Conversation > With pinned message(s) > Pinned messages bar > More actions menu button > Accessibility label"
|
||||
},
|
||||
"icu:PinnedMessagesBar__ActionsMenu__UnpinMessage": {
|
||||
"messageformat": "Unpin message",
|
||||
"description": "Conversation > With pinned message(s) > Pinned messages bar > More actions menu > Unpin message"
|
||||
},
|
||||
"icu:PinnedMessagesBar__ActionsMenu__GoToMessage": {
|
||||
"messageformat": "Go to message",
|
||||
"description": "Conversation > With pinned message(s) > Pinned messages bar > More actions menu > Go to message"
|
||||
},
|
||||
"icu:PinnedMessagesBar__ActionsMenu__SeeAllMessages": {
|
||||
"messageformat": "See all messages",
|
||||
"description": "Conversation > With pinned message(s) > Pinned messages bar > More actions menu > See all messages"
|
||||
},
|
||||
"icu:sessionEnded": {
|
||||
"messageformat": "Secure session reset",
|
||||
"description": "This is a past tense, informational message. In other words, your secure session has been reset."
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useCallback, useRef, useState } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { ReactNode, MouseEvent, FC } from 'react';
|
||||
import { useLayoutEffect } from '@react-aria/utils';
|
||||
import { computeAccessibleName } from 'dom-accessibility-api';
|
||||
import { tw } from './tw.dom.js';
|
||||
import { assert } from './_internal/assert.dom.js';
|
||||
import {
|
||||
@@ -146,6 +147,10 @@ export namespace AriaClickable {
|
||||
*/
|
||||
|
||||
export type HiddenTriggerProps = Readonly<{
|
||||
/**
|
||||
* Describe the action to be taken `onClick`
|
||||
*/
|
||||
'aria-label'?: string;
|
||||
/**
|
||||
* This should reference the ID of an element that describes the action that
|
||||
* will be taken `onClick`, not the entire clickable root.
|
||||
@@ -156,10 +161,12 @@ export namespace AriaClickable {
|
||||
* <HiddenTrigger aria-labelledby="see-more-1"/>
|
||||
* ```
|
||||
*/
|
||||
'aria-labelledby': string;
|
||||
'aria-labelledby'?: string;
|
||||
onClick: (event: MouseEvent) => void;
|
||||
}>;
|
||||
|
||||
const hiddenTriggerDisplayName = `${Namespace}.HiddenTrigger`;
|
||||
|
||||
/**
|
||||
* Provides an invisible button that fills the entire area of
|
||||
* `<AriaClickable.Root>`
|
||||
@@ -221,16 +228,26 @@ export namespace AriaClickable {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
assert(
|
||||
computeAccessibleName(assert(ref.current)) !== '',
|
||||
`${hiddenTriggerDisplayName} child must have an accessible name`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
className={tw('absolute inset-0 z-10 outline-0')}
|
||||
aria-label={props['aria-label']}
|
||||
aria-labelledby={props['aria-labelledby']}
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
HiddenTrigger.displayName = `${Namespace}.HiddenTrigger`;
|
||||
HiddenTrigger.displayName = hiddenTriggerDisplayName;
|
||||
}
|
||||
|
||||
@@ -66,11 +66,11 @@ const Namespace = 'AxoContextMenu';
|
||||
* ```
|
||||
*/
|
||||
export namespace AxoContextMenu {
|
||||
export type RootContextType = Readonly<{
|
||||
type RootContextType = Readonly<{
|
||||
open: boolean;
|
||||
}>;
|
||||
|
||||
export const RootContext = createStrictContext<RootContextType>(
|
||||
const RootContext = createStrictContext<RootContextType>(
|
||||
`${Namespace}.RootContext`
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useEffect, useId, useRef } from 'react';
|
||||
import React, {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { DropdownMenu } from 'radix-ui';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { computeAccessibleName } from 'dom-accessibility-api';
|
||||
@@ -17,6 +25,10 @@ import {
|
||||
getElementAriaRole,
|
||||
isAriaWidgetRole,
|
||||
} from './_internal/ariaRoles.dom.js';
|
||||
import {
|
||||
createStrictContext,
|
||||
useStrictContext,
|
||||
} from './_internal/StrictContext.dom.js';
|
||||
|
||||
const Namespace = 'AxoDropdownMenu';
|
||||
|
||||
@@ -63,6 +75,14 @@ const Namespace = 'AxoDropdownMenu';
|
||||
* ```
|
||||
*/
|
||||
export namespace AxoDropdownMenu {
|
||||
type RootContextType = Readonly<{
|
||||
open: boolean;
|
||||
}>;
|
||||
|
||||
const RootContext = createStrictContext<RootContextType>(
|
||||
`${Namespace}.RootContext`
|
||||
);
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.Root>
|
||||
* ---------------------------------
|
||||
@@ -71,17 +91,37 @@ export namespace AxoDropdownMenu {
|
||||
export type RootProps = AxoBaseMenu.MenuRootProps &
|
||||
Readonly<{
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Contains all the parts of a dropdown menu.
|
||||
*/
|
||||
export const Root: FC<RootProps> = memo(props => {
|
||||
const { onOpenChange } = props;
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (typeof props.open === 'boolean' && open !== props.open) {
|
||||
setOpen(props.open);
|
||||
}
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
setOpen(nextOpen);
|
||||
onOpenChange?.(nextOpen);
|
||||
},
|
||||
[onOpenChange]
|
||||
);
|
||||
|
||||
const context = useMemo((): RootContextType => {
|
||||
return { open };
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root open={props.open} onOpenChange={props.onOpenChange}>
|
||||
{props.children}
|
||||
</DropdownMenu.Root>
|
||||
<RootContext.Provider value={context}>
|
||||
<DropdownMenu.Root open={open} onOpenChange={handleOpenChange}>
|
||||
{props.children}
|
||||
</DropdownMenu.Root>
|
||||
</RootContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -102,6 +142,7 @@ export namespace AxoDropdownMenu {
|
||||
* against the trigger.
|
||||
*/
|
||||
export const Trigger: FC<TriggerProps> = memo(props => {
|
||||
const context = useStrictContext(RootContext);
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -122,7 +163,13 @@ export namespace AxoDropdownMenu {
|
||||
});
|
||||
|
||||
return (
|
||||
<DropdownMenu.Trigger ref={ref} asChild disabled={props.disabled}>
|
||||
<DropdownMenu.Trigger
|
||||
ref={ref}
|
||||
asChild
|
||||
disabled={props.disabled}
|
||||
data-axo-dropdownmenu-trigger
|
||||
data-axo-dropdownmenu-state={context.open ? 'open' : 'closed'}
|
||||
>
|
||||
{props.children}
|
||||
</DropdownMenu.Trigger>
|
||||
);
|
||||
|
||||
@@ -36,31 +36,38 @@ export namespace AxoIconButton {
|
||||
const Variants: Record<Variant, TailwindStyles> = {
|
||||
secondary: tw(
|
||||
'bg-fill-secondary pressed:bg-fill-secondary-pressed',
|
||||
'data-[axo-dropdownmenu-state=open]:bg-fill-secondary-pressed',
|
||||
'text-label-primary disabled:text-label-disabled',
|
||||
pressedStyles.fillInverted
|
||||
),
|
||||
primary: tw(
|
||||
'bg-color-fill-primary pressed:bg-color-fill-primary-pressed',
|
||||
'data-[axo-dropdownmenu-state=open]:bg-color-fill-primary-pressed',
|
||||
'text-label-primary-on-color disabled:text-label-disabled-on-color',
|
||||
pressedStyles.fillInverted
|
||||
),
|
||||
affirmative: tw(
|
||||
'bg-color-fill-affirmative pressed:bg-color-fill-affirmative-pressed',
|
||||
'data-[axo-dropdownmenu-state=open]:bg-color-fill-affirmative-pressed',
|
||||
'text-label-primary-on-color disabled:text-label-disabled-on-color',
|
||||
pressedStyles.fillInverted
|
||||
),
|
||||
destructive: tw(
|
||||
'bg-color-fill-destructive pressed:bg-color-fill-destructive-pressed',
|
||||
'data-[axo-dropdownmenu-state=open]:bg-color-fill-destructive-pressed',
|
||||
'text-label-primary-on-color disabled:text-label-disabled-on-color',
|
||||
pressedStyles.fillInverted
|
||||
),
|
||||
'borderless-secondary': tw(
|
||||
'hovered:bg-fill-secondary pressed:bg-fill-secondary-pressed',
|
||||
'focus:bg-fill-secondary',
|
||||
'data-[axo-dropdownmenu-state=open]:bg-fill-secondary-pressed',
|
||||
'text-label-primary disabled:text-label-disabled',
|
||||
pressedStyles.colorFillPrimary
|
||||
),
|
||||
'floating-secondary': tw(
|
||||
'bg-fill-floating pressed:bg-fill-floating-pressed',
|
||||
'data-[axo-dropdownmenu-state=open]:bg-fill-floating-pressed',
|
||||
'text-label-primary disabled:text-label-disabled',
|
||||
'shadow-elevation-1',
|
||||
pressedStyles.fillInverted
|
||||
@@ -130,7 +137,7 @@ export namespace AxoIconButton {
|
||||
>
|
||||
<span
|
||||
className={tw(
|
||||
'forced-color-adjust-none',
|
||||
'align-top leading-none forced-color-adjust-none',
|
||||
experimentalSpinner != null ? 'opacity-0' : null
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { useState } from 'react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Pin, PinId } from './PinnedMessagesBar.dom.js';
|
||||
import { PinnedMessagesBar } from './PinnedMessagesBar.dom.js';
|
||||
import { tw } from '../../../axo/tw.dom.js';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
export default {
|
||||
title: 'Components/PinnedMessages/PinnedMessagesBar',
|
||||
} satisfies Meta;
|
||||
|
||||
const PIN_1: Pin = {
|
||||
id: 'pin-1' as PinId,
|
||||
sender: {
|
||||
id: 'conversation-1',
|
||||
title: 'Jamie',
|
||||
isMe: true,
|
||||
},
|
||||
message: {
|
||||
id: 'message-1',
|
||||
body: 'What should we get for lunch?',
|
||||
},
|
||||
};
|
||||
|
||||
const PIN_2: Pin = {
|
||||
id: 'pin-2' as PinId,
|
||||
sender: {
|
||||
id: 'conversation-1',
|
||||
title: 'Tyler',
|
||||
isMe: false,
|
||||
},
|
||||
message: {
|
||||
id: 'message-2',
|
||||
body: 'We found a cute pottery store close to Inokashira Park that we’re going to check out on Saturday. Anyone want to meet at the south exit at Kichijoji station at 1pm? Too early?',
|
||||
},
|
||||
};
|
||||
|
||||
const PIN_3: Pin = {
|
||||
id: 'pin-3' as PinId,
|
||||
sender: {
|
||||
id: 'conversation-1',
|
||||
title: 'Adrian',
|
||||
isMe: false,
|
||||
},
|
||||
message: {
|
||||
id: 'message-3',
|
||||
body: 'Photo',
|
||||
attachment: {
|
||||
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function Template(props: { defaultCurrent: PinId; pins: ReadonlyArray<Pin> }) {
|
||||
const [current, setCurrent] = useState(props.defaultCurrent);
|
||||
return (
|
||||
<PinnedMessagesBar
|
||||
i18n={i18n}
|
||||
current={current}
|
||||
onCurrentChange={setCurrent}
|
||||
pins={props.pins}
|
||||
onPinGoTo={action('onPinGoTo')}
|
||||
onPinRemove={action('onPinRemove')}
|
||||
onPinsShowAll={action('onPinsShowAll')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Default(): JSX.Element {
|
||||
return (
|
||||
<div className={tw('flex max-w-4xl flex-col gap-4 bg-fill-inverted p-4')}>
|
||||
<Template defaultCurrent={PIN_1.id} pins={[PIN_1]} />
|
||||
<Template defaultCurrent={PIN_2.id} pins={[PIN_1, PIN_2]} />
|
||||
<Template defaultCurrent={PIN_3.id} pins={[PIN_1, PIN_2, PIN_3]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { Tabs } from 'radix-ui';
|
||||
import type { LocalizerType } from '../../../types/I18N.std.js';
|
||||
import { tw } from '../../../axo/tw.dom.js';
|
||||
import { strictAssert } from '../../../util/assert.std.js';
|
||||
import { AxoIconButton } from '../../../axo/AxoIconButton.dom.js';
|
||||
import { AxoDropdownMenu } from '../../../axo/AxoDropdownMenu.dom.js';
|
||||
import { AriaClickable } from '../../../axo/AriaClickable.dom.js';
|
||||
import { UserText } from '../../UserText.dom.js';
|
||||
|
||||
export type PinId = string & { PinId: never };
|
||||
|
||||
export type Pin = Readonly<{
|
||||
id: PinId;
|
||||
sender: {
|
||||
id: string;
|
||||
title: string;
|
||||
isMe: boolean;
|
||||
};
|
||||
message: {
|
||||
id: string;
|
||||
body: string;
|
||||
attachment?: {
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
export type PinnedMessagesBarProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
pins: ReadonlyArray<Pin>;
|
||||
current: PinId;
|
||||
onCurrentChange: (current: PinId) => void;
|
||||
onPinGoTo: (pinId: PinId) => void;
|
||||
onPinRemove: (pinId: PinId) => void;
|
||||
onPinsShowAll: () => void;
|
||||
}>;
|
||||
|
||||
export const PinnedMessagesBar = memo(function PinnedMessagesBar(
|
||||
props: PinnedMessagesBarProps
|
||||
) {
|
||||
const { i18n, onCurrentChange } = props;
|
||||
|
||||
strictAssert(props.pins.length > 0, 'Must have at least one pin');
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(value: string) => {
|
||||
onCurrentChange(value as PinId);
|
||||
},
|
||||
[onCurrentChange]
|
||||
);
|
||||
|
||||
if (props.pins.length === 1) {
|
||||
const pin = props.pins.at(0);
|
||||
strictAssert(pin != null, 'Missing pin');
|
||||
return (
|
||||
<Container i18n={i18n} pinsCount={props.pins.length}>
|
||||
<Content
|
||||
i18n={i18n}
|
||||
pin={pin}
|
||||
onPinGoTo={props.onPinGoTo}
|
||||
onPinRemove={props.onPinRemove}
|
||||
onPinsShowAll={props.onPinsShowAll}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs.Root
|
||||
orientation="vertical"
|
||||
value={props.current}
|
||||
onValueChange={handleValueChange}
|
||||
asChild
|
||||
activationMode="manual"
|
||||
>
|
||||
<Container i18n={i18n} pinsCount={props.pins.length}>
|
||||
<TabsList
|
||||
i18n={i18n}
|
||||
pins={props.pins}
|
||||
current={props.current}
|
||||
onCurrentChange={props.onCurrentChange}
|
||||
/>
|
||||
{props.pins.map(pin => {
|
||||
return (
|
||||
<Tabs.Content key={pin.id} tabIndex={-1} value={pin.id} asChild>
|
||||
<Content
|
||||
i18n={i18n}
|
||||
pin={pin}
|
||||
onPinGoTo={props.onPinGoTo}
|
||||
onPinRemove={props.onPinRemove}
|
||||
onPinsShowAll={props.onPinsShowAll}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
</Tabs.Root>
|
||||
);
|
||||
});
|
||||
|
||||
function Container(props: {
|
||||
i18n: LocalizerType;
|
||||
pinsCount: number;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { i18n } = props;
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={i18n('icu:PinnedMessagesBar__AccessibilityLabel', {
|
||||
pinsCount: props.pinsCount,
|
||||
})}
|
||||
>
|
||||
<AriaClickable.Root
|
||||
className={tw(
|
||||
'flex h-14 items-center bg-background-primary py-2.5 pe-3 select-none',
|
||||
'rounded-xs',
|
||||
'outline-0 outline-border-focused',
|
||||
'data-[focused]:outline-[2.5px]',
|
||||
props.pinsCount === 1 && 'ps-4'
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</AriaClickable.Root>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsList(props: {
|
||||
i18n: LocalizerType;
|
||||
pins: ReadonlyArray<Pin>;
|
||||
current: PinId;
|
||||
onCurrentChange: (current: PinId) => void;
|
||||
}) {
|
||||
const { i18n } = props;
|
||||
|
||||
strictAssert(props.pins.length >= 2, 'Too few pins for tabs');
|
||||
strictAssert(props.pins.length <= 3, 'Too many pins for tabs');
|
||||
|
||||
return (
|
||||
<AriaClickable.SubWidget>
|
||||
<Tabs.List className={tw('flex h-full flex-col')}>
|
||||
{props.pins.toReversed().map((pin, pinIndex) => {
|
||||
return (
|
||||
<TabTrigger
|
||||
key={pin.id}
|
||||
i18n={i18n}
|
||||
pin={pin}
|
||||
pinNumber={pinIndex + 1}
|
||||
pinsCount={props.pins.length}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs.List>
|
||||
</AriaClickable.SubWidget>
|
||||
);
|
||||
}
|
||||
|
||||
function TabTrigger(props: {
|
||||
i18n: LocalizerType;
|
||||
pin: Pin;
|
||||
pinNumber: number;
|
||||
pinsCount: number;
|
||||
}) {
|
||||
const { i18n } = props;
|
||||
return (
|
||||
<Tabs.Trigger
|
||||
value={props.pin.id}
|
||||
aria-label={i18n('icu:PinnedMessagesBar__Tab__AccessibilityLabel', {
|
||||
pinNumber: props.pinNumber,
|
||||
})}
|
||||
className={tw(
|
||||
'group flex-1 px-[7px] outline-0',
|
||||
props.pinsCount === 3 ? 'py-[1px]' : 'py-0.5'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={tw(
|
||||
'block h-full w-0.5 rounded-full',
|
||||
'bg-label-disabled',
|
||||
'group-data-[state=active]:bg-label-primary',
|
||||
'outline-border-focused',
|
||||
'group-focused:outline-[2.5px]'
|
||||
)}
|
||||
/>
|
||||
</Tabs.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function Content(props: {
|
||||
i18n: LocalizerType;
|
||||
pin: Pin;
|
||||
onPinGoTo: (pinId: PinId) => void;
|
||||
onPinRemove: (pinId: PinId) => void;
|
||||
onPinsShowAll: () => void;
|
||||
}) {
|
||||
const { i18n, pin, onPinGoTo, onPinRemove, onPinsShowAll } = props;
|
||||
|
||||
const handlePinGoTo = useCallback(() => {
|
||||
onPinGoTo(pin.id);
|
||||
}, [onPinGoTo, pin.id]);
|
||||
|
||||
const handlePinRemove = useCallback(() => {
|
||||
onPinRemove(pin.id);
|
||||
}, [onPinRemove, pin.id]);
|
||||
|
||||
const handlePinsShowAll = useCallback(() => {
|
||||
onPinsShowAll();
|
||||
}, [onPinsShowAll]);
|
||||
|
||||
return (
|
||||
<div className={tw('flex min-w-0 flex-1 flex-row items-center')}>
|
||||
{props.pin.message.attachment != null && (
|
||||
<ImageThumbnail url={props.pin.message.attachment.url} />
|
||||
)}
|
||||
<div className={tw('min-w-0 flex-1')}>
|
||||
<h1 className={tw('type-body-small font-semibold text-label-primary')}>
|
||||
<UserText text={props.pin.sender.title} />
|
||||
</h1>
|
||||
<p className={tw('me-2 truncate type-body-medium text-label-primary')}>
|
||||
<UserText text={props.pin.message.body} />
|
||||
</p>
|
||||
<AriaClickable.HiddenTrigger
|
||||
aria-label={i18n(
|
||||
'icu:PinnedMessagesBar__GoToMessageClickableArea__AccessibilityLabel'
|
||||
)}
|
||||
onClick={handlePinGoTo}
|
||||
/>
|
||||
</div>
|
||||
<AriaClickable.SubWidget>
|
||||
<AxoDropdownMenu.Root>
|
||||
<AxoDropdownMenu.Trigger>
|
||||
<AxoIconButton.Root
|
||||
variant="borderless-secondary"
|
||||
size="md"
|
||||
symbol="pin"
|
||||
aria-label={i18n(
|
||||
'icu:PinnedMessagesBar__ActionsMenu__Button__AccessibilityLabel'
|
||||
)}
|
||||
/>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
<AxoDropdownMenu.Content>
|
||||
<AxoDropdownMenu.Item symbol="pin-slash" onSelect={handlePinRemove}>
|
||||
{i18n('icu:PinnedMessagesBar__ActionsMenu__UnpinMessage')}
|
||||
</AxoDropdownMenu.Item>
|
||||
<AxoDropdownMenu.Item
|
||||
symbol="message-arrow"
|
||||
onSelect={handlePinGoTo}
|
||||
>
|
||||
{i18n('icu:PinnedMessagesBar__ActionsMenu__GoToMessage')}
|
||||
</AxoDropdownMenu.Item>
|
||||
<AxoDropdownMenu.Item
|
||||
symbol="list-bullet"
|
||||
onSelect={handlePinsShowAll}
|
||||
>
|
||||
{i18n('icu:PinnedMessagesBar__ActionsMenu__SeeAllMessages')}
|
||||
</AxoDropdownMenu.Item>
|
||||
</AxoDropdownMenu.Content>
|
||||
</AxoDropdownMenu.Root>
|
||||
</AriaClickable.SubWidget>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ImageThumbnail(props: { url: string }) {
|
||||
return (
|
||||
<img
|
||||
alt=""
|
||||
src={props.url}
|
||||
className={tw('me-2 size-8 rounded-[10px] object-cover')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user