Confine message selector cache to component
This commit is contained in:
parent
7f0ed2599d
commit
ef13eb06fc
11 changed files with 395 additions and 311 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue