Add view-once media support for pinned messages bar

This commit is contained in:
Jamie
2026-01-07 14:28:24 -08:00
committed by GitHub
parent 66e46b750a
commit 3a2dafd2b3
4 changed files with 121 additions and 105 deletions

View File

@@ -1726,6 +1726,10 @@
"messageformat": "Sticker",
"description": "Conversation > With pinned message(s) > Pinned messages bar > Message Preview > Symbol Accessibility Label (Sticker)"
},
"icu:PinnedMessagesBar__MessagePreview__SymbolLabel--ViewOnceMedia": {
"messageformat": "View-once media",
"description": "Conversation > With pinned message(s) > Pinned messages bar > Message Preview > Symbol Accessibility Label (View-once media)"
},
"icu:PinnedMessagesBar__MessagePreview__Text--Photo": {
"messageformat": "Photo",
"description": "Conversation > With pinned message(s) > Pinned messages bar > Message Preview > Symbol Accessibility Label (Photo)"
@@ -1750,6 +1754,10 @@
"messageformat": "Sticker",
"description": "Conversation > With pinned message(s) > Pinned messages bar > Message Preview > Text (Sticker)"
},
"icu:PinnedMessagesBar__MessagePreview__Text--ViewOnceMedia": {
"messageformat": "View-once media",
"description": "Conversation > With pinned message(s) > Pinned messages bar > Message Preview > Text (View-once media)"
},
"icu:PinnedMessagesPanel__Title": {
"messageformat": "Pinned messages",
"description": "Conversation > Pinned messages panel (view all) > Title"

View File

@@ -16,6 +16,26 @@ export default {
title: 'Components/PinnedMessages/PinnedMessagesBar',
} satisfies Meta;
type MockPinMessageProps = Partial<
Omit<PinMessage, 'id' | 'sentAtTimestamp' | 'receivedAtCounter'>
>;
function mockPinMessage(id: number, props: MockPinMessageProps): PinMessage {
return {
id: `message-${id}`,
sentAtTimestamp: id,
receivedAtCounter: id,
text: null,
attachment: null,
contact: null,
payment: false,
poll: null,
sticker: false,
viewOnceMedia: false,
...props,
};
}
const PIN_1: Pin = {
id: 1 as PinnedMessageId,
sender: {
@@ -23,14 +43,11 @@ const PIN_1: Pin = {
title: 'Jamie',
isMe: true,
},
message: {
id: 'message-1',
sentAtTimestamp: 1,
receivedAtCounter: 1,
message: mockPinMessage(1, {
poll: {
question: 'What should we get for lunch?',
},
},
}),
};
const PIN_2: Pin = {
@@ -40,10 +57,7 @@ const PIN_2: Pin = {
title: 'Tyler',
isMe: false,
},
message: {
id: 'message-2',
sentAtTimestamp: 2,
receivedAtCounter: 2,
message: mockPinMessage(2, {
text: {
body: 'We found a cute pottery store close to Inokashira Park that were going to check out on Saturday. Anyone want to meet at the south exit at Kichijoji station at 1pm? Too early?',
bodyRanges: [
@@ -51,7 +65,7 @@ const PIN_2: Pin = {
{ start: 39, length: 15, style: BodyRange.Style.SPOILER },
],
},
},
}),
};
const PIN_3: Pin = {
@@ -61,10 +75,7 @@ const PIN_3: Pin = {
title: 'Adrian',
isMe: false,
},
message: {
id: 'message-3',
sentAtTimestamp: 3,
receivedAtCounter: 3,
message: mockPinMessage(3, {
text: {
body: 'Photo',
bodyRanges: [],
@@ -73,7 +84,7 @@ const PIN_3: Pin = {
type: 'image',
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
},
},
}),
};
function Template(props: {
@@ -113,10 +124,7 @@ export function Default(): React.JSX.Element {
);
}
function Variant(props: {
title: string;
message: Omit<PinMessage, 'id' | 'sentAtTimestamp' | 'receivedAtCounter'>;
}) {
function Variant(props: { title: string; message: MockPinMessageProps }) {
const pin: Pin = {
id: 1 as PinnedMessageId,
sender: {
@@ -124,12 +132,7 @@ function Variant(props: {
title: props.title,
isMe: true,
},
message: {
id: 'message-1',
sentAtTimestamp: 1,
receivedAtCounter: 1,
...props.message,
},
message: mockPinMessage(1, props.message),
};
return <Template defaultCurrent={pin.id} pins={[pin]} />;
}
@@ -182,6 +185,7 @@ export function Variants(): React.JSX.Element {
<Variant title="Sticker" message={{ sticker: true }} />
<Variant title="Contact" message={{ contact: { name: 'Tyler' } }} />
<Variant title="Payment" message={{ payment: true }} />
<Variant title="View-Once Media" message={{ viewOnceMedia: true }} />
</Stack>
);
}

View File

@@ -45,12 +45,13 @@ export type PinMessage = Readonly<{
id: string;
sentAtTimestamp: number;
receivedAtCounter: number;
text?: PinMessageText | null;
attachment?: PinMessageAttachment | null;
contact?: PinMessageContact | null;
payment?: boolean;
poll?: PinMessagePoll | null;
sticker?: boolean;
text: PinMessageText | null;
attachment: PinMessageAttachment | null;
contact: PinMessageContact | null;
payment: boolean;
poll: PinMessagePoll | null;
sticker: boolean;
viewOnceMedia: boolean;
}>;
export type PinSender = Readonly<{
@@ -336,6 +337,10 @@ const Content = forwardRef(function Content(
});
function getThumbnailUrl(message: PinMessage): string | null {
// Never render a thumbnail if its view-once media
if (message.viewOnceMedia) {
return null;
}
if (message.attachment == null) {
return null;
}
@@ -363,122 +368,116 @@ type PreviewIcon = Readonly<{
label: string;
}>;
function getMessagePreviewIcon(
i18n: LocalizerType,
message: PinMessage
): PreviewIcon | null {
type Preview = Readonly<{
icon: PreviewIcon | null;
text: ReactNode | null;
}>;
function getMessagePreview(i18n: LocalizerType, message: PinMessage): Preview {
// Note: The order of this function matters, once something higher-up assigns
// `icon` or `text` nothing else should override it.
let icon: PreviewIcon | null = null;
let text: ReactNode | null = null;
// 1. View-once media is highest priority, no other icons or text should be shown
if (message.viewOnceMedia) {
text ??= i18n('icu:PinnedMessagesBar__MessagePreview__Text--ViewOnceMedia');
icon ??= {
symbol: 'viewonce',
label: i18n(
'icu:PinnedMessagesBar__MessagePreview__SymbolLabel--ViewOnceMedia'
),
};
}
// 2. The message body should take priority over other generic text labels
if (message.text != null) {
text ??= <MessageTextPreview i18n={i18n} text={message.text} />;
}
// 3. And everything else...
if (message.attachment != null) {
if (message.attachment.type === 'voiceMessage') {
return {
if (message.attachment.type === 'image') {
text ??= i18n('icu:PinnedMessagesBar__MessagePreview__Text--Photo');
} else if (message.attachment.type === 'video') {
text ??= i18n('icu:PinnedMessagesBar__MessagePreview__Text--Video');
} else if (message.attachment.type === 'voiceMessage') {
text ??= i18n(
'icu:PinnedMessagesBar__MessagePreview__Text--VoiceMessage'
);
icon ??= {
symbol: 'audio',
label: i18n(
'icu:PinnedMessagesBar__MessagePreview__SymbolLabel--VoiceMessage'
),
};
}
if (message.attachment.type === 'gif') {
return {
} else if (message.attachment.type === 'gif') {
text ??= i18n('icu:PinnedMessagesBar__MessagePreview__Text--Gif');
icon ??= {
symbol: 'gif',
label: i18n('icu:PinnedMessagesBar__MessagePreview__SymbolLabel--Gif'),
};
}
if (message.attachment.type === 'file') {
return {
} else if (message.attachment.type === 'file') {
text ??= <UserText text={message.attachment.name ?? ''} />;
icon ??= {
symbol: 'file',
label: i18n('icu:PinnedMessagesBar__MessagePreview__SymbolLabel--File'),
};
} else {
throw missingCaseError(message.attachment);
}
}
if (message.contact != null) {
return {
} else if (message.contact != null) {
const name = message.contact.name ?? i18n('icu:unknownContact');
text ??= <UserText text={name} />;
icon ??= {
symbol: 'person-circle',
label: message.contact.name ?? i18n('icu:unknownContact'),
label: name,
};
}
if (message.payment) {
return {
} else if (message.payment) {
text ??= i18n('icu:PinnedMessagesBar__MessagePreview__Text--Payment');
icon ??= {
symbol: 'creditcard',
label: i18n(
'icu:PinnedMessagesBar__MessagePreview__SymbolLabel--Payment'
),
};
}
if (message.poll != null) {
return {
} else if (message.poll != null) {
text ??= <UserText text={message.poll.question} />;
icon ??= {
symbol: 'poll',
label: i18n('icu:PinnedMessagesBar__MessagePreview__SymbolLabel--Poll'),
};
}
if (message.sticker) {
return {
} else if (message.sticker) {
text ??= i18n('icu:PinnedMessagesBar__MessagePreview__Text--Sticker');
icon ??= {
symbol: 'sticker',
label: i18n(
'icu:PinnedMessagesBar__MessagePreview__SymbolLabel--Sticker'
),
};
}
return null;
}
function getMessagePreviewText(
i18n: LocalizerType,
message: PinMessage
): ReactNode {
if (message.text != null) {
return <MessageTextPreview i18n={i18n} text={message.text} />;
}
if (message.attachment != null) {
if (message.attachment.type === 'image') {
return i18n('icu:PinnedMessagesBar__MessagePreview__Text--Photo');
}
if (message.attachment.type === 'video') {
return i18n('icu:PinnedMessagesBar__MessagePreview__Text--Video');
}
if (message.attachment.type === 'voiceMessage') {
return i18n('icu:PinnedMessagesBar__MessagePreview__Text--VoiceMessage');
}
if (message.attachment.type === 'gif') {
return i18n('icu:PinnedMessagesBar__MessagePreview__Text--Gif');
}
if (message.attachment.type === 'file') {
return <UserText text={message.attachment.name ?? ''} />;
}
throw missingCaseError(message.attachment);
}
if (message.contact?.name != null) {
return <UserText text={message.contact.name} />;
}
if (message.payment != null) {
return i18n('icu:PinnedMessagesBar__MessagePreview__Text--Payment');
}
if (message.poll != null) {
return <UserText text={message.poll.question} />;
}
if (message.sticker != null) {
return i18n('icu:PinnedMessagesBar__MessagePreview__Text--Sticker');
}
return null;
return { icon, text };
}
function MessagePreview(props: { i18n: LocalizerType; message: PinMessage }) {
const { i18n, message } = props;
const icon = useMemo(() => {
return getMessagePreviewIcon(i18n, message);
}, [i18n, message]);
const text = useMemo(() => {
return getMessagePreviewText(i18n, message);
const preview = useMemo(() => {
return getMessagePreview(i18n, message);
}, [i18n, message]);
return (
<>
{icon != null && (
{preview.icon != null && (
<>
<AxoSymbol.InlineGlyph symbol={icon.symbol} label={null} />{' '}
<AxoSymbol.InlineGlyph
symbol={preview.icon.symbol}
label={null}
/>{' '}
</>
)}
{text}
{preview.text}
</>
);
}

View File

@@ -101,6 +101,10 @@ function isPinMessageSticker(props: MessagePropsType): boolean {
return props.isSticker ?? false;
}
function isPinMessageViewOnceMedia(props: MessagePropsType): boolean {
return props.isTapToView ?? false;
}
function getPinMessage(
props: MessagePropsType,
sentAtTimestamp: number,
@@ -116,6 +120,7 @@ function getPinMessage(
payment: isPinMessagePayment(props),
poll: getPinMessagePoll(props),
sticker: isPinMessageSticker(props),
viewOnceMedia: isPinMessageViewOnceMedia(props),
};
}