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

@ -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",

View 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]
)
);
}

View file

@ -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

View file

@ -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,11 +268,10 @@ 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;
@ -298,14 +296,12 @@ export const getAttachmentsForMessage = memoize(
.filter(attachment => !attachment.error || canBeDownloaded(attachment)) .filter(attachment => !attachment.error || canBeDownloaded(attachment))
.map(attachment => getPropsForAttachment(attachment)) .map(attachment => getPropsForAttachment(attachment))
.filter(isNotNil); .filter(isNotNil);
} };
);
export const processBodyRanges = memoize( export const processBodyRanges = (
(
{ bodyRanges }: Pick<MessageWithUIFieldsType, 'bodyRanges'>, { bodyRanges }: Pick<MessageWithUIFieldsType, 'bodyRanges'>,
options: { conversationSelector: GetConversationByIdType } options: { conversationSelector: GetConversationByIdType }
): HydratedBodyRangesType | undefined => { ): HydratedBodyRangesType | undefined => {
if (!bodyRanges) { if (!bodyRanges) {
return undefined; return undefined;
} }
@ -323,8 +319,7 @@ export const processBodyRanges = memoize(
}; };
}) })
.sort((a, b) => b.start - a.start); .sort((a, b) => b.start - a.start);
} };
);
const getAuthorForMessage = ( const getAuthorForMessage = (
message: MessageWithUIFieldsType, message: MessageWithUIFieldsType,
@ -482,8 +477,7 @@ const getPropsForStoryReplyContext = (
}; };
}; };
export const getPropsForQuote = memoize( export const getPropsForQuote = (
(
message: Pick< message: Pick<
MessageWithUIFieldsType, MessageWithUIFieldsType,
'conversationId' | 'quote' | 'payment' 'conversationId' | 'quote' | 'payment'
@ -495,7 +489,7 @@ export const getPropsForQuote = memoize(
conversationSelector: GetConversationByIdType; conversationSelector: GetConversationByIdType;
ourConversationId?: string; ourConversationId?: string;
} }
): PropsData['quote'] => { ): PropsData['quote'] => {
const { quote } = message; const { quote } = message;
if (!quote) { if (!quote) {
return undefined; return undefined;
@ -548,8 +542,7 @@ export const getPropsForQuote = memoize(
sentAt: Number(sentAt), sentAt: Number(sentAt),
text, text,
}; };
} };
);
export type GetPropsForMessageOptions = Pick< export type GetPropsForMessageOptions = Pick<
GetPropsForBubbleOptions, GetPropsForBubbleOptions,
@ -632,11 +625,10 @@ 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);
@ -673,8 +665,7 @@ export const getPropsForMessage = memoize(
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, {
@ -737,8 +728,7 @@ export const getPropsForMessage = memoize(
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,

View file

@ -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,8 +21,21 @@ import {
import { getActiveCall, getCallSelector } from './calling'; import { getActiveCall, getCallSelector } from './calling';
import { getPropsForBubble } from './message'; import { getPropsForBubble } from './message';
const getTimelineItemInner = memoize( export const getTimelineItem = (
(message: MessageWithUIFieldsType, state: StateType): TimelineItemType => { state: StateType,
id?: string
): TimelineItemType | undefined => {
if (id === undefined) {
return undefined;
}
const messageLookup = getMessages(state);
const message = messageLookup[id];
if (!message) {
return undefined;
}
const selectedMessage = getSelectedMessage(state); const selectedMessage = getSelectedMessage(state);
const conversationSelector = getConversationSelector(state); const conversationSelector = getConversationSelector(state);
const regionCode = getRegionCode(state); const regionCode = getRegionCode(state);
@ -51,19 +62,4 @@ const getTimelineItemInner = memoize(
activeCall, activeCall,
accountSelector, accountSelector,
}); });
}
);
export const getTimelineItem = (
state: StateType,
id: string
): TimelineItemType | undefined => {
const messageLookup = getMessages(state);
const message = messageLookup[id];
if (!message) {
return undefined;
}
return getTimelineItemInner(message, state);
}; };

View file

@ -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}
/> />
); );

View file

@ -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}
/>
);
}

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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;
} }

View file

@ -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"