From 1e275a917c19ababacf9057778290757ab6019c5 Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Fri, 15 Mar 2024 07:38:26 -0700 Subject: [PATCH] Always use static/cached selectors in useSelector --- ts/state/selectors/app.ts | 2 ++ ts/state/selectors/conversations.ts | 6 ++++++ ts/state/selectors/inbox.ts | 17 +++++++++++++++++ ts/state/selectors/safetyNumber.ts | 9 +++++++++ ts/state/smart/App.tsx | 6 +++--- ts/state/smart/ChatsTab.tsx | 16 ++++++++++------ ts/state/smart/CompositionArea.tsx | 17 ++++++++++++----- ts/state/smart/ConversationHeader.tsx | 25 +++++++++++-------------- ts/state/smart/ConversationView.tsx | 19 +++++-------------- ts/state/smart/DeleteMessagesModal.tsx | 18 +++++++++++------- ts/state/smart/Inbox.tsx | 20 +++++++++----------- ts/state/smart/SafetyNumberModal.tsx | 10 +++++----- ts/state/smart/SafetyNumberViewer.tsx | 10 +++++----- 13 files changed, 105 insertions(+), 70 deletions(-) create mode 100644 ts/state/selectors/inbox.ts diff --git a/ts/state/selectors/app.ts b/ts/state/selectors/app.ts index 79a5f80233..1ceb9364ed 100644 --- a/ts/state/selectors/app.ts +++ b/ts/state/selectors/app.ts @@ -10,3 +10,5 @@ export const getHasInitialLoadCompleted = createSelector( getApp, ({ hasInitialLoadCompleted }) => hasInitialLoadCompleted ); + +export const getAppView = createSelector(getApp, ({ appView }) => appView); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 8b7af75c71..e498027ad5 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -208,6 +208,12 @@ export const getTargetedMessage = createSelector( }; } ); +export const getTargetedMessageSource = createSelector( + getConversations, + (state: ConversationsStateType): string | undefined => { + return state.targetedMessageSource; + } +); export const getSelectedMessageIds = createSelector( getConversations, (state: ConversationsStateType): ReadonlyArray | undefined => { diff --git a/ts/state/selectors/inbox.ts b/ts/state/selectors/inbox.ts new file mode 100644 index 0000000000..d3f15b9de2 --- /dev/null +++ b/ts/state/selectors/inbox.ts @@ -0,0 +1,17 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { createSelector } from 'reselect'; +import type { StateType } from '../reducer'; + +const getInboxState = (state: StateType) => state.inbox; + +export const getInboxEnvelopeTimestamp = createSelector( + getInboxState, + ({ envelopeTimestamp }) => envelopeTimestamp +); + +export const getInboxFirstEnvelopeTimestamp = createSelector( + getInboxState, + ({ firstEnvelopeTimestamp }) => firstEnvelopeTimestamp +); diff --git a/ts/state/selectors/safetyNumber.ts b/ts/state/selectors/safetyNumber.ts index aa3fd0a479..d4083ef5ba 100644 --- a/ts/state/selectors/safetyNumber.ts +++ b/ts/state/selectors/safetyNumber.ts @@ -25,3 +25,12 @@ export const getContactSafetyNumber = createSelector( contactID: string ): SafetyNumberContactType | void => contacts[contactID] ); + +export const getContactSafetyNumberSelector = createSelector( + [getSafetyNumber], + ({ contacts }) => { + return (contactId: string) => { + return contacts[contactId]; + }; + } +); diff --git a/ts/state/smart/App.tsx b/ts/state/smart/App.tsx index 9243c8035d..fe7a82df73 100644 --- a/ts/state/smart/App.tsx +++ b/ts/state/smart/App.tsx @@ -16,20 +16,20 @@ import { getIsMainWindowFullScreen, } from '../selectors/user'; import { hasSelectedStoryData } from '../selectors/stories'; -import type { StateType } from '../reducer'; import { useAppActions } from '../ducks/app'; import { useConversationsActions } from '../ducks/conversations'; import { useStoriesActions } from '../ducks/stories'; import { ErrorBoundary } from '../../components/ErrorBoundary'; import { ModalContainer } from '../../components/ModalContainer'; import { SmartInbox } from './Inbox'; +import { getAppView } from '../selectors/app'; function renderInbox(): JSX.Element { return ; } export const SmartApp = memo(function SmartApp() { - const app = useSelector((state: StateType) => state.app); + const appView = useSelector(getAppView); const { openInbox } = useAppActions(); const { scrollToMessage } = useConversationsActions(); @@ -37,7 +37,7 @@ export const SmartApp = memo(function SmartApp() { return ( ; @@ -40,9 +44,9 @@ export const SmartChatsTab = memo(function SmartChatsTab() { const hasFailedStorySends = useSelector(getHasAnyFailedStorySends); const hasPendingUpdate = useSelector(getHasPendingUpdate); const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats); - - const { selectedConversationId, targetedMessage, targetedMessageSource } = - useSelector((state: StateType) => state.conversations); + const selectedConversationId = useSelector(getSelectedConversationId); + const targetedMessage = useSelector(getTargetedMessage); + const targetedMessageSource = useSelector(getTargetedMessageSource); const { onConversationClosed, @@ -60,14 +64,14 @@ export const SmartChatsTab = memo(function SmartChatsTab() { if (selectedConversationId !== lastOpenedConversationId.current) { lastOpenedConversationId.current = selectedConversationId; if (selectedConversationId) { - onConversationOpened(selectedConversationId, targetedMessage); + onConversationOpened(selectedConversationId, targetedMessage?.id); } } else if ( selectedConversationId && targetedMessage && targetedMessageSource !== TargetedMessageSource.Focus ) { - scrollToMessage(selectedConversationId, targetedMessage); + scrollToMessage(selectedConversationId, targetedMessage?.id); } }, [onConversationOpened, selectedConversationId, scrollToMessage, targetedMessage, targetedMessageSource]); diff --git a/ts/state/smart/CompositionArea.tsx b/ts/state/smart/CompositionArea.tsx index 638217fd40..4777d43c95 100644 --- a/ts/state/smart/CompositionArea.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -15,7 +15,6 @@ import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPe import { imageToBlurHash } from '../../util/imageToBlurHash'; import { isConversationSMSOnly } from '../../util/isConversationSMSOnly'; import { isSignalConversation } from '../../util/isSignalConversation'; -import type { StateType } from '../reducer'; import { getErrorDialogAudioRecorderType, getRecordingState, @@ -163,15 +162,23 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({ }; } - const quotedMessageProps = useSelector((state: StateType) => { + const ourConversationId = useSelector(getUserConversationId); + const defaultConversationColor = useSelector(getDefaultConversationColor); + + const quotedMessageProps = useMemo(() => { return quotedMessage ? getPropsForQuote(quotedMessage, { conversationSelector, - ourConversationId: getUserConversationId(state), - defaultConversationColor: getDefaultConversationColor(state), + ourConversationId, + defaultConversationColor, }) : undefined; - }); + }, [ + quotedMessage, + conversationSelector, + ourConversationId, + defaultConversationColor, + ]); const { putItem, removeItem } = useItemsActions(); diff --git a/ts/state/smart/ConversationHeader.tsx b/ts/state/smart/ConversationHeader.tsx index 78ccafe6ba..ba67253c13 100644 --- a/ts/state/smart/ConversationHeader.tsx +++ b/ts/state/smart/ConversationHeader.tsx @@ -5,7 +5,6 @@ import React, { memo, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { pick } from 'lodash'; import type { ConversationType } from '../ducks/conversations'; -import type { StateType } from '../reducer'; import { ConversationHeader, OutgoingCallButtonStyle, @@ -19,14 +18,13 @@ import { isMissingRequiredProfileSharing, } from '../selectors/conversations'; import { CallMode } from '../../types/Calling'; -import { getActiveCall, useCallingActions } from '../ducks/calling'; +import { useCallingActions } from '../ducks/calling'; import { isAnybodyElseInGroupCall } from '../ducks/callingHelpers'; import { getConversationCallMode, useConversationsActions, } from '../ducks/conversations'; import { getHasStoriesSelector } from '../selectors/stories2'; -import { getOwn } from '../../util/getOwn'; import { getUserACI, getIntl, getTheme } from '../selectors/user'; import { isConversationSMSOnly } from '../../util/isConversationSMSOnly'; import { missingCaseError } from '../../util/missingCaseError'; @@ -39,20 +37,21 @@ import { getGroupMemberships } from '../../util/getGroupMemberships'; import { isGroupOrAdhocCallState } from '../../util/isGroupOrAdhocCall'; import { useContactNameData } from '../../components/conversation/ContactName'; import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation'; +import { getActiveCallState, getCallSelector } from '../selectors/calling'; export type OwnProps = { id: string; }; -const getOutgoingCallButtonStyle = ( - conversation: ConversationType, - state: StateType +const useOutgoingCallButtonStyle = ( + conversation: ConversationType ): OutgoingCallButtonStyle => { - const { calling } = state; - const ourAci = getUserACI(state); - strictAssert(ourAci, 'getOutgoingCallButtonStyle missing our uuid'); + const ourAci = useSelector(getUserACI); + const activeCall = useSelector(getActiveCallState); + const callSelector = useSelector(getCallSelector); + strictAssert(ourAci, 'useOutgoingCallButtonStyle missing our uuid'); - if (getActiveCall(calling)) { + if (activeCall != null) { return OutgoingCallButtonStyle.None; } @@ -64,7 +63,7 @@ const getOutgoingCallButtonStyle = ( return OutgoingCallButtonStyle.Both; case CallMode.Group: case CallMode.Adhoc: { - const call = getOwn(calling.callsByConversation, conversation.id); + const call = callSelector(conversation.id); if ( isGroupOrAdhocCallState(call) && isAnybodyElseInGroupCall(call.peekInfo, ourAci) @@ -94,9 +93,7 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({ const badge = badgeSelector(conversation.badges); const i18n = useSelector(getIntl); const hasPanelShowing = useSelector(getHasPanelOpen); - const outgoingCallButtonStyle = useSelector((state: StateType) => { - return getOutgoingCallButtonStyle(conversation, state); - }); + const outgoingCallButtonStyle = useOutgoingCallButtonStyle(conversation); const theme = useSelector(getTheme); const { diff --git a/ts/state/smart/ConversationView.tsx b/ts/state/smart/ConversationView.tsx index 408b1b00b7..6f5fbf3a3e 100644 --- a/ts/state/smart/ConversationView.tsx +++ b/ts/state/smart/ConversationView.tsx @@ -3,7 +3,6 @@ import React, { memo, useCallback } from 'react'; import { useSelector } from 'react-redux'; -import type { StateType } from '../reducer'; import { ConversationPanel } from './ConversationPanel'; import { ConversationView } from '../../components/conversation/ConversationView'; import { SmartCompositionArea } from './CompositionArea'; @@ -17,6 +16,7 @@ import { } from '../selectors/conversations'; import { useComposerActions } from '../ducks/composer'; import { useConversationsActions } from '../ducks/conversations'; +import { isShowingAnyModal } from '../selectors/globalModals'; function renderCompositionArea(conversationId: string) { return ; @@ -48,19 +48,10 @@ export const SmartConversationView = memo( const { processAttachments } = useComposerActions(); - const hasOpenModal = useSelector((state: StateType) => { - return ( - state.globalModals.forwardMessagesProps != null || - state.globalModals.deleteMessagesProps != null || - state.globalModals.hasConfirmationModal - ); - }); - - const shouldHideConversationView = useSelector((state: StateType) => { - const activePanel = getActivePanel(state); - const isAnimating = getIsPanelAnimating(state); - return activePanel && !isAnimating; - }); + const hasOpenModal = useSelector(isShowingAnyModal); + const activePanel = useSelector(getActivePanel); + const isPanelAnimating = useSelector(getIsPanelAnimating); + const shouldHideConversationView = activePanel && !isPanelAnimating; const onExitSelectMode = useCallback(() => { toggleSelectMode(false); diff --git a/ts/state/smart/DeleteMessagesModal.tsx b/ts/state/smart/DeleteMessagesModal.tsx index bcba307354..c0ff0a08c4 100644 --- a/ts/state/smart/DeleteMessagesModal.tsx +++ b/ts/state/smart/DeleteMessagesModal.tsx @@ -1,7 +1,7 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import { useSelector } from 'react-redux'; import type { StateType } from '../reducer'; import { getIntl } from '../selectors/user'; @@ -25,13 +25,17 @@ export const SmartDeleteMessagesModal = memo( 'Cannot render delete messages modal without messages' ); const { conversationId, messageIds, onDelete } = deleteMessagesProps; - const isMe = useSelector((state: StateType) => { - return getConversationSelector(state)(conversationId).isMe; - }); + const conversationSelector = useSelector(getConversationSelector); + const conversation = conversationSelector(conversationId); + const { isMe } = conversation; - const canDeleteForEveryone = useSelector((state: StateType) => { - return canDeleteMessagesForEveryone(state, { messageIds, isMe }); - }); + const getCanDeleteForEveryone = useCallback( + (state: StateType) => { + return canDeleteMessagesForEveryone(state, { messageIds, isMe }); + }, + [messageIds, isMe] + ); + const canDeleteForEveryone = useSelector(getCanDeleteForEveryone); const lastSelectedMessage = useSelector(getLastSelectedMessage); const i18n = useSelector(getIntl); const { toggleDeleteMessagesModal } = useGlobalModalActions(); diff --git a/ts/state/smart/Inbox.tsx b/ts/state/smart/Inbox.tsx index d57c87cb2e..a7544c4a99 100644 --- a/ts/state/smart/Inbox.tsx +++ b/ts/state/smart/Inbox.tsx @@ -3,7 +3,6 @@ import React, { memo } from 'react'; import { useSelector } from 'react-redux'; -import type { StateType } from '../reducer'; import { Inbox } from '../../components/Inbox'; import { getIntl } from '../selectors/user'; import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal'; @@ -15,6 +14,11 @@ import { SmartCallsTab } from './CallsTab'; import { useItemsActions } from '../ducks/items'; import { getNavTabsCollapsed } from '../selectors/items'; import { SmartChatsTab } from './ChatsTab'; +import { getHasInitialLoadCompleted } from '../selectors/app'; +import { + getInboxEnvelopeTimestamp, + getInboxFirstEnvelopeTimestamp, +} from '../selectors/inbox'; function renderChatsTab() { return ; @@ -41,17 +45,11 @@ export const SmartInbox = memo(function SmartInbox(): JSX.Element { const isCustomizingPreferredReactions = useSelector( getIsCustomizingPreferredReactions ); - const envelopeTimestamp = useSelector( - (state: StateType) => state.inbox.envelopeTimestamp - ); - const firstEnvelopeTimestamp = useSelector( - (state: StateType) => state.inbox.firstEnvelopeTimestamp - ); - const { hasInitialLoadCompleted } = useSelector( - (state: StateType) => state.app - ); - + const envelopeTimestamp = useSelector(getInboxEnvelopeTimestamp); + const firstEnvelopeTimestamp = useSelector(getInboxFirstEnvelopeTimestamp); + const hasInitialLoadCompleted = useSelector(getHasInitialLoadCompleted); const navTabsCollapsed = useSelector(getNavTabsCollapsed); + const { toggleNavTabsCollapse } = useItemsActions(); return ( diff --git a/ts/state/smart/SafetyNumberModal.tsx b/ts/state/smart/SafetyNumberModal.tsx index 1df56761e8..954bf0ada3 100644 --- a/ts/state/smart/SafetyNumberModal.tsx +++ b/ts/state/smart/SafetyNumberModal.tsx @@ -3,8 +3,7 @@ import React, { memo } from 'react'; import { useSelector } from 'react-redux'; import { SafetyNumberModal } from '../../components/SafetyNumberModal'; -import type { StateType } from '../reducer'; -import { getContactSafetyNumber } from '../selectors/safetyNumber'; +import { getContactSafetyNumberSelector } from '../selectors/safetyNumber'; import { getConversationSelector } from '../selectors/conversations'; import { getIntl } from '../selectors/user'; import { useSafetyNumberActions } from '../ducks/safetyNumber'; @@ -20,9 +19,10 @@ export const SmartSafetyNumberModal = memo(function SmartSafetyNumberModal({ const i18n = useSelector(getIntl); const conversationSelector = useSelector(getConversationSelector); const contact = conversationSelector(contactID); - const contactSafetyNumber = useSelector((state: StateType) => { - return getContactSafetyNumber(state, { contactID }); - }); + const contactSafetyNumberSelector = useSelector( + getContactSafetyNumberSelector + ); + const contactSafetyNumber = contactSafetyNumberSelector(contactID); const { generateSafetyNumber, toggleVerified } = useSafetyNumberActions(); const { toggleSafetyNumberModal } = useGlobalModalActions(); return ( diff --git a/ts/state/smart/SafetyNumberViewer.tsx b/ts/state/smart/SafetyNumberViewer.tsx index 011e59ed52..6870917844 100644 --- a/ts/state/smart/SafetyNumberViewer.tsx +++ b/ts/state/smart/SafetyNumberViewer.tsx @@ -4,9 +4,8 @@ import React, { memo } from 'react'; import { useSelector } from 'react-redux'; import { SafetyNumberViewer } from '../../components/SafetyNumberViewer'; -import type { StateType } from '../reducer'; import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog'; -import { getContactSafetyNumber } from '../selectors/safetyNumber'; +import { getContactSafetyNumberSelector } from '../selectors/safetyNumber'; import { getConversationSelector } from '../selectors/conversations'; import { getIntl } from '../selectors/user'; import { useSafetyNumberActions } from '../ducks/safetyNumber'; @@ -16,9 +15,10 @@ export const SmartSafetyNumberViewer = memo(function SmartSafetyNumberViewer({ onClose, }: SafetyNumberProps) { const i18n = useSelector(getIntl); - const safetyNumberContact = useSelector((state: StateType) => { - return getContactSafetyNumber(state, { contactID }); - }); + const contactSafetyNumberSelector = useSelector( + getContactSafetyNumberSelector + ); + const safetyNumberContact = contactSafetyNumberSelector(contactID); const conversationSelector = useSelector(getConversationSelector); const contact = conversationSelector(contactID);