Adjust story replies for direct conversations

This commit is contained in:
Josh Perez 2022-05-10 15:02:21 -04:00 committed by GitHub
parent fa7b7fcd08
commit 0ca66d6e95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 490 additions and 131 deletions

View File

@ -7114,6 +7114,32 @@
"message": "Visit link",
"description": "Title for the link preview tooltip"
},
"Quote__story": {
"message": "Story",
"description": "Title for replies to stories"
},
"Quote__story-reaction": {
"message": "Reacted to a story from $name$",
"description": "Label for when a person reacts to a story",
"placeholders": {
"name": {
"content": "$1",
"example": "Charlie"
}
}
},
"Quote__story-reaction--yours": {
"message": "Reacted to your story",
"description": "Label for when a person reacts to your story"
},
"Quote__story-reaction--single": {
"message": "Reacted to a story",
"description": "Used whenever we can't find a user's first name"
},
"Quote__story-unavailable": {
"message": "No longer available",
"description": "Label for when a story is not found"
},
"WhatsNew__modal-title": {
"message": "What's New",
"description": "Title for the whats new modal"

View File

@ -62,6 +62,19 @@
transition: background 0.1s ease-out;
}
.module-message__quote-story-reaction-header {
@include font-subtitle;
margin-bottom: 6px;
.module-message__container-outgoing & {
color: $color-white-alpha-80;
}
.module-message__container-incoming & {
color: $color-gray-60;
}
}
.module-message--expired {
animation: module-message__shake 0.2s linear infinite;
}

View File

@ -30,6 +30,25 @@
box-shadow: 0px 0px 0px 2px $color-ultramarine;
}
}
&__reaction-emoji {
bottom: 5px;
position: absolute;
right: 25px;
z-index: $z-index-base;
img.emoji {
height: 24px;
width: 24px;
}
&--story-unavailable {
align-items: flex-end;
display: flex;
margin-left: 32px;
padding-bottom: 7px;
}
}
}
.module-quote--no-click {

View File

@ -3,7 +3,7 @@
.StoryReplyQuote {
&__primary {
min-height: 64px;
min-height: 52px;
}
&__icon-container {

View File

@ -152,8 +152,8 @@
&__quote {
&__container {
margin-top: 8px;
margin-bottom: 8px;
margin: 8px 0;
margin-right: 38px;
}
&--outgoing-ultramarine {

View File

@ -3,16 +3,24 @@
.TextAttachment {
max-height: 100%;
max-width: 100%;
display: flex;
justify-content: center;
align-items: center;
&__story {
align-items: center;
display: flex;
flex-direction: column;
height: 1280px;
justify-content: center;
overflow: hidden;
transform-origin: top center;
user-select: none;
height: 1280px;
max-height: 1280px;
max-width: 720px;
min-height: 1280px;
min-width: 720px;
width: 720px;
}

View File

@ -2869,8 +2869,9 @@ export async function startApp(): Promise<void> {
source: ReactionSource.FromSomeoneElse,
};
const reactionModel = Reactions.getSingleton().add(attributes);
// Note: We do not wait for completion here
Reactions.getSingleton().onReaction(reactionModel);
Reactions.getSingleton().onReaction(reactionModel, message);
confirm();
return Promise.resolve();
}
@ -3237,7 +3238,7 @@ export async function startApp(): Promise<void> {
};
const reactionModel = Reactions.getSingleton().add(attributes);
// Note: We do not wait for completion here
Reactions.getSingleton().onReaction(reactionModel);
Reactions.getSingleton().onReaction(reactionModel, message);
event.confirm();
return Promise.resolve();

View File

@ -670,7 +670,7 @@ export const StoryViewer = ({
/>
{hasReplyModal && canReply && (
<StoryViewsNRepliesModal
authorTitle={title}
authorTitle={firstName || title}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
isGroupStory={isGroupStory}

View File

@ -27,6 +27,7 @@ import { Tabs } from './Tabs';
import { Theme } from '../util/theme';
import { ThemeType } from '../types/Util';
import { getAvatarColor } from '../types/Colors';
import { getStoryReplyText } from '../util/getStoryReplyText';
type ViewType = Pick<
ConversationType,
@ -135,88 +136,88 @@ export const StoryViewsNRepliesModal = ({
if (!isMyStory) {
composerElement = (
<div className="StoryViewsNRepliesModal__compose-container">
<div className="StoryViewsNRepliesModal__composer">
{!isGroupStory && (
<Quote
authorTitle={authorTitle}
conversationColor="ultramarine"
i18n={i18n}
isFromMe={false}
isViewOnce={false}
moduleClassName="StoryViewsNRepliesModal__quote"
rawAttachment={storyPreviewAttachment}
referencedMessageNotFound={false}
text={i18n('message--getNotificationText--text-with-emoji', {
text: i18n('message--getNotificationText--photo'),
emoji: '📷',
})}
/>
)}
<CompositionInput
draftText={messageBodyText}
getPreferredBadge={getPreferredBadge}
<>
{!isGroupStory && (
<Quote
authorTitle={authorTitle}
conversationColor="ultramarine"
i18n={i18n}
inputApi={inputApiRef}
moduleClassName="StoryViewsNRepliesModal__input"
onEditorStateChange={messageText => {
setMessageBodyText(messageText);
}}
onPickEmoji={insertEmoji}
onSubmit={(...args) => {
inputApiRef.current?.reset();
onReply(...args);
}}
onTextTooLong={onTextTooLong}
placeholder={
isGroupStory
? i18n('StoryViewer__reply-group')
: i18n('StoryViewer__reply')
}
theme={ThemeType.dark}
>
<EmojiButton
className="StoryViewsNRepliesModal__emoji-button"
i18n={i18n}
onPickEmoji={insertEmoji}
onClose={focusComposer}
recentEmojis={recentEmojis}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
/>
</CompositionInput>
</div>
<button
aria-label={i18n('StoryViewsNRepliesModal__react')}
className="StoryViewsNRepliesModal__react"
onClick={() => {
setShowReactionPicker(!showReactionPicker);
}}
ref={setReferenceElement}
type="button"
/>
{showReactionPicker && (
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<ReactionPicker
i18n={i18n}
onClose={() => {
setShowReactionPicker(false);
}}
onPick={emoji => {
setShowReactionPicker(false);
onReact(emoji);
}}
onSetSkinTone={onSetSkinTone}
preferredReactionEmoji={preferredReactionEmoji}
renderEmojiPicker={renderEmojiPicker}
/>
</div>
isFromMe={false}
isStoryReply
isViewOnce={false}
moduleClassName="StoryViewsNRepliesModal__quote"
rawAttachment={storyPreviewAttachment}
referencedMessageNotFound={false}
text={getStoryReplyText(i18n, storyPreviewAttachment)}
/>
)}
</div>
<div className="StoryViewsNRepliesModal__compose-container">
<div className="StoryViewsNRepliesModal__composer">
<CompositionInput
draftText={messageBodyText}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
inputApi={inputApiRef}
moduleClassName="StoryViewsNRepliesModal__input"
onEditorStateChange={messageText => {
setMessageBodyText(messageText);
}}
onPickEmoji={insertEmoji}
onSubmit={(...args) => {
inputApiRef.current?.reset();
onReply(...args);
}}
onTextTooLong={onTextTooLong}
placeholder={
isGroupStory
? i18n('StoryViewer__reply-group')
: i18n('StoryViewer__reply')
}
theme={ThemeType.dark}
>
<EmojiButton
className="StoryViewsNRepliesModal__emoji-button"
i18n={i18n}
onPickEmoji={insertEmoji}
onClose={focusComposer}
recentEmojis={recentEmojis}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
/>
</CompositionInput>
</div>
<button
aria-label={i18n('StoryViewsNRepliesModal__react')}
className="StoryViewsNRepliesModal__react"
onClick={() => {
setShowReactionPicker(!showReactionPicker);
}}
ref={setReferenceElement}
type="button"
/>
{showReactionPicker && (
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<ReactionPicker
i18n={i18n}
onClose={() => {
setShowReactionPicker(false);
}}
onPick={emoji => {
setShowReactionPicker(false);
onReact(emoji);
}}
onSetSkinTone={onSetSkinTone}
preferredReactionEmoji={preferredReactionEmoji}
renderEmojiPicker={renderEmojiPicker}
/>
</div>
)}
</div>
</>
);
}

View File

@ -1540,15 +1540,53 @@ story.add('Story reply', () => {
const conversation = getDefaultConversation();
return renderThree({
...createProps({ text: 'Wow!' }),
...createProps({ direction: 'outgoing', text: 'Wow!' }),
storyReplyContext: {
authorTitle: conversation.title,
authorTitle: conversation.firstName || conversation.title,
conversationColor: ConversationColors[0],
isFromMe: false,
rawAttachment: fakeAttachment({
url: '/fixtures/snow.jpg',
thumbnail: fakeThumbnail('/fixtures/snow.jpg'),
}),
text: 'Photo',
},
});
});
story.add('Story reply (yours)', () => {
const conversation = getDefaultConversation();
return renderThree({
...createProps({ direction: 'incoming', text: 'Wow!' }),
storyReplyContext: {
authorTitle: conversation.firstName || conversation.title,
conversationColor: ConversationColors[0],
isFromMe: true,
rawAttachment: fakeAttachment({
url: '/fixtures/snow.jpg',
thumbnail: fakeThumbnail('/fixtures/snow.jpg'),
}),
text: 'Photo',
},
});
});
story.add('Story reply (emoji)', () => {
const conversation = getDefaultConversation();
return renderThree({
...createProps({ direction: 'outgoing', text: 'Wow!' }),
storyReplyContext: {
authorTitle: conversation.firstName || conversation.title,
conversationColor: ConversationColors[0],
emoji: '💄',
isFromMe: false,
rawAttachment: fakeAttachment({
url: '/fixtures/snow.jpg',
thumbnail: fakeThumbnail('/fixtures/snow.jpg'),
}),
text: 'Photo',
},
});
});

View File

@ -227,8 +227,11 @@ export type PropsData = {
authorTitle: string;
conversationColor: ConversationColorType;
customColor?: CustomColorType;
emoji?: string;
isFromMe: boolean;
rawAttachment?: QuotedAttachmentType;
referencedMessageNotFound?: boolean;
text: string;
};
previews: Array<LinkPreviewType>;
@ -1299,27 +1302,35 @@ export class Message extends React.PureComponent<Props, State> {
}
return (
<Quote
authorTitle={storyReplyContext.authorTitle}
conversationColor={conversationColor}
curveTopLeft={curveTopLeft}
curveTopRight={curveTopRight}
customColor={customColor}
i18n={i18n}
isFromMe={storyReplyContext.isFromMe}
isIncoming={isIncoming}
isViewOnce={false}
moduleClassName="StoryReplyQuote"
onClick={() => {
// TODO DESKTOP-3255
}}
rawAttachment={storyReplyContext.rawAttachment}
referencedMessageNotFound={false}
text={i18n('message--getNotificationText--text-with-emoji', {
text: i18n('message--getNotificationText--photo'),
emoji: '📷',
})}
/>
<>
{storyReplyContext.emoji && (
<div className="module-message__quote-story-reaction-header">
{i18n('Quote__story-reaction', [storyReplyContext.authorTitle])}
</div>
)}
<Quote
authorTitle={storyReplyContext.authorTitle}
conversationColor={conversationColor}
curveTopLeft={curveTopLeft}
curveTopRight={curveTopRight}
customColor={customColor}
i18n={i18n}
isFromMe={storyReplyContext.isFromMe}
isIncoming={isIncoming}
isStoryReply
isViewOnce={false}
moduleClassName="StoryReplyQuote"
onClick={() => {
// TODO DESKTOP-3255
}}
rawAttachment={storyReplyContext.rawAttachment}
reactionEmoji={storyReplyContext.emoji}
referencedMessageNotFound={Boolean(
storyReplyContext.referencedMessageNotFound
)}
text={storyReplyContext.text}
/>
</>
);
}

View File

@ -521,3 +521,51 @@ story.add('Custom Color', () => (
/>
</>
));
story.add('isStoryReply', () => {
const props = createProps({
text: 'Wow!',
});
return (
<Quote
{...props}
authorTitle="Amanda"
isStoryReply
moduleClassName="StoryReplyQuote"
onClose={undefined}
rawAttachment={{
contentType: VIDEO_MP4,
fileName: 'great-video.mp4',
isVoiceMessage: false,
}}
/>
);
});
story.add('isStoryReply emoji', () => {
const props = createProps();
return (
<Quote
{...props}
authorTitle="Charlie"
isStoryReply
moduleClassName="StoryReplyQuote"
onClose={undefined}
rawAttachment={{
contentType: IMAGE_PNG,
fileName: 'sax.png',
isVoiceMessage: false,
thumbnail: {
contentType: IMAGE_PNG,
height: 100,
width: 100,
path: pngUrl,
objectUrl: pngUrl,
},
}}
reactionEmoji="🏋️"
/>
);
});

View File

@ -17,6 +17,8 @@ import type {
CustomColorType,
} from '../../types/Colors';
import { ContactName } from './ContactName';
import { Emojify } from './Emojify';
import { TextAttachment } from '../TextAttachment';
import { getTextWithMentions } from '../../util/getTextWithMentions';
import { getClassNamesFor } from '../../util/getClassNamesFor';
import { getCustomColorStyle } from '../../util/getCustomColorStyle';
@ -31,12 +33,14 @@ export type Props = {
i18n: LocalizerType;
isFromMe: boolean;
isIncoming?: boolean;
isStoryReply?: boolean;
moduleClassName?: string;
onClick?: () => void;
onClose?: () => void;
text: string;
rawAttachment?: QuotedAttachmentType;
isViewOnce: boolean;
reactionEmoji?: string;
referencedMessageNotFound: boolean;
doubleCheckMissingQuoteReference?: () => unknown;
};
@ -51,6 +55,13 @@ export type QuotedAttachmentType = Pick<
>;
function validateQuote(quote: Props): boolean {
if (
quote.isStoryReply &&
(quote.referencedMessageNotFound || quote.reactionEmoji)
) {
return true;
}
if (quote.text) {
return true;
}
@ -250,7 +261,7 @@ export class Quote extends React.Component<Props, State> {
}
public renderIconContainer(): JSX.Element | null {
const { rawAttachment, isViewOnce } = this.props;
const { rawAttachment, isViewOnce, i18n } = this.props;
const { imageBroken } = this.state;
const attachment = getAttachment(rawAttachment);
@ -265,9 +276,16 @@ export class Quote extends React.Component<Props, State> {
return this.renderIcon('view-once');
}
// TODO DESKTOP-3433
if (textAttachment) {
return this.renderIcon('image');
return (
<div className={this.getClassName('__icon-container')}>
<TextAttachment
i18n={i18n}
isThumbnail
textAttachment={textAttachment}
/>
</div>
);
}
if (GoogleChrome.isVideoTypeSupported(contentType)) {
@ -385,7 +403,17 @@ export class Quote extends React.Component<Props, State> {
}
public renderAuthor(): JSX.Element {
const { authorTitle, i18n, isFromMe, isIncoming } = this.props;
const { authorTitle, i18n, isFromMe, isIncoming, isStoryReply } =
this.props;
const title = isFromMe ? i18n('you') : <ContactName title={authorTitle} />;
const author = isStoryReply ? (
<>
{title} &middot; {i18n('Quote__story')}
</>
) : (
title
);
return (
<div
@ -394,7 +422,7 @@ export class Quote extends React.Component<Props, State> {
isIncoming ? this.getClassName('__primary__author--incoming') : null
)}
>
{isFromMe ? i18n('you') : <ContactName title={authorTitle} />}
{author}
</div>
);
}
@ -405,10 +433,11 @@ export class Quote extends React.Component<Props, State> {
customColor,
i18n,
isIncoming,
isStoryReply,
referencedMessageNotFound,
} = this.props;
if (!referencedMessageNotFound) {
if (!referencedMessageNotFound || isStoryReply) {
return null;
}
@ -452,6 +481,8 @@ export class Quote extends React.Component<Props, State> {
customColor,
isIncoming,
onClick,
rawAttachment,
reactionEmoji,
referencedMessageNotFound,
} = this.props;
@ -486,6 +517,17 @@ export class Quote extends React.Component<Props, State> {
{this.renderGenericFile()}
{this.renderText()}
</div>
{reactionEmoji && (
<div
className={
rawAttachment
? this.getClassName('__reaction-emoji')
: this.getClassName('__reaction-emoji--story-unavailable')
}
>
<Emojify text={reactionEmoji} />
</div>
)}
{this.renderIconContainer()}
{this.renderClose()}
</button>

View File

@ -26,6 +26,7 @@ import { canReact, isStory } from '../../state/selectors/message';
import { findAndFormatContact } from '../../util/findAndFormatContact';
import { UUID } from '../../types/UUID';
import { handleMultipleSendErrors } from './handleMultipleSendErrors';
import { incrementMessageCounter } from '../../util/incrementMessageCounter';
import type {
ConversationQueueJobBundle,
@ -138,9 +139,8 @@ export async function sendReaction(
type: 'outgoing',
conversationId: conversation.get('id'),
sent_at: pendingReaction.timestamp,
received_at: window.Signal.Util.incrementMessageCounter(),
received_at: incrementMessageCounter(),
received_at_ms: pendingReaction.timestamp,
reaction: reactionForSend,
timestamp: pendingReaction.timestamp,
sendStateByConversationId: zipObject(
Object.keys(pendingReaction.isSentByConversationId || {}),
@ -150,7 +150,18 @@ export async function sendReaction(
})
),
});
ephemeralMessageForReactionSend.doNotSave = true;
if (
isStory(message.attributes) &&
isDirectConversation(conversation.attributes)
) {
ephemeralMessageForReactionSend.set({
storyId: message.id,
storyReactionEmoji: reactionForSend.emoji,
});
} else {
ephemeralMessageForReactionSend.doNotSave = true;
}
let didFullySend: boolean;
const successfulConversationIds = new Set<string>();
@ -308,6 +319,22 @@ export async function sendReaction(
didFullySend = false;
}
}
if (!ephemeralMessageForReactionSend.doNotSave) {
const reactionMessage = ephemeralMessageForReactionSend;
await Promise.all([
await window.Signal.Data.saveMessage(reactionMessage.attributes, {
ourUuid,
forceSave: true,
}),
reactionMessage.hydrateStoryContext(message),
]);
conversation.addSingleMessage(
window.MessageController.register(reactionMessage.id, reactionMessage)
);
}
}
const newReactions = reactionUtil.markOutgoingReactionSent(

View File

@ -4,11 +4,12 @@
/* eslint-disable max-classes-per-file */
import { Collection, Model } from 'backbone';
import type { MessageModel } from '../models/messages';
import { getContactId, getContact } from '../messages/helpers';
import { isOutgoing } from '../state/selectors/message';
import type { ReactionAttributesType } from '../model-types.d';
import * as log from '../logging/log';
import type { MessageModel } from '../models/messages';
import type { ReactionAttributesType } from '../model-types.d';
import { getContactId, getContact } from '../messages/helpers';
import { isDirectConversation } from '../util/whatTypeOfConversation';
import { isOutgoing, isStory } from '../state/selectors/message';
export class ReactionModel extends Model<ReactionAttributesType> {}
@ -55,7 +56,10 @@ export class Reactions extends Collection<ReactionModel> {
return [];
}
async onReaction(reaction: ReactionModel): Promise<void> {
async onReaction(
reaction: ReactionModel,
generatedMessage: MessageModel
): Promise<void> {
try {
// The conversation the target message was in; we have to find it in the database
// to to figure that out.
@ -133,6 +137,35 @@ export class Reactions extends Collection<ReactionModel> {
targetMessage
);
// Use the generated message in ts/background.ts to create a message
// if the reaction is targetted at a story on a 1:1 conversation.
if (
isStory(targetMessage) &&
isDirectConversation(targetConversation.attributes)
) {
generatedMessage.set({
storyId: targetMessage.id,
storyReactionEmoji: reaction.get('emoji'),
});
await Promise.all([
window.Signal.Data.saveMessage(generatedMessage.attributes, {
ourUuid: window.textsecure.storage.user
.getCheckedUuid()
.toString(),
forceSave: true,
}),
generatedMessage.hydrateStoryContext(message),
]);
targetConversation.addSingleMessage(
window.MessageController.register(
generatedMessage.id,
generatedMessage
)
);
}
await message.handleReaction(reaction);
this.remove(reaction);

2
ts/model-types.d.ts vendored
View File

@ -186,7 +186,7 @@ export type MessageAttributesType = {
unidentifiedDeliveries?: Array<string>;
contact?: Array<EmbeddedContactType>;
conversationId: string;
reaction?: WhatIsThis;
storyReactionEmoji?: string;
expirationTimerUpdate?: {
expireTimer: number;

View File

@ -306,7 +306,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
);
}
async hydrateStoryContext(): Promise<void> {
async hydrateStoryContext(inMemoryMessage?: MessageModel): Promise<void> {
const storyId = this.get('storyId');
if (!storyId) {
return;
@ -316,9 +316,24 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return;
}
const message = await getMessageById(storyId);
const message = inMemoryMessage || (await getMessageById(storyId));
if (!message) {
const conversation = this.getConversation();
strictAssert(
conversation && isDirectConversation(conversation.attributes),
'Hydrating story context on a non-private conversation'
);
this.set({
storyReplyContext: {
attachment: undefined,
// This is ok to do because story replies only show in 1:1 conversations
// so the story that was quoted should be from the same conversation.
authorUuid: conversation.get('uuid'),
// No messageId, referenced story not found!
messageId: '',
},
});
return;
}
@ -772,6 +787,21 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const { text, emoji } = this.getNotificationData();
const { attributes } = this;
if (attributes.storyReactionEmoji) {
const conversation = this.getConversation();
const firstName = conversation?.attributes.profileName;
if (!conversation || !firstName) {
return window.i18n('Quote__story-reaction--single');
}
if (isMe(conversation.attributes)) {
return window.i18n('Quote__story-reaction--yours');
}
return window.i18n('Quote__story-reaction', [firstName]);
}
let modifiedText = text;
const bodyRanges = processBodyRanges(attributes, {
@ -936,6 +966,25 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
async doubleCheckMissingQuoteReference(): Promise<void> {
const logId = this.idForLogging();
const storyId = this.get('storyId');
if (storyId) {
log.warn(
`doubleCheckMissingQuoteReference/${logId}: missing story reference`
);
const message = window.MessageController.getById(storyId);
if (!message) {
return;
}
if (this.get('storyReplyContext')) {
this.unset('storyReplyContext');
}
await this.hydrateStoryContext(message);
return;
}
const quote = this.get('quote');
if (!quote) {
log.warn(`doubleCheckMissingQuoteReference/${logId}: Missing quote!`);

View File

@ -76,6 +76,7 @@ import {
import * as log from '../../logging/log';
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
import { DAY, HOUR } from '../../util/durations';
import { getStoryReplyText } from '../../util/getStoryReplyText';
const THREE_HOURS = 3 * HOUR;
@ -435,7 +436,7 @@ export const getPropsForStoryReplyContext = createSelectorCreator(
(
message: Pick<
MessageWithUIFieldsType,
'body' | 'conversationId' | 'storyReplyContext'
'body' | 'conversationId' | 'storyReactionEmoji' | 'storyReplyContext'
>,
{
conversationSelector,
@ -445,14 +446,14 @@ export const getPropsForStoryReplyContext = createSelectorCreator(
ourConversationId?: string;
}
): PropsData['storyReplyContext'] => {
const { storyReplyContext } = message;
const { storyReactionEmoji, storyReplyContext } = message;
if (!storyReplyContext) {
return undefined;
}
const contact = conversationSelector(storyReplyContext.authorUuid);
const authorTitle = contact.title;
const authorTitle = contact.firstName || contact.title;
const isFromMe = contact.id === ourConversationId;
const conversation = getConversation(message, conversationSelector);
@ -464,10 +465,13 @@ export const getPropsForStoryReplyContext = createSelectorCreator(
authorTitle,
conversationColor,
customColor,
emoji: storyReactionEmoji,
isFromMe,
rawAttachment: storyReplyContext.attachment
? processQuoteAttachment(storyReplyContext.attachment)
: undefined,
referencedMessageNotFound: !storyReplyContext.messageId,
text: getStoryReplyText(window.i18n, storyReplyContext.attachment),
};
},

View File

@ -0,0 +1,39 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AttachmentType } from '../types/Attachment';
import type { LocalizerType } from '../types/Util';
import { isGIF, isImage, isVideo } from '../types/Attachment';
export function getStoryReplyText(
i18n: LocalizerType,
attachment?: AttachmentType
): string {
if (!attachment) {
return i18n('Quote__story-unavailable');
}
if (attachment.caption) {
return attachment.caption;
}
const attachments = [attachment];
if (isImage(attachments)) {
return i18n('message--getNotificationText--photo');
}
if (isGIF(attachments)) {
return i18n('message--getNotificationText--gif');
}
if (isVideo(attachments)) {
return i18n('message--getNotificationText--video');
}
if (attachment.textAttachment && attachment.textAttachment.text) {
return attachment.textAttachment.text;
}
return i18n('message--getNotificationText--file');
}