Confine message selector cache to component
This commit is contained in:
parent
7f0ed2599d
commit
ef13eb06fc
11 changed files with 395 additions and 311 deletions
|
@ -86,7 +86,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/fast-memoize": "1.2.6",
|
"@formatjs/fast-memoize": "1.2.6",
|
||||||
"@indutny/frameless-titlebar": "2.3.5",
|
"@indutny/frameless-titlebar": "2.3.5",
|
||||||
"@indutny/sneequals": "3.2.0",
|
"@indutny/sneequals": "4.0.0",
|
||||||
"@popperjs/core": "2.11.6",
|
"@popperjs/core": "2.11.6",
|
||||||
"@react-spring/web": "9.5.5",
|
"@react-spring/web": "9.5.5",
|
||||||
"@signalapp/better-sqlite3": "8.1.1",
|
"@signalapp/better-sqlite3": "8.1.1",
|
||||||
|
|
23
ts/hooks/useProxySelector.ts
Normal file
23
ts/hooks/useProxySelector.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { memoize } from '@indutny/sneequals';
|
||||||
|
|
||||||
|
import type { StateType } from '../state/reducer';
|
||||||
|
|
||||||
|
export function useProxySelector<Params extends Array<unknown>, Result>(
|
||||||
|
selector: (state: StateType, ...params: Params) => Result,
|
||||||
|
...params: Params
|
||||||
|
): Result {
|
||||||
|
const memoized = useMemo(() => memoize(selector), [selector]);
|
||||||
|
|
||||||
|
return useSelector(
|
||||||
|
useCallback(
|
||||||
|
(state: StateType) => memoized(state, ...params),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[memoized, ...params]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
|
@ -55,6 +55,8 @@ import { ToastType } from '../../types/Toast';
|
||||||
import type { ShowToastActionType } from './toast';
|
import type { ShowToastActionType } from './toast';
|
||||||
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue';
|
||||||
import MessageSender from '../../textsecure/SendMessage';
|
import MessageSender from '../../textsecure/SendMessage';
|
||||||
|
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||||
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -1566,6 +1568,10 @@ export const actions = {
|
||||||
toggleSpeakerView,
|
toggleSpeakerView,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCallingActions = (): BoundActionCreatorsMapObject<
|
||||||
|
typeof actions
|
||||||
|
> => useBoundActions(actions);
|
||||||
|
|
||||||
export type ActionsType = ReadonlyDeep<typeof actions>;
|
export type ActionsType = ReadonlyDeep<typeof actions>;
|
||||||
|
|
||||||
// Reducer
|
// Reducer
|
||||||
|
|
|
@ -7,7 +7,6 @@ import filesize from 'filesize';
|
||||||
import getDirection from 'direction';
|
import getDirection from 'direction';
|
||||||
import emojiRegex from 'emoji-regex';
|
import emojiRegex from 'emoji-regex';
|
||||||
import LinkifyIt from 'linkify-it';
|
import LinkifyIt from 'linkify-it';
|
||||||
import { memoize } from '@indutny/sneequals';
|
|
||||||
|
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import type {
|
import type {
|
||||||
|
@ -269,62 +268,58 @@ export function getConversation(
|
||||||
|
|
||||||
// Message
|
// Message
|
||||||
|
|
||||||
export const getAttachmentsForMessage = memoize(
|
export const getAttachmentsForMessage = ({
|
||||||
({
|
sticker,
|
||||||
sticker,
|
attachments = [],
|
||||||
attachments = [],
|
}: MessageWithUIFieldsType): Array<AttachmentType> => {
|
||||||
}: MessageWithUIFieldsType): Array<AttachmentType> => {
|
if (sticker && sticker.data) {
|
||||||
if (sticker && sticker.data) {
|
const { data } = sticker;
|
||||||
const { data } = sticker;
|
|
||||||
|
|
||||||
// We don't show anything if we don't have the sticker or the blurhash...
|
// We don't show anything if we don't have the sticker or the blurhash...
|
||||||
if (!data.blurHash && (data.pending || !data.path)) {
|
if (!data.blurHash && (data.pending || !data.path)) {
|
||||||
return [];
|
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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return attachments
|
return [
|
||||||
.filter(attachment => !attachment.error || canBeDownloaded(attachment))
|
{
|
||||||
.map(attachment => getPropsForAttachment(attachment))
|
...data,
|
||||||
.filter(isNotNil);
|
// 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(
|
return attachments
|
||||||
(
|
.filter(attachment => !attachment.error || canBeDownloaded(attachment))
|
||||||
{ bodyRanges }: Pick<MessageWithUIFieldsType, 'bodyRanges'>,
|
.map(attachment => getPropsForAttachment(attachment))
|
||||||
options: { conversationSelector: GetConversationByIdType }
|
.filter(isNotNil);
|
||||||
): HydratedBodyRangesType | undefined => {
|
};
|
||||||
if (!bodyRanges) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bodyRanges
|
export const processBodyRanges = (
|
||||||
.filter(range => range.mentionUuid)
|
{ bodyRanges }: Pick<MessageWithUIFieldsType, 'bodyRanges'>,
|
||||||
.map(range => {
|
options: { conversationSelector: GetConversationByIdType }
|
||||||
const { conversationSelector } = options;
|
): HydratedBodyRangesType | undefined => {
|
||||||
const conversation = conversationSelector(range.mentionUuid);
|
if (!bodyRanges) {
|
||||||
|
return undefined;
|
||||||
return {
|
|
||||||
...range,
|
|
||||||
conversationID: conversation.id,
|
|
||||||
replacementText: conversation.title,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => b.start - a.start);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
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 = (
|
const getAuthorForMessage = (
|
||||||
message: MessageWithUIFieldsType,
|
message: MessageWithUIFieldsType,
|
||||||
|
@ -482,74 +477,72 @@ const getPropsForStoryReplyContext = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPropsForQuote = memoize(
|
export const getPropsForQuote = (
|
||||||
(
|
message: Pick<
|
||||||
message: Pick<
|
MessageWithUIFieldsType,
|
||||||
MessageWithUIFieldsType,
|
'conversationId' | 'quote' | 'payment'
|
||||||
'conversationId' | 'quote' | 'payment'
|
>,
|
||||||
>,
|
{
|
||||||
{
|
conversationSelector,
|
||||||
conversationSelector,
|
ourConversationId,
|
||||||
ourConversationId,
|
}: {
|
||||||
}: {
|
conversationSelector: GetConversationByIdType;
|
||||||
conversationSelector: GetConversationByIdType;
|
ourConversationId?: string;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
): 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<
|
export type GetPropsForMessageOptions = Pick<
|
||||||
GetPropsForBubbleOptions,
|
GetPropsForBubbleOptions,
|
||||||
|
@ -632,113 +625,110 @@ function getTextDirection(body?: string): TextDirection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPropsForMessage = memoize(
|
export const getPropsForMessage = (
|
||||||
(
|
message: MessageWithUIFieldsType,
|
||||||
message: MessageWithUIFieldsType,
|
options: GetPropsForMessageOptions
|
||||||
options: GetPropsForMessageOptions
|
): Omit<PropsForMessage, 'renderingContext' | 'menu' | 'contextMenu'> => {
|
||||||
): Omit<PropsForMessage, 'renderingContext' | 'menu' | 'contextMenu'> => {
|
const attachments = getAttachmentsForMessage(message);
|
||||||
const attachments = getAttachmentsForMessage(message);
|
const bodyRanges = processBodyRanges(message, options);
|
||||||
const bodyRanges = processBodyRanges(message, options);
|
const author = getAuthorForMessage(message, options);
|
||||||
const author = getAuthorForMessage(message, options);
|
const previews = getPreviewsForMessage(message);
|
||||||
const previews = getPreviewsForMessage(message);
|
const reactions = getReactionsForMessage(message, options);
|
||||||
const reactions = getReactionsForMessage(message, options);
|
const quote = getPropsForQuote(message, options);
|
||||||
const quote = getPropsForQuote(message, options);
|
const storyReplyContext = getPropsForStoryReplyContext(message, options);
|
||||||
const storyReplyContext = getPropsForStoryReplyContext(message, options);
|
const textAttachment = getTextAttachment(message);
|
||||||
const textAttachment = getTextAttachment(message);
|
const payment = getPayment(message);
|
||||||
const payment = getPayment(message);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
accountSelector,
|
accountSelector,
|
||||||
conversationSelector,
|
conversationSelector,
|
||||||
ourConversationId,
|
ourConversationId,
|
||||||
ourNumber,
|
ourNumber,
|
||||||
ourACI,
|
ourACI,
|
||||||
regionCode,
|
regionCode,
|
||||||
selectedMessageId,
|
selectedMessageId,
|
||||||
selectedMessageCounter,
|
selectedMessageCounter,
|
||||||
contactNameColorSelector,
|
contactNameColorSelector,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const { expireTimer, expirationStartTimestamp, conversationId } = message;
|
const { expireTimer, expirationStartTimestamp, conversationId } = message;
|
||||||
const expirationLength = expireTimer
|
const expirationLength = expireTimer
|
||||||
? DurationInSeconds.toMillis(expireTimer)
|
? DurationInSeconds.toMillis(expireTimer)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const conversation = getConversation(message, conversationSelector);
|
const conversation = getConversation(message, conversationSelector);
|
||||||
const isGroup = conversation.type === 'group';
|
const isGroup = conversation.type === 'group';
|
||||||
const { sticker } = message;
|
const { sticker } = message;
|
||||||
|
|
||||||
const isMessageTapToView = isTapToView(message);
|
const isMessageTapToView = isTapToView(message);
|
||||||
|
|
||||||
const isSelected = message.id === selectedMessageId;
|
const isSelected = message.id === selectedMessageId;
|
||||||
|
|
||||||
const selectedReaction = (
|
const selectedReaction = (
|
||||||
(message.reactions || []).find(re => re.fromId === ourConversationId) ||
|
(message.reactions || []).find(re => re.fromId === ourConversationId) || {}
|
||||||
{}
|
).emoji;
|
||||||
).emoji;
|
|
||||||
|
|
||||||
const authorId = getContactId(message, {
|
const authorId = getContactId(message, {
|
||||||
conversationSelector,
|
conversationSelector,
|
||||||
ourConversationId,
|
ourConversationId,
|
||||||
ourNumber,
|
ourNumber,
|
||||||
ourACI,
|
ourACI,
|
||||||
});
|
});
|
||||||
const contactNameColor = contactNameColorSelector(conversationId, authorId);
|
const contactNameColor = contactNameColorSelector(conversationId, authorId);
|
||||||
|
|
||||||
const { conversationColor, customColor } =
|
const { conversationColor, customColor } =
|
||||||
getConversationColorAttributes(conversation);
|
getConversationColorAttributes(conversation);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attachments,
|
attachments,
|
||||||
author,
|
author,
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
previews,
|
previews,
|
||||||
quote,
|
quote,
|
||||||
reactions,
|
reactions,
|
||||||
storyReplyContext,
|
storyReplyContext,
|
||||||
textAttachment,
|
textAttachment,
|
||||||
payment,
|
payment,
|
||||||
canDeleteForEveryone: canDeleteForEveryone(message),
|
canDeleteForEveryone: canDeleteForEveryone(message),
|
||||||
canDownload: canDownload(message, conversationSelector),
|
canDownload: canDownload(message, conversationSelector),
|
||||||
canReact: canReact(message, ourConversationId, conversationSelector),
|
canReact: canReact(message, ourConversationId, conversationSelector),
|
||||||
canReply: canReply(message, ourConversationId, conversationSelector),
|
canReply: canReply(message, ourConversationId, conversationSelector),
|
||||||
canRetry: hasErrors(message),
|
canRetry: hasErrors(message),
|
||||||
canRetryDeleteForEveryone: canRetryDeleteForEveryone(message),
|
canRetryDeleteForEveryone: canRetryDeleteForEveryone(message),
|
||||||
contact: getPropsForEmbeddedContact(message, regionCode, accountSelector),
|
contact: getPropsForEmbeddedContact(message, regionCode, accountSelector),
|
||||||
contactNameColor,
|
contactNameColor,
|
||||||
conversationColor,
|
conversationColor,
|
||||||
conversationId,
|
conversationId,
|
||||||
conversationTitle: conversation.title,
|
conversationTitle: conversation.title,
|
||||||
conversationType: isGroup ? 'group' : 'direct',
|
conversationType: isGroup ? 'group' : 'direct',
|
||||||
customColor,
|
customColor,
|
||||||
deletedForEveryone: message.deletedForEveryone || false,
|
deletedForEveryone: message.deletedForEveryone || false,
|
||||||
direction: isIncoming(message) ? 'incoming' : 'outgoing',
|
direction: isIncoming(message) ? 'incoming' : 'outgoing',
|
||||||
displayLimit: message.displayLimit,
|
displayLimit: message.displayLimit,
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp: calculateExpirationTimestamp({
|
expirationTimestamp: calculateExpirationTimestamp({
|
||||||
expireTimer,
|
expireTimer,
|
||||||
expirationStartTimestamp,
|
expirationStartTimestamp,
|
||||||
}),
|
}),
|
||||||
giftBadge: message.giftBadge,
|
giftBadge: message.giftBadge,
|
||||||
id: message.id,
|
id: message.id,
|
||||||
isBlocked: conversation.isBlocked || false,
|
isBlocked: conversation.isBlocked || false,
|
||||||
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
|
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
|
||||||
isSelected,
|
isSelected,
|
||||||
isSelectedCounter: isSelected ? selectedMessageCounter : undefined,
|
isSelectedCounter: isSelected ? selectedMessageCounter : undefined,
|
||||||
isSticker: Boolean(sticker),
|
isSticker: Boolean(sticker),
|
||||||
isTapToView: isMessageTapToView,
|
isTapToView: isMessageTapToView,
|
||||||
isTapToViewError:
|
isTapToViewError:
|
||||||
isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid,
|
isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid,
|
||||||
isTapToViewExpired: isMessageTapToView && message.isErased,
|
isTapToViewExpired: isMessageTapToView && message.isErased,
|
||||||
readStatus: message.readStatus ?? ReadStatus.Read,
|
readStatus: message.readStatus ?? ReadStatus.Read,
|
||||||
selectedReaction,
|
selectedReaction,
|
||||||
status: getMessagePropStatus(message, ourConversationId),
|
status: getMessagePropStatus(message, ourConversationId),
|
||||||
text: message.body,
|
text: message.body,
|
||||||
textDirection: getTextDirection(message.body),
|
textDirection: getTextDirection(message.body),
|
||||||
timestamp: message.sent_at,
|
timestamp: message.sent_at,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
);
|
|
||||||
|
|
||||||
// This is getPropsForMessage but wrapped in reselect's createSelector so that
|
// This is getPropsForMessage but wrapped in reselect's createSelector so that
|
||||||
// we can derive all of the selector dependencies that getPropsForMessage
|
// we can derive all of the selector dependencies that getPropsForMessage
|
||||||
|
@ -1067,7 +1057,7 @@ function getPropsForTimerNotification(
|
||||||
const { expireTimer, fromSync, source, sourceUuid } = timerUpdate;
|
const { expireTimer, fromSync, source, sourceUuid } = timerUpdate;
|
||||||
const disabled = !expireTimer;
|
const disabled = !expireTimer;
|
||||||
const sourceId = sourceUuid || source;
|
const sourceId = sourceUuid || source;
|
||||||
const formattedContact = conversationSelector(sourceId);
|
const { id: formattedContactId, title } = conversationSelector(sourceId);
|
||||||
|
|
||||||
// Pacify typescript
|
// Pacify typescript
|
||||||
type MaybeExpireTimerType =
|
type MaybeExpireTimerType =
|
||||||
|
@ -1087,7 +1077,7 @@ function getPropsForTimerNotification(
|
||||||
};
|
};
|
||||||
|
|
||||||
const basicProps = {
|
const basicProps = {
|
||||||
...formattedContact,
|
title,
|
||||||
...maybeExpireTimer,
|
...maybeExpireTimer,
|
||||||
type: 'fromOther' as const,
|
type: 'fromOther' as const,
|
||||||
};
|
};
|
||||||
|
@ -1098,7 +1088,7 @@ function getPropsForTimerNotification(
|
||||||
type: 'fromSync' as const,
|
type: 'fromSync' as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (formattedContact.id === ourConversationId) {
|
if (formattedContactId === ourConversationId) {
|
||||||
return {
|
return {
|
||||||
...basicProps,
|
...basicProps,
|
||||||
type: 'fromMe' as const,
|
type: 'fromMe' as const,
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { memoize } from '@indutny/sneequals';
|
|
||||||
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
|
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
|
||||||
|
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import type { MessageWithUIFieldsType } from '../ducks/conversations';
|
|
||||||
import {
|
import {
|
||||||
getContactNameColorSelector,
|
getContactNameColorSelector,
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
|
@ -23,41 +21,14 @@ import {
|
||||||
import { getActiveCall, getCallSelector } from './calling';
|
import { getActiveCall, getCallSelector } from './calling';
|
||||||
import { getPropsForBubble } from './message';
|
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 = (
|
export const getTimelineItem = (
|
||||||
state: StateType,
|
state: StateType,
|
||||||
id: string
|
id?: string
|
||||||
): TimelineItemType | undefined => {
|
): TimelineItemType | undefined => {
|
||||||
|
if (id === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const messageLookup = getMessages(state);
|
const messageLookup = getMessages(state);
|
||||||
|
|
||||||
const message = messageLookup[id];
|
const message = messageLookup[id];
|
||||||
|
@ -65,5 +36,30 @@ export const getTimelineItem = (
|
||||||
return undefined;
|
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,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,9 +32,6 @@ import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog'
|
||||||
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from './ContactSpoofingReviewDialog';
|
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from './ContactSpoofingReviewDialog';
|
||||||
import { SmartTypingBubble } from './TypingBubble';
|
import { SmartTypingBubble } from './TypingBubble';
|
||||||
import { SmartHeroRow } from './HeroRow';
|
import { SmartHeroRow } from './HeroRow';
|
||||||
import { renderAudioAttachment } from './renderAudioAttachment';
|
|
||||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
|
||||||
import { renderReactionPicker } from './renderReactionPicker';
|
|
||||||
|
|
||||||
import { getOwn } from '../../util/getOwn';
|
import { getOwn } from '../../util/getOwn';
|
||||||
import { assertDev } from '../../util/assert';
|
import { assertDev } from '../../util/assert';
|
||||||
|
@ -82,9 +79,6 @@ function renderItem({
|
||||||
messageId={messageId}
|
messageId={messageId}
|
||||||
previousMessageId={previousMessageId}
|
previousMessageId={previousMessageId}
|
||||||
nextMessageId={nextMessageId}
|
nextMessageId={nextMessageId}
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
|
||||||
renderReactionPicker={renderReactionPicker}
|
|
||||||
renderAudioAttachment={renderAudioAttachment}
|
|
||||||
unreadIndicatorPlacement={unreadIndicatorPlacement}
|
unreadIndicatorPlacement={unreadIndicatorPlacement}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,18 +3,21 @@
|
||||||
|
|
||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { mapDispatchToProps } from '../actions';
|
|
||||||
import type { StateType } from '../reducer';
|
|
||||||
|
|
||||||
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
||||||
|
import type { WidthBreakpoint } from '../../components/_util';
|
||||||
|
import { useProxySelector } from '../../hooks/useProxySelector';
|
||||||
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
|
import { useComposerActions } from '../ducks/composer';
|
||||||
|
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||||
|
import { useAccountsActions } from '../ducks/accounts';
|
||||||
|
import { useLightboxActions } from '../ducks/lightbox';
|
||||||
|
import { useStoriesActions } from '../ducks/stories';
|
||||||
|
import { useCallingActions } from '../ducks/calling';
|
||||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||||
import { getIntl, getInteractionMode, getTheme } from '../selectors/user';
|
import { getIntl, getInteractionMode, getTheme } from '../selectors/user';
|
||||||
import {
|
import { getSelectedMessage } from '../selectors/conversations';
|
||||||
getConversationSelector,
|
|
||||||
getSelectedMessage,
|
|
||||||
} from '../selectors/conversations';
|
|
||||||
import { getTimelineItem } from '../selectors/timeline';
|
import { getTimelineItem } from '../selectors/timeline';
|
||||||
import {
|
import {
|
||||||
areMessagesInSameGroup,
|
areMessagesInSameGroup,
|
||||||
|
@ -25,9 +28,13 @@ import {
|
||||||
import { SmartContactName } from './ContactName';
|
import { SmartContactName } from './ContactName';
|
||||||
import { SmartUniversalTimerNotification } from './UniversalTimerNotification';
|
import { SmartUniversalTimerNotification } from './UniversalTimerNotification';
|
||||||
import { isSameDay } from '../../util/timestamp';
|
import { isSameDay } from '../../util/timestamp';
|
||||||
|
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||||
|
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||||
|
import { renderReactionPicker } from './renderReactionPicker';
|
||||||
|
|
||||||
type ExternalProps = {
|
type ExternalProps = {
|
||||||
containerElementRef: RefObject<HTMLElement>;
|
containerElementRef: RefObject<HTMLElement>;
|
||||||
|
containerWidthBreakpoint: WidthBreakpoint;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
isOldestTimelineItem: boolean;
|
isOldestTimelineItem: boolean;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
@ -44,9 +51,10 @@ function renderUniversalTimerNotification(): JSX.Element {
|
||||||
return <SmartUniversalTimerNotification />;
|
return <SmartUniversalTimerNotification />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
export function SmartTimelineItem(props: ExternalProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
|
containerWidthBreakpoint,
|
||||||
conversationId,
|
conversationId,
|
||||||
isOldestTimelineItem,
|
isOldestTimelineItem,
|
||||||
messageId,
|
messageId,
|
||||||
|
@ -55,21 +63,19 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
unreadIndicatorPlacement,
|
unreadIndicatorPlacement,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const item = getTimelineItem(state, messageId);
|
const i18n = useSelector(getIntl);
|
||||||
const previousItem = previousMessageId
|
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||||
? getTimelineItem(state, previousMessageId)
|
const interactionMode = useSelector(getInteractionMode);
|
||||||
: undefined;
|
const theme = useSelector(getTheme);
|
||||||
const nextItem = nextMessageId
|
const item = useProxySelector(getTimelineItem, messageId);
|
||||||
? getTimelineItem(state, nextMessageId)
|
const previousItem = useProxySelector(getTimelineItem, previousMessageId);
|
||||||
: undefined;
|
const nextItem = useProxySelector(getTimelineItem, nextMessageId);
|
||||||
|
|
||||||
const selectedMessage = getSelectedMessage(state);
|
const selectedMessage = useSelector(getSelectedMessage);
|
||||||
const isSelected = Boolean(
|
const isSelected = Boolean(
|
||||||
selectedMessage && messageId === selectedMessage.id
|
selectedMessage && messageId === selectedMessage.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const conversation = getConversationSelector(state)(conversationId);
|
|
||||||
|
|
||||||
const isNextItemCallingNotification = nextItem?.type === 'callHistory';
|
const isNextItemCallingNotification = nextItem?.type === 'callHistory';
|
||||||
|
|
||||||
const shouldCollapseAbove = areMessagesInSameGroup(
|
const shouldCollapseAbove = areMessagesInSameGroup(
|
||||||
|
@ -97,28 +103,96 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
!isSameDay(previousItem.timestamp, item.timestamp)
|
!isSameDay(previousItem.timestamp, item.timestamp)
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
const {
|
||||||
item,
|
blockGroupLinkRequests,
|
||||||
id: messageId,
|
clearSelectedMessage,
|
||||||
containerElementRef,
|
deleteMessage,
|
||||||
conversationId,
|
deleteMessageForEveryone,
|
||||||
conversationColor: conversation.conversationColor,
|
doubleCheckMissingQuoteReference,
|
||||||
customColor: conversation.customColor,
|
kickOffAttachmentDownload,
|
||||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
markAttachmentAsCorrupted,
|
||||||
isNextItemCallingNotification,
|
messageExpanded,
|
||||||
isSelected,
|
openGiftBadge,
|
||||||
renderContact,
|
pushPanelForConversation,
|
||||||
renderUniversalTimerNotification,
|
retryDeleteForEveryone,
|
||||||
shouldCollapseAbove,
|
retryMessageSend,
|
||||||
shouldCollapseBelow,
|
saveAttachment,
|
||||||
shouldHideMetadata,
|
selectMessage,
|
||||||
shouldRenderDateHeader,
|
showConversation,
|
||||||
i18n: getIntl(state),
|
showExpiredIncomingTapToViewToast,
|
||||||
interactionMode: getInteractionMode(state),
|
showExpiredOutgoingTapToViewToast,
|
||||||
theme: getTheme(state),
|
startConversation,
|
||||||
};
|
} = useConversationsActions();
|
||||||
};
|
|
||||||
|
|
||||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
const { reactToMessage, scrollToQuotedMessage, setQuoteByMessageId } =
|
||||||
|
useComposerActions();
|
||||||
|
|
||||||
export const SmartTimelineItem = smart(TimelineItem);
|
const {
|
||||||
|
showContactModal,
|
||||||
|
toggleForwardMessageModal,
|
||||||
|
toggleSafetyNumberModal,
|
||||||
|
} = useGlobalModalActions();
|
||||||
|
|
||||||
|
const { checkForAccount } = useAccountsActions();
|
||||||
|
|
||||||
|
const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions();
|
||||||
|
|
||||||
|
const { viewStory } = useStoriesActions();
|
||||||
|
|
||||||
|
const { returnToActiveCall, startCallingLobby } = useCallingActions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimelineItem
|
||||||
|
item={item}
|
||||||
|
id={messageId}
|
||||||
|
containerElementRef={containerElementRef}
|
||||||
|
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||||
|
conversationId={conversationId}
|
||||||
|
getPreferredBadge={getPreferredBadge}
|
||||||
|
isNextItemCallingNotification={isNextItemCallingNotification}
|
||||||
|
isSelected={isSelected}
|
||||||
|
renderAudioAttachment={renderAudioAttachment}
|
||||||
|
renderContact={renderContact}
|
||||||
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
|
renderReactionPicker={renderReactionPicker}
|
||||||
|
renderUniversalTimerNotification={renderUniversalTimerNotification}
|
||||||
|
shouldCollapseAbove={shouldCollapseAbove}
|
||||||
|
shouldCollapseBelow={shouldCollapseBelow}
|
||||||
|
shouldHideMetadata={shouldHideMetadata}
|
||||||
|
shouldRenderDateHeader={shouldRenderDateHeader}
|
||||||
|
i18n={i18n}
|
||||||
|
interactionMode={interactionMode}
|
||||||
|
theme={theme}
|
||||||
|
blockGroupLinkRequests={blockGroupLinkRequests}
|
||||||
|
checkForAccount={checkForAccount}
|
||||||
|
clearSelectedMessage={clearSelectedMessage}
|
||||||
|
deleteMessage={deleteMessage}
|
||||||
|
deleteMessageForEveryone={deleteMessageForEveryone}
|
||||||
|
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
||||||
|
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||||
|
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||||
|
messageExpanded={messageExpanded}
|
||||||
|
openGiftBadge={openGiftBadge}
|
||||||
|
pushPanelForConversation={pushPanelForConversation}
|
||||||
|
reactToMessage={reactToMessage}
|
||||||
|
retryDeleteForEveryone={retryDeleteForEveryone}
|
||||||
|
retryMessageSend={retryMessageSend}
|
||||||
|
returnToActiveCall={returnToActiveCall}
|
||||||
|
saveAttachment={saveAttachment}
|
||||||
|
scrollToQuotedMessage={scrollToQuotedMessage}
|
||||||
|
selectMessage={selectMessage}
|
||||||
|
setQuoteByMessageId={setQuoteByMessageId}
|
||||||
|
showContactModal={showContactModal}
|
||||||
|
showConversation={showConversation}
|
||||||
|
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
|
||||||
|
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
|
||||||
|
showLightbox={showLightbox}
|
||||||
|
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
|
||||||
|
startCallingLobby={startCallingLobby}
|
||||||
|
startConversation={startConversation}
|
||||||
|
toggleForwardMessageModal={toggleForwardMessageModal}
|
||||||
|
toggleSafetyNumberModal={toggleSafetyNumberModal}
|
||||||
|
viewStory={viewStory}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -123,7 +123,10 @@ void (async () => {
|
||||||
const leftPane = window.locator('.left-pane-wrapper');
|
const leftPane = window.locator('.left-pane-wrapper');
|
||||||
|
|
||||||
const item = leftPane
|
const item = leftPane
|
||||||
.locator('.module-conversation-list__item--contact-or-conversation')
|
.locator(
|
||||||
|
'.module-conversation-list__item--contact-or-conversation' +
|
||||||
|
`>> text=${LAST_MESSAGE}`
|
||||||
|
)
|
||||||
.first();
|
.first();
|
||||||
await item.click();
|
await item.click();
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ void (async () => {
|
||||||
{
|
{
|
||||||
const leftPane = window.locator('.left-pane-wrapper');
|
const leftPane = window.locator('.left-pane-wrapper');
|
||||||
const item = leftPane.locator(
|
const item = leftPane.locator(
|
||||||
`[data-testid="${first.toContact().uuid}"]`
|
`[data-testid="${first.toContact().uuid}"] >> text=${LAST_MESSAGE}`
|
||||||
);
|
);
|
||||||
await item.click();
|
await item.click();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { has } from 'lodash';
|
|
||||||
|
|
||||||
export function getOwn<TObject extends object, TKey extends keyof TObject>(
|
export function getOwn<TObject extends object, TKey extends keyof TObject>(
|
||||||
obj: TObject,
|
obj: TObject,
|
||||||
key: TKey
|
key: TKey
|
||||||
): TObject[TKey] | undefined {
|
): TObject[TKey] | undefined {
|
||||||
return has(obj, key) ? obj[key] : undefined;
|
return Object.hasOwn(obj, key) ? obj[key] : undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1705,10 +1705,10 @@
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
deepmerge "^4.2.2"
|
deepmerge "^4.2.2"
|
||||||
|
|
||||||
"@indutny/sneequals@3.2.0":
|
"@indutny/sneequals@4.0.0":
|
||||||
version "3.2.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@indutny/sneequals/-/sneequals-3.2.0.tgz#dd73d097fca6c8a89b1766bb87cc78f32017165d"
|
resolved "https://registry.yarnpkg.com/@indutny/sneequals/-/sneequals-4.0.0.tgz#94f74e577019759c5d12818e7c7ff1b9300653a4"
|
||||||
integrity sha512-dnL/SCNA2BceqJ4J/CR8R+dUCBfHnDBgPFBUv5w7Sa7QRoi2pNplJoundP9b8L8FnVrh4VAVPqM5q3H0f+n6Dg==
|
integrity sha512-kQUBQtcm4aVqJil+KRfA7SycJqcWlFEa7MJTYyl4XAahHOPXnzgqvlzUPQOw1tRFlvnzxRpXNUpJxej2fdAPjg==
|
||||||
|
|
||||||
"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3":
|
"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3":
|
||||||
version "0.1.3"
|
version "0.1.3"
|
||||||
|
|
Loading…
Reference in a new issue