Use proxy-compare for message bubbles
This commit is contained in:
parent
f92f81dfd6
commit
55a1c5f6c5
11 changed files with 442 additions and 562 deletions
|
@ -1,18 +1,8 @@
|
|||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import {
|
||||
groupBy,
|
||||
identity,
|
||||
isEmpty,
|
||||
isEqual,
|
||||
isNumber,
|
||||
isObject,
|
||||
map,
|
||||
omit,
|
||||
pick,
|
||||
} from 'lodash';
|
||||
import { createSelector, createSelectorCreator } from 'reselect';
|
||||
import { groupBy, isEmpty, isNumber, isObject, map, omit } from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import filesize from 'filesize';
|
||||
import getDirection from 'direction';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
|
@ -66,7 +56,7 @@ import { isVoiceMessage, canBeDownloaded } from '../../types/Attachment';
|
|||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
|
||||
import type { CallingNotificationType } from '../../util/callingNotification';
|
||||
import { memoizeByRoot } from '../../util/memoizeByRoot';
|
||||
import { proxyMemoize } from '../../util/proxyMemoize';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { getRecipients } from '../../util/getRecipients';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
|
@ -280,13 +270,11 @@ export function getConversation(
|
|||
|
||||
// Message
|
||||
|
||||
export const getAttachmentsForMessage = createSelectorCreator(memoizeByRoot)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
|
||||
({ sticker }: MessageWithUIFieldsType) => sticker,
|
||||
({ attachments }: MessageWithUIFieldsType) => attachments,
|
||||
(_, sticker, attachments = []): Array<AttachmentType> => {
|
||||
export const getAttachmentsForMessage = proxyMemoize(
|
||||
({
|
||||
sticker,
|
||||
attachments = [],
|
||||
}: MessageWithUIFieldsType): Array<AttachmentType> => {
|
||||
if (sticker && sticker.data) {
|
||||
const { data } = sticker;
|
||||
|
||||
|
@ -311,16 +299,16 @@ export const getAttachmentsForMessage = createSelectorCreator(memoizeByRoot)(
|
|||
.filter(attachment => !attachment.error || canBeDownloaded(attachment))
|
||||
.map(attachment => getPropsForAttachment(attachment))
|
||||
.filter(isNotNil);
|
||||
},
|
||||
{
|
||||
name: 'getAttachmentsForMessage',
|
||||
}
|
||||
);
|
||||
|
||||
export const processBodyRanges = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
|
||||
export const processBodyRanges = proxyMemoize(
|
||||
(
|
||||
{ bodyRanges }: Pick<MessageWithUIFieldsType, 'bodyRanges'>,
|
||||
{ conversationSelector }: { conversationSelector: GetConversationByIdType }
|
||||
options: { conversationSelector: GetConversationByIdType }
|
||||
): HydratedBodyRangesType | undefined => {
|
||||
if (!bodyRanges) {
|
||||
return undefined;
|
||||
|
@ -329,6 +317,7 @@ export const processBodyRanges = createSelectorCreator(memoizeByRoot, isEqual)(
|
|||
return bodyRanges
|
||||
.filter(range => range.mentionUuid)
|
||||
.map(range => {
|
||||
const { conversationSelector } = options;
|
||||
const conversation = conversationSelector(range.mentionUuid);
|
||||
|
||||
return {
|
||||
|
@ -339,16 +328,83 @@ export const processBodyRanges = createSelectorCreator(memoizeByRoot, isEqual)(
|
|||
})
|
||||
.sort((a, b) => b.start - a.start);
|
||||
},
|
||||
(_, ranges): undefined | HydratedBodyRangesType => ranges
|
||||
{
|
||||
name: 'processBodyRanges',
|
||||
}
|
||||
);
|
||||
|
||||
const getAuthorForMessage = createSelectorCreator(memoizeByRoot)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
const getAuthorForMessage = (
|
||||
message: MessageWithUIFieldsType,
|
||||
options: GetContactOptions
|
||||
): PropsData['author'] => {
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
badges,
|
||||
color,
|
||||
id,
|
||||
isMe,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
} = getContact(message, options);
|
||||
|
||||
getContact,
|
||||
const unsafe = {
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
badges,
|
||||
color,
|
||||
id,
|
||||
isMe,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
};
|
||||
|
||||
const safe: AssertProps<PropsData['author'], typeof unsafe> = unsafe;
|
||||
|
||||
return safe;
|
||||
};
|
||||
|
||||
const getPreviewsForMessage = ({
|
||||
preview: previews = [],
|
||||
}: MessageWithUIFieldsType): Array<LinkPreviewType> => {
|
||||
return previews.map(preview => ({
|
||||
...preview,
|
||||
isStickerPack: isStickerPack(preview.url),
|
||||
domain: getDomain(preview.url),
|
||||
image: preview.image ? getPropsForAttachment(preview.image) : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
const getReactionsForMessage = (
|
||||
{ reactions = [] }: MessageWithUIFieldsType,
|
||||
{ conversationSelector }: { conversationSelector: GetConversationByIdType }
|
||||
) => {
|
||||
const reactionBySender = new Map<string, MessageReactionType>();
|
||||
for (const reaction of reactions) {
|
||||
const existingReaction = reactionBySender.get(reaction.fromId);
|
||||
if (!existingReaction || reaction.timestamp > existingReaction.timestamp) {
|
||||
reactionBySender.set(reaction.fromId, reaction);
|
||||
}
|
||||
}
|
||||
|
||||
const reactionsWithEmpties = reactionBySender.values();
|
||||
const reactionsWithEmoji = iterables.filter(
|
||||
reactionsWithEmpties,
|
||||
re => re.emoji
|
||||
);
|
||||
const formattedReactions = iterables.map(reactionsWithEmoji, re => {
|
||||
const c = conversationSelector(re.fromId);
|
||||
|
||||
type From = NonNullable<PropsData['reactions']>[0]['from'];
|
||||
|
||||
(_, convo: ConversationType): PropsData['author'] => {
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
|
@ -361,8 +417,7 @@ const getAuthorForMessage = createSelectorCreator(memoizeByRoot)(
|
|||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
} = convo;
|
||||
} = c;
|
||||
|
||||
const unsafe = {
|
||||
acceptedMessageRequest,
|
||||
|
@ -376,155 +431,65 @@ const getAuthorForMessage = createSelectorCreator(memoizeByRoot)(
|
|||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
};
|
||||
|
||||
const safe: AssertProps<PropsData['author'], typeof unsafe> = unsafe;
|
||||
const from: AssertProps<From, typeof unsafe> = unsafe;
|
||||
|
||||
return safe;
|
||||
}
|
||||
);
|
||||
|
||||
const getCachedAuthorForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
getAuthorForMessage,
|
||||
(_, author): PropsData['author'] => author
|
||||
);
|
||||
|
||||
export const getPreviewsForMessage = createSelectorCreator(memoizeByRoot)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
({ preview }: MessageWithUIFieldsType) => preview,
|
||||
(_, previews = []): Array<LinkPreviewType> => {
|
||||
return previews.map(preview => ({
|
||||
...preview,
|
||||
isStickerPack: isStickerPack(preview.url),
|
||||
domain: getDomain(preview.url),
|
||||
image: preview.image ? getPropsForAttachment(preview.image) : undefined,
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
export const getReactionsForMessage = createSelectorCreator(
|
||||
memoizeByRoot,
|
||||
isEqual
|
||||
)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
|
||||
(
|
||||
{ reactions = [] }: MessageWithUIFieldsType,
|
||||
{ conversationSelector }: { conversationSelector: GetConversationByIdType }
|
||||
) => {
|
||||
const reactionBySender = new Map<string, MessageReactionType>();
|
||||
for (const reaction of reactions) {
|
||||
const existingReaction = reactionBySender.get(reaction.fromId);
|
||||
if (
|
||||
!existingReaction ||
|
||||
reaction.timestamp > existingReaction.timestamp
|
||||
) {
|
||||
reactionBySender.set(reaction.fromId, reaction);
|
||||
}
|
||||
}
|
||||
|
||||
const reactionsWithEmpties = reactionBySender.values();
|
||||
const reactionsWithEmoji = iterables.filter(
|
||||
reactionsWithEmpties,
|
||||
re => re.emoji
|
||||
);
|
||||
const formattedReactions = iterables.map(reactionsWithEmoji, re => {
|
||||
const c = conversationSelector(re.fromId);
|
||||
|
||||
type From = NonNullable<PropsData['reactions']>[0]['from'];
|
||||
|
||||
const unsafe = pick(c, [
|
||||
'acceptedMessageRequest',
|
||||
'avatarPath',
|
||||
'badges',
|
||||
'color',
|
||||
'id',
|
||||
'isMe',
|
||||
'name',
|
||||
'phoneNumber',
|
||||
'profileName',
|
||||
'sharedGroupNames',
|
||||
'title',
|
||||
]);
|
||||
|
||||
const from: AssertProps<From, typeof unsafe> = unsafe;
|
||||
|
||||
strictAssert(re.emoji, 'Expected all reactions to have an emoji');
|
||||
|
||||
return {
|
||||
emoji: re.emoji,
|
||||
timestamp: re.timestamp,
|
||||
from,
|
||||
};
|
||||
});
|
||||
|
||||
return [...formattedReactions];
|
||||
},
|
||||
|
||||
(_, reactions): PropsData['reactions'] => reactions
|
||||
);
|
||||
|
||||
export const getPropsForStoryReplyContext = createSelectorCreator(
|
||||
memoizeByRoot,
|
||||
isEqual
|
||||
)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
|
||||
(
|
||||
message: Pick<
|
||||
MessageWithUIFieldsType,
|
||||
'body' | 'conversationId' | 'storyReaction' | 'storyReplyContext'
|
||||
>,
|
||||
{
|
||||
conversationSelector,
|
||||
ourConversationId,
|
||||
}: {
|
||||
conversationSelector: GetConversationByIdType;
|
||||
ourConversationId?: string;
|
||||
}
|
||||
): PropsData['storyReplyContext'] => {
|
||||
const { storyReaction, storyReplyContext } = message;
|
||||
if (!storyReplyContext) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const contact = conversationSelector(storyReplyContext.authorUuid);
|
||||
|
||||
const authorTitle = contact.firstName || contact.title;
|
||||
const isFromMe = contact.id === ourConversationId;
|
||||
|
||||
const conversation = getConversation(message, conversationSelector);
|
||||
|
||||
const { conversationColor, customColor } =
|
||||
getConversationColorAttributes(conversation);
|
||||
strictAssert(re.emoji, 'Expected all reactions to have an emoji');
|
||||
|
||||
return {
|
||||
authorTitle,
|
||||
conversationColor,
|
||||
customColor,
|
||||
emoji: storyReaction?.emoji,
|
||||
isFromMe,
|
||||
rawAttachment: storyReplyContext.attachment
|
||||
? processQuoteAttachment(storyReplyContext.attachment)
|
||||
: undefined,
|
||||
storyId: storyReplyContext.messageId,
|
||||
text: getStoryReplyText(window.i18n, storyReplyContext.attachment),
|
||||
emoji: re.emoji,
|
||||
timestamp: re.timestamp,
|
||||
from,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
(_, storyReplyContext): PropsData['storyReplyContext'] => storyReplyContext
|
||||
);
|
||||
return [...formattedReactions];
|
||||
};
|
||||
|
||||
export const getPropsForQuote = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
const getPropsForStoryReplyContext = (
|
||||
message: Pick<
|
||||
MessageWithUIFieldsType,
|
||||
'body' | 'conversationId' | 'storyReaction' | 'storyReplyContext'
|
||||
>,
|
||||
{
|
||||
conversationSelector,
|
||||
ourConversationId,
|
||||
}: {
|
||||
conversationSelector: GetConversationByIdType;
|
||||
ourConversationId?: string;
|
||||
}
|
||||
): PropsData['storyReplyContext'] => {
|
||||
const { storyReaction, storyReplyContext } = message;
|
||||
if (!storyReplyContext) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const contact = conversationSelector(storyReplyContext.authorUuid);
|
||||
|
||||
const authorTitle = contact.firstName || contact.title;
|
||||
const isFromMe = contact.id === ourConversationId;
|
||||
|
||||
const conversation = getConversation(message, conversationSelector);
|
||||
|
||||
const { conversationColor, customColor } =
|
||||
getConversationColorAttributes(conversation);
|
||||
|
||||
return {
|
||||
authorTitle,
|
||||
conversationColor,
|
||||
customColor,
|
||||
emoji: storyReaction?.emoji,
|
||||
isFromMe,
|
||||
rawAttachment: storyReplyContext.attachment
|
||||
? processQuoteAttachment(storyReplyContext.attachment)
|
||||
: undefined,
|
||||
storyId: storyReplyContext.messageId,
|
||||
text: getStoryReplyText(window.i18n, storyReplyContext.attachment),
|
||||
};
|
||||
};
|
||||
|
||||
export const getPropsForQuote = proxyMemoize(
|
||||
(
|
||||
message: Pick<
|
||||
MessageWithUIFieldsType,
|
||||
|
@ -591,8 +556,9 @@ export const getPropsForQuote = createSelectorCreator(memoizeByRoot, isEqual)(
|
|||
text,
|
||||
};
|
||||
},
|
||||
|
||||
(_, quote): PropsData['quote'] => quote
|
||||
{
|
||||
name: 'getPropsForQuote',
|
||||
}
|
||||
);
|
||||
|
||||
export type GetPropsForMessageOptions = Pick<
|
||||
|
@ -609,136 +575,6 @@ export type GetPropsForMessageOptions = Pick<
|
|||
| 'contactNameColorSelector'
|
||||
>;
|
||||
|
||||
type ShallowPropsType = Pick<
|
||||
PropsForMessage,
|
||||
| 'canDeleteForEveryone'
|
||||
| 'canDownload'
|
||||
| 'canReact'
|
||||
| 'canReply'
|
||||
| 'canRetry'
|
||||
| 'canRetryDeleteForEveryone'
|
||||
| 'contact'
|
||||
| 'contactNameColor'
|
||||
| 'conversationColor'
|
||||
| 'conversationId'
|
||||
| 'conversationTitle'
|
||||
| 'conversationType'
|
||||
| 'customColor'
|
||||
| 'deletedForEveryone'
|
||||
| 'direction'
|
||||
| 'displayLimit'
|
||||
| 'expirationLength'
|
||||
| 'expirationTimestamp'
|
||||
| 'giftBadge'
|
||||
| 'id'
|
||||
| 'isBlocked'
|
||||
| 'isMessageRequestAccepted'
|
||||
| 'isSelected'
|
||||
| 'isSelectedCounter'
|
||||
| 'isSticker'
|
||||
| 'isTapToView'
|
||||
| 'isTapToViewError'
|
||||
| 'isTapToViewExpired'
|
||||
| 'readStatus'
|
||||
| 'selectedReaction'
|
||||
| 'status'
|
||||
| 'text'
|
||||
| 'textDirection'
|
||||
| 'timestamp'
|
||||
>;
|
||||
|
||||
const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
|
||||
(
|
||||
message: MessageWithUIFieldsType,
|
||||
{
|
||||
accountSelector,
|
||||
conversationSelector,
|
||||
ourConversationId,
|
||||
ourNumber,
|
||||
ourACI,
|
||||
regionCode,
|
||||
selectedMessageId,
|
||||
selectedMessageCounter,
|
||||
contactNameColorSelector,
|
||||
}: GetPropsForMessageOptions
|
||||
): ShallowPropsType => {
|
||||
const { expireTimer, expirationStartTimestamp, conversationId } = message;
|
||||
const expirationLength = expireTimer
|
||||
? DurationInSeconds.toMillis(expireTimer)
|
||||
: undefined;
|
||||
|
||||
const conversation = getConversation(message, conversationSelector);
|
||||
const isGroup = conversation.type === 'group';
|
||||
const { sticker } = message;
|
||||
|
||||
const isMessageTapToView = isTapToView(message);
|
||||
|
||||
const isSelected = message.id === selectedMessageId;
|
||||
|
||||
const selectedReaction = (
|
||||
(message.reactions || []).find(re => re.fromId === ourConversationId) ||
|
||||
{}
|
||||
).emoji;
|
||||
|
||||
const authorId = getContactId(message, {
|
||||
conversationSelector,
|
||||
ourConversationId,
|
||||
ourNumber,
|
||||
ourACI,
|
||||
});
|
||||
const contactNameColor = contactNameColorSelector(conversationId, authorId);
|
||||
|
||||
const { conversationColor, customColor } =
|
||||
getConversationColorAttributes(conversation);
|
||||
|
||||
return {
|
||||
canDeleteForEveryone: canDeleteForEveryone(message),
|
||||
canDownload: canDownload(message, conversationSelector),
|
||||
canReact: canReact(message, ourConversationId, conversationSelector),
|
||||
canReply: canReply(message, ourConversationId, conversationSelector),
|
||||
canRetry: hasErrors(message),
|
||||
canRetryDeleteForEveryone: canRetryDeleteForEveryone(message),
|
||||
contact: getPropsForEmbeddedContact(message, regionCode, accountSelector),
|
||||
contactNameColor,
|
||||
conversationColor,
|
||||
conversationId,
|
||||
conversationTitle: conversation.title,
|
||||
conversationType: isGroup ? 'group' : 'direct',
|
||||
customColor,
|
||||
deletedForEveryone: message.deletedForEveryone || false,
|
||||
direction: isIncoming(message) ? 'incoming' : 'outgoing',
|
||||
displayLimit: message.displayLimit,
|
||||
expirationLength,
|
||||
expirationTimestamp: calculateExpirationTimestamp({
|
||||
expireTimer,
|
||||
expirationStartTimestamp,
|
||||
}),
|
||||
giftBadge: message.giftBadge,
|
||||
id: message.id,
|
||||
isBlocked: conversation.isBlocked || false,
|
||||
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
|
||||
isSelected,
|
||||
isSelectedCounter: isSelected ? selectedMessageCounter : undefined,
|
||||
isSticker: Boolean(sticker),
|
||||
isTapToView: isMessageTapToView,
|
||||
isTapToViewError:
|
||||
isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid,
|
||||
isTapToViewExpired: isMessageTapToView && message.isErased,
|
||||
readStatus: message.readStatus ?? ReadStatus.Read,
|
||||
selectedReaction,
|
||||
status: getMessagePropStatus(message, ourConversationId),
|
||||
text: message.body,
|
||||
textDirection: getTextDirection(message.body),
|
||||
timestamp: message.sent_at,
|
||||
};
|
||||
},
|
||||
|
||||
(_: unknown, props: ShallowPropsType) => props
|
||||
);
|
||||
|
||||
function getTextAttachment(
|
||||
message: MessageWithUIFieldsType
|
||||
): AttachmentType | undefined {
|
||||
|
@ -806,51 +642,116 @@ function getTextDirection(body?: string): TextDirection {
|
|||
}
|
||||
}
|
||||
|
||||
export const getPropsForMessage: (
|
||||
message: MessageWithUIFieldsType,
|
||||
options: GetPropsForMessageOptions
|
||||
) => Omit<PropsForMessage, 'renderingContext' | 'menu' | 'contextMenu'> =
|
||||
createSelectorCreator(memoizeByRoot)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
export const getPropsForMessage = proxyMemoize(
|
||||
(
|
||||
message: MessageWithUIFieldsType,
|
||||
options: GetPropsForMessageOptions
|
||||
): Omit<PropsForMessage, 'renderingContext' | 'menu' | 'contextMenu'> => {
|
||||
const attachments = getAttachmentsForMessage(message);
|
||||
const bodyRanges = processBodyRanges(message, options);
|
||||
const author = getAuthorForMessage(message, options);
|
||||
const previews = getPreviewsForMessage(message);
|
||||
const reactions = getReactionsForMessage(message, options);
|
||||
const quote = getPropsForQuote(message, options);
|
||||
const storyReplyContext = getPropsForStoryReplyContext(message, options);
|
||||
const textAttachment = getTextAttachment(message);
|
||||
const payment = getPayment(message);
|
||||
|
||||
getAttachmentsForMessage,
|
||||
processBodyRanges,
|
||||
getCachedAuthorForMessage,
|
||||
getPreviewsForMessage,
|
||||
getReactionsForMessage,
|
||||
getPropsForQuote,
|
||||
getPropsForStoryReplyContext,
|
||||
getTextAttachment,
|
||||
getPayment,
|
||||
getShallowPropsForMessage,
|
||||
(
|
||||
_,
|
||||
attachments: Array<AttachmentType>,
|
||||
bodyRanges: HydratedBodyRangesType | undefined,
|
||||
author: PropsData['author'],
|
||||
previews: Array<LinkPreviewType>,
|
||||
reactions: PropsData['reactions'],
|
||||
quote: PropsData['quote'],
|
||||
storyReplyContext: PropsData['storyReplyContext'],
|
||||
textAttachment: PropsData['textAttachment'],
|
||||
payment: PropsData['payment'],
|
||||
shallowProps: ShallowPropsType
|
||||
): Omit<PropsForMessage, 'renderingContext' | 'menu' | 'contextMenu'> => {
|
||||
return {
|
||||
attachments,
|
||||
author,
|
||||
bodyRanges,
|
||||
previews,
|
||||
quote,
|
||||
reactions,
|
||||
storyReplyContext,
|
||||
textAttachment,
|
||||
payment,
|
||||
...shallowProps,
|
||||
};
|
||||
}
|
||||
);
|
||||
const {
|
||||
accountSelector,
|
||||
conversationSelector,
|
||||
ourConversationId,
|
||||
ourNumber,
|
||||
ourACI,
|
||||
regionCode,
|
||||
selectedMessageId,
|
||||
selectedMessageCounter,
|
||||
contactNameColorSelector,
|
||||
} = options;
|
||||
|
||||
const { expireTimer, expirationStartTimestamp, conversationId } = message;
|
||||
const expirationLength = expireTimer
|
||||
? DurationInSeconds.toMillis(expireTimer)
|
||||
: undefined;
|
||||
|
||||
const conversation = getConversation(message, conversationSelector);
|
||||
const isGroup = conversation.type === 'group';
|
||||
const { sticker } = message;
|
||||
|
||||
const isMessageTapToView = isTapToView(message);
|
||||
|
||||
const isSelected = message.id === selectedMessageId;
|
||||
|
||||
const selectedReaction = (
|
||||
(message.reactions || []).find(re => re.fromId === ourConversationId) ||
|
||||
{}
|
||||
).emoji;
|
||||
|
||||
const authorId = getContactId(message, {
|
||||
conversationSelector,
|
||||
ourConversationId,
|
||||
ourNumber,
|
||||
ourACI,
|
||||
});
|
||||
const contactNameColor = contactNameColorSelector(conversationId, authorId);
|
||||
|
||||
const { conversationColor, customColor } =
|
||||
getConversationColorAttributes(conversation);
|
||||
|
||||
return {
|
||||
attachments,
|
||||
author,
|
||||
bodyRanges,
|
||||
previews,
|
||||
quote,
|
||||
reactions,
|
||||
storyReplyContext,
|
||||
textAttachment,
|
||||
payment,
|
||||
canDeleteForEveryone: canDeleteForEveryone(message),
|
||||
canDownload: canDownload(message, conversationSelector),
|
||||
canReact: canReact(message, ourConversationId, conversationSelector),
|
||||
canReply: canReply(message, ourConversationId, conversationSelector),
|
||||
canRetry: hasErrors(message),
|
||||
canRetryDeleteForEveryone: canRetryDeleteForEveryone(message),
|
||||
contact: getPropsForEmbeddedContact(message, regionCode, accountSelector),
|
||||
contactNameColor,
|
||||
conversationColor,
|
||||
conversationId,
|
||||
conversationTitle: conversation.title,
|
||||
conversationType: isGroup ? 'group' : 'direct',
|
||||
customColor,
|
||||
deletedForEveryone: message.deletedForEveryone || false,
|
||||
direction: isIncoming(message) ? 'incoming' : 'outgoing',
|
||||
displayLimit: message.displayLimit,
|
||||
expirationLength,
|
||||
expirationTimestamp: calculateExpirationTimestamp({
|
||||
expireTimer,
|
||||
expirationStartTimestamp,
|
||||
}),
|
||||
giftBadge: message.giftBadge,
|
||||
id: message.id,
|
||||
isBlocked: conversation.isBlocked || false,
|
||||
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
|
||||
isSelected,
|
||||
isSelectedCounter: isSelected ? selectedMessageCounter : undefined,
|
||||
isSticker: Boolean(sticker),
|
||||
isTapToView: isMessageTapToView,
|
||||
isTapToViewError:
|
||||
isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid,
|
||||
isTapToViewExpired: isMessageTapToView && message.isErased,
|
||||
readStatus: message.readStatus ?? ReadStatus.Read,
|
||||
selectedReaction,
|
||||
status: getMessagePropStatus(message, ourConversationId),
|
||||
text: message.body,
|
||||
textDirection: getTextDirection(message.body),
|
||||
timestamp: message.sent_at,
|
||||
};
|
||||
},
|
||||
{
|
||||
name: 'getPropsForMessage',
|
||||
}
|
||||
);
|
||||
|
||||
// This is getPropsForMessage but wrapped in reselect's createSelector so that
|
||||
// we can derive all of the selector dependencies that getPropsForMessage
|
||||
|
@ -893,19 +794,6 @@ export const getMessagePropsSelector = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
export const getBubblePropsForMessage = createSelectorCreator(memoizeByRoot)(
|
||||
// `memoizeByRoot` requirement
|
||||
identity,
|
||||
|
||||
getPropsForMessage,
|
||||
|
||||
(_, data): TimelineItemType => ({
|
||||
type: 'message' as const,
|
||||
data,
|
||||
timestamp: data.timestamp,
|
||||
})
|
||||
);
|
||||
|
||||
// Top-level prop generation for the message bubble
|
||||
export function getPropsForBubble(
|
||||
message: MessageWithUIFieldsType,
|
||||
|
@ -1038,7 +926,13 @@ export function getPropsForBubble(
|
|||
};
|
||||
}
|
||||
|
||||
return getBubblePropsForMessage(message, options);
|
||||
const data = getPropsForMessage(message, options);
|
||||
|
||||
return {
|
||||
type: 'message' as const,
|
||||
data,
|
||||
timestamp: data.timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
function getPropsForPaymentEvent(
|
||||
|
@ -1047,7 +941,7 @@ function getPropsForPaymentEvent(
|
|||
): Omit<PaymentEventNotificationPropsType, 'i18n'> {
|
||||
return {
|
||||
sender: conversationSelector(message.sourceUuid),
|
||||
conversation: conversationSelector(message.conversationId),
|
||||
conversation: getConversation(message, conversationSelector),
|
||||
event: message.payment,
|
||||
};
|
||||
}
|
||||
|
@ -1583,7 +1477,7 @@ function getPropsForDeliveryIssue(
|
|||
{ conversationSelector }: GetPropsForBubbleOptions
|
||||
): DeliveryIssuePropsType {
|
||||
const sender = conversationSelector(message.sourceUuid);
|
||||
const conversation = conversationSelector(message.conversationId);
|
||||
const conversation = getConversation(message, conversationSelector);
|
||||
|
||||
return {
|
||||
sender,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue