Confine message selector cache to component

This commit is contained in:
Fedor Indutny 2023-01-19 11:56:02 -08:00 committed by GitHub
parent 7f0ed2599d
commit ef13eb06fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 395 additions and 311 deletions

View file

@ -7,7 +7,6 @@ import filesize from 'filesize';
import getDirection from 'direction';
import emojiRegex from 'emoji-regex';
import LinkifyIt from 'linkify-it';
import { memoize } from '@indutny/sneequals';
import type { StateType } from '../reducer';
import type {
@ -269,62 +268,58 @@ export function getConversation(
// Message
export const getAttachmentsForMessage = memoize(
({
sticker,
attachments = [],
}: MessageWithUIFieldsType): Array<AttachmentType> => {
if (sticker && sticker.data) {
const { data } = sticker;
export const getAttachmentsForMessage = ({
sticker,
attachments = [],
}: MessageWithUIFieldsType): Array<AttachmentType> => {
if (sticker && sticker.data) {
const { data } = sticker;
// We don't show anything if we don't have the sticker or the blurhash...
if (!data.blurHash && (data.pending || !data.path)) {
return [];
}
return [
{
...data,
// We want to show the blurhash for stickers, not the spinner
pending: false,
url: data.path
? window.Signal.Migrations.getAbsoluteAttachmentPath(data.path)
: undefined,
},
];
// We don't show anything if we don't have the sticker or the blurhash...
if (!data.blurHash && (data.pending || !data.path)) {
return [];
}
return attachments
.filter(attachment => !attachment.error || canBeDownloaded(attachment))
.map(attachment => getPropsForAttachment(attachment))
.filter(isNotNil);
return [
{
...data,
// We want to show the blurhash for stickers, not the spinner
pending: false,
url: data.path
? window.Signal.Migrations.getAbsoluteAttachmentPath(data.path)
: undefined,
},
];
}
);
export const processBodyRanges = memoize(
(
{ bodyRanges }: Pick<MessageWithUIFieldsType, 'bodyRanges'>,
options: { conversationSelector: GetConversationByIdType }
): HydratedBodyRangesType | undefined => {
if (!bodyRanges) {
return undefined;
}
return attachments
.filter(attachment => !attachment.error || canBeDownloaded(attachment))
.map(attachment => getPropsForAttachment(attachment))
.filter(isNotNil);
};
return bodyRanges
.filter(range => range.mentionUuid)
.map(range => {
const { conversationSelector } = options;
const conversation = conversationSelector(range.mentionUuid);
return {
...range,
conversationID: conversation.id,
replacementText: conversation.title,
};
})
.sort((a, b) => b.start - a.start);
export const processBodyRanges = (
{ bodyRanges }: Pick<MessageWithUIFieldsType, 'bodyRanges'>,
options: { conversationSelector: GetConversationByIdType }
): HydratedBodyRangesType | undefined => {
if (!bodyRanges) {
return undefined;
}
);
return bodyRanges
.filter(range => range.mentionUuid)
.map(range => {
const { conversationSelector } = options;
const conversation = conversationSelector(range.mentionUuid);
return {
...range,
conversationID: conversation.id,
replacementText: conversation.title,
};
})
.sort((a, b) => b.start - a.start);
};
const getAuthorForMessage = (
message: MessageWithUIFieldsType,
@ -482,74 +477,72 @@ const getPropsForStoryReplyContext = (
};
};
export const getPropsForQuote = memoize(
(
message: Pick<
MessageWithUIFieldsType,
'conversationId' | 'quote' | 'payment'
>,
{
conversationSelector,
ourConversationId,
}: {
conversationSelector: GetConversationByIdType;
ourConversationId?: string;
}
): PropsData['quote'] => {
const { quote } = message;
if (!quote) {
return undefined;
}
const {
author,
authorUuid,
id: sentAt,
isViewOnce,
isGiftBadge: isTargetGiftBadge,
referencedMessageNotFound,
payment,
text = '',
} = quote;
const contact = conversationSelector(authorUuid || author);
const authorId = contact.id;
const authorName = contact.name;
const authorPhoneNumber = contact.phoneNumber;
const authorProfileName = contact.profileName;
const authorTitle = contact.title;
const isFromMe = authorId === ourConversationId;
const firstAttachment = quote.attachments && quote.attachments[0];
const conversation = getConversation(message, conversationSelector);
const { conversationColor, customColor } =
getConversationColorAttributes(conversation);
return {
authorId,
authorName,
authorPhoneNumber,
authorProfileName,
authorTitle,
bodyRanges: processBodyRanges(quote, { conversationSelector }),
conversationColor,
conversationTitle: conversation.title,
customColor,
isFromMe,
rawAttachment: firstAttachment
? processQuoteAttachment(firstAttachment)
: undefined,
payment,
isGiftBadge: Boolean(isTargetGiftBadge),
isViewOnce,
referencedMessageNotFound,
sentAt: Number(sentAt),
text,
};
export const getPropsForQuote = (
message: Pick<
MessageWithUIFieldsType,
'conversationId' | 'quote' | 'payment'
>,
{
conversationSelector,
ourConversationId,
}: {
conversationSelector: GetConversationByIdType;
ourConversationId?: string;
}
);
): PropsData['quote'] => {
const { quote } = message;
if (!quote) {
return undefined;
}
const {
author,
authorUuid,
id: sentAt,
isViewOnce,
isGiftBadge: isTargetGiftBadge,
referencedMessageNotFound,
payment,
text = '',
} = quote;
const contact = conversationSelector(authorUuid || author);
const authorId = contact.id;
const authorName = contact.name;
const authorPhoneNumber = contact.phoneNumber;
const authorProfileName = contact.profileName;
const authorTitle = contact.title;
const isFromMe = authorId === ourConversationId;
const firstAttachment = quote.attachments && quote.attachments[0];
const conversation = getConversation(message, conversationSelector);
const { conversationColor, customColor } =
getConversationColorAttributes(conversation);
return {
authorId,
authorName,
authorPhoneNumber,
authorProfileName,
authorTitle,
bodyRanges: processBodyRanges(quote, { conversationSelector }),
conversationColor,
conversationTitle: conversation.title,
customColor,
isFromMe,
rawAttachment: firstAttachment
? processQuoteAttachment(firstAttachment)
: undefined,
payment,
isGiftBadge: Boolean(isTargetGiftBadge),
isViewOnce,
referencedMessageNotFound,
sentAt: Number(sentAt),
text,
};
};
export type GetPropsForMessageOptions = Pick<
GetPropsForBubbleOptions,
@ -632,113 +625,110 @@ function getTextDirection(body?: string): TextDirection {
}
}
export const getPropsForMessage = memoize(
(
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);
export const getPropsForMessage = (
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);
const {
accountSelector,
conversationSelector,
ourConversationId,
ourNumber,
ourACI,
regionCode,
selectedMessageId,
selectedMessageCounter,
contactNameColorSelector,
} = options;
const {
accountSelector,
conversationSelector,
ourConversationId,
ourNumber,
ourACI,
regionCode,
selectedMessageId,
selectedMessageCounter,
contactNameColorSelector,
} = options;
const { expireTimer, expirationStartTimestamp, conversationId } = message;
const expirationLength = expireTimer
? DurationInSeconds.toMillis(expireTimer)
: undefined;
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 conversation = getConversation(message, conversationSelector);
const isGroup = conversation.type === 'group';
const { sticker } = message;
const isMessageTapToView = isTapToView(message);
const isMessageTapToView = isTapToView(message);
const isSelected = message.id === selectedMessageId;
const isSelected = message.id === selectedMessageId;
const selectedReaction = (
(message.reactions || []).find(re => re.fromId === ourConversationId) ||
{}
).emoji;
const selectedReaction = (
(message.reactions || []).find(re => re.fromId === ourConversationId) || {}
).emoji;
const authorId = getContactId(message, {
conversationSelector,
ourConversationId,
ourNumber,
ourACI,
});
const contactNameColor = contactNameColorSelector(conversationId, authorId);
const authorId = getContactId(message, {
conversationSelector,
ourConversationId,
ourNumber,
ourACI,
});
const contactNameColor = contactNameColorSelector(conversationId, authorId);
const { conversationColor, customColor } =
getConversationColorAttributes(conversation);
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,
};
}
);
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,
};
};
// This is getPropsForMessage but wrapped in reselect's createSelector so that
// we can derive all of the selector dependencies that getPropsForMessage
@ -1067,7 +1057,7 @@ function getPropsForTimerNotification(
const { expireTimer, fromSync, source, sourceUuid } = timerUpdate;
const disabled = !expireTimer;
const sourceId = sourceUuid || source;
const formattedContact = conversationSelector(sourceId);
const { id: formattedContactId, title } = conversationSelector(sourceId);
// Pacify typescript
type MaybeExpireTimerType =
@ -1087,7 +1077,7 @@ function getPropsForTimerNotification(
};
const basicProps = {
...formattedContact,
title,
...maybeExpireTimer,
type: 'fromOther' as const,
};
@ -1098,7 +1088,7 @@ function getPropsForTimerNotification(
type: 'fromSync' as const,
};
}
if (formattedContact.id === ourConversationId) {
if (formattedContactId === ourConversationId) {
return {
...basicProps,
type: 'fromMe' as const,

View file

@ -1,11 +1,9 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { memoize } from '@indutny/sneequals';
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
import type { StateType } from '../reducer';
import type { MessageWithUIFieldsType } from '../ducks/conversations';
import {
getContactNameColorSelector,
getConversationSelector,
@ -23,41 +21,14 @@ import {
import { getActiveCall, getCallSelector } from './calling';
import { getPropsForBubble } from './message';
const getTimelineItemInner = memoize(
(message: MessageWithUIFieldsType, state: StateType): TimelineItemType => {
const selectedMessage = getSelectedMessage(state);
const conversationSelector = getConversationSelector(state);
const regionCode = getRegionCode(state);
const ourNumber = getUserNumber(state);
const ourACI = getUserACI(state);
const ourPNI = getUserPNI(state);
const ourConversationId = getUserConversationId(state);
const callSelector = getCallSelector(state);
const activeCall = getActiveCall(state);
const accountSelector = getAccountSelector(state);
const contactNameColorSelector = getContactNameColorSelector(state);
return getPropsForBubble(message, {
conversationSelector,
ourConversationId,
ourNumber,
ourACI,
ourPNI,
regionCode,
selectedMessageId: selectedMessage?.id,
selectedMessageCounter: selectedMessage?.counter,
contactNameColorSelector,
callSelector,
activeCall,
accountSelector,
});
}
);
export const getTimelineItem = (
state: StateType,
id: string
id?: string
): TimelineItemType | undefined => {
if (id === undefined) {
return undefined;
}
const messageLookup = getMessages(state);
const message = messageLookup[id];
@ -65,5 +36,30 @@ export const getTimelineItem = (
return undefined;
}
return getTimelineItemInner(message, state);
const selectedMessage = getSelectedMessage(state);
const conversationSelector = getConversationSelector(state);
const regionCode = getRegionCode(state);
const ourNumber = getUserNumber(state);
const ourACI = getUserACI(state);
const ourPNI = getUserPNI(state);
const ourConversationId = getUserConversationId(state);
const callSelector = getCallSelector(state);
const activeCall = getActiveCall(state);
const accountSelector = getAccountSelector(state);
const contactNameColorSelector = getContactNameColorSelector(state);
return getPropsForBubble(message, {
conversationSelector,
ourConversationId,
ourNumber,
ourACI,
ourPNI,
regionCode,
selectedMessageId: selectedMessage?.id,
selectedMessageCounter: selectedMessage?.counter,
contactNameColorSelector,
callSelector,
activeCall,
accountSelector,
});
};