Add pinned message notifications

This commit is contained in:
Jamie
2025-12-16 07:47:46 -08:00
committed by GitHub
parent 5ec3f763cd
commit 0a12e1ec17
6 changed files with 59 additions and 18 deletions

View File

@@ -3408,6 +3408,14 @@
"messageformat": "You donated for {recipient}", "messageformat": "You donated for {recipient}",
"description": "Shown to label a donation badge you've sent in notifications and the left pane" "description": "Shown to label a donation badge you've sent in notifications and the left pane"
}, },
"icu:message--pinned--preview--received": {
"messageformat": "{sender} pinned a message",
"description": "Shown to label pinned message notification in notifications and the left pane."
},
"icu:message--pinned--preview--sent": {
"messageformat": "You pinned a message",
"description": "Shown to label pinned message notification in notifications and the left pane."
},
"icu:message--donation": { "icu:message--donation": {
"messageformat": "Donation", "messageformat": "Donation",
"description": "Shown to label the donation badge you've redeemed on another device." "description": "Shown to label the donation badge you've redeemed on another device."

View File

@@ -232,26 +232,27 @@ function TabTrigger(props: {
); );
} }
const Content = forwardRef(function Content( type ContentProps = Readonly<{
props: {
i18n: LocalizerType; i18n: LocalizerType;
pin: Pin; pin: Pin;
onPinGoTo: (messageId: string) => void; onPinGoTo: (messageId: string) => void;
onPinRemove: (messageId: string) => void; onPinRemove: (messageId: string) => void;
onPinsShowAll: () => void; onPinsShowAll: () => void;
canPinMessages: boolean; canPinMessages: boolean;
}, }>;
ref: ForwardedRef<HTMLDivElement>
): JSX.Element { const Content = forwardRef(function Content(
const { {
i18n, i18n,
pin, pin,
onPinGoTo, onPinGoTo,
onPinRemove, onPinRemove,
onPinsShowAll, onPinsShowAll,
canPinMessages,
...forwardedProps ...forwardedProps
} = props; }: ContentProps,
ref: ForwardedRef<HTMLDivElement>
): JSX.Element {
const handlePinGoTo = useCallback(() => { const handlePinGoTo = useCallback(() => {
onPinGoTo(pin.message.id); onPinGoTo(pin.message.id);
}, [onPinGoTo, pin.message.id]); }, [onPinGoTo, pin.message.id]);
@@ -277,10 +278,10 @@ const Content = forwardRef(function Content(
{thumbnailUrl != null && <ImageThumbnail url={thumbnailUrl} />} {thumbnailUrl != null && <ImageThumbnail url={thumbnailUrl} />}
<div className={tw('min-w-0 flex-1')}> <div className={tw('min-w-0 flex-1')}>
<h1 className={tw('type-body-small font-semibold text-label-primary')}> <h1 className={tw('type-body-small font-semibold text-label-primary')}>
<UserText text={props.pin.sender.title} /> <UserText text={pin.sender.title} />
</h1> </h1>
<p className={tw('me-2 truncate type-body-medium text-label-primary')}> <p className={tw('me-2 truncate type-body-medium text-label-primary')}>
<MessagePreview i18n={i18n} message={props.pin.message} /> <MessagePreview i18n={i18n} message={pin.message} />
</p> </p>
<AriaClickable.HiddenTrigger <AriaClickable.HiddenTrigger
aria-label={i18n( aria-label={i18n(
@@ -302,7 +303,7 @@ const Content = forwardRef(function Content(
/> />
</AxoDropdownMenu.Trigger> </AxoDropdownMenu.Trigger>
<AxoDropdownMenu.Content> <AxoDropdownMenu.Content>
{props.canPinMessages && ( {canPinMessages && (
<AxoDropdownMenu.Item <AxoDropdownMenu.Item
symbol="pin-slash" symbol="pin-slash"
onSelect={handlePinRemove} onSelect={handlePinRemove}

View File

@@ -93,6 +93,13 @@ export async function onPinnedMessageAdd(
} }
} }
if (result.change?.inserted) {
await targetConversation.addNotification('pinned-message-notification', {
pinnedMessageId: targetMessage.id,
sourceServiceId: props.pinnedByAci,
});
}
window.reduxActions.pinnedMessages.onPinnedMessagesChanged( window.reduxActions.pinnedMessages.onPinnedMessagesChanged(
targetConversation.id targetConversation.id
); );

View File

@@ -1933,7 +1933,8 @@ export class BackupExportStream extends Readable {
} }
if (isPinnedMessageNotification(message)) { if (isPinnedMessageNotification(message)) {
throw new Error('unimplemented'); // TODO(jamie): Implement backups for pinned messages
return { kind: NonBubbleResultKind.Drop };
} }
if (isProfileChange(message)) { if (isProfileChange(message)) {

View File

@@ -134,6 +134,11 @@ function onPinnedMessageAdd(
): StateThunk { ): StateThunk {
return async dispatch => { return async dispatch => {
const target = await getPinnedMessageTarget(targetMessageId); const target = await getPinnedMessageTarget(targetMessageId);
const targetConversation = window.ConversationController.get(
target.conversationId
);
strictAssert(targetConversation != null, 'Missing target conversation');
await conversationJobQueue.add({ await conversationJobQueue.add({
type: conversationQueueJobEnum.enum.PinMessage, type: conversationQueueJobEnum.enum.PinMessage,
...target, ...target,
@@ -152,6 +157,11 @@ function onPinnedMessageAdd(
pinnedAt, pinnedAt,
}); });
await targetConversation.addNotification('pinned-message-notification', {
pinnedMessageId: targetMessageId,
sourceServiceId: itemStorage.user.getCheckedAci(),
});
dispatch(onPinnedMessagesChanged(target.conversationId)); dispatch(onPinnedMessagesChanged(target.conversationId));
}; };
} }

View File

@@ -512,7 +512,21 @@ export function getNotificationDataForMessage(
} }
if (isPinnedMessageNotification(attributes)) { if (isPinnedMessageNotification(attributes)) {
throw new Error('unimplemented'); const fromContact = getAuthor(attributes);
const ourAci = itemStorage.user.getCheckedAci();
let text: string;
if (fromContact?.getAci() === ourAci) {
text = i18n('icu:message--pinned--preview--sent');
} else {
const sender = fromContact?.getTitle() ?? i18n('icu:unknownContact');
text = i18n('icu:message--pinned--preview--received', { sender });
}
return {
emoji: '📌',
text,
};
} }
const { poll } = attributes; const { poll } = attributes;