From f2cddb83c8618f8c59ea933fe3dfa07347e1a784 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 20 Jun 2023 11:28:48 +0200 Subject: [PATCH] chore: broke apart big Message selectors into smaller ones --- ts/components/avatar/Avatar.tsx | 18 +- .../message-content/MessageAuthorText.tsx | 25 +- .../message/message-content/MessageAvatar.tsx | 44 ++- .../message-content/MessageContent.tsx | 2 +- .../message-content/MessageLinkPreview.tsx | 17 +- .../message/message-content/MessageQuote.tsx | 10 +- .../message/message-content/MessageStatus.tsx | 9 +- .../message/message-item/MessageDetail.tsx | 10 +- ts/components/icon/SessionIcon.tsx | 4 +- .../ConversationListItem.tsx | 2 +- .../menu/ConversationListItemContextMenu.tsx | 8 +- ts/models/conversation.ts | 12 +- ts/models/message.ts | 11 +- .../conversations/ConversationController.ts | 5 +- ts/state/ducks/conversations.ts | 100 +++--- ts/state/ducks/sogsRoomInfo.tsx | 17 +- ts/state/selectors/conversations.ts | 293 ++++-------------- ts/state/selectors/index.ts | 2 + ts/state/selectors/messages.ts | 112 +++++++ ts/state/selectors/search.ts | 10 +- 20 files changed, 307 insertions(+), 404 deletions(-) create mode 100644 ts/state/selectors/messages.ts diff --git a/ts/components/avatar/Avatar.tsx b/ts/components/avatar/Avatar.tsx index e073ce270..0318dfedc 100644 --- a/ts/components/avatar/Avatar.tsx +++ b/ts/components/avatar/Avatar.tsx @@ -1,19 +1,18 @@ -import React, { useState } from 'react'; import classNames from 'classnames'; +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; +import { useDisableDrag } from '../../hooks/useDisableDrag'; import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch'; -import { isEqual } from 'lodash'; import { useAvatarPath, useConversationUsername, useIsClosedGroup, } from '../../hooks/useParamSelector'; +import { isMessageSelectionMode } from '../../state/selectors/conversations'; +import { SessionIcon } from '../icon'; import { AvatarPlaceHolder } from './AvatarPlaceHolder/AvatarPlaceHolder'; import { ClosedGroupAvatar } from './AvatarPlaceHolder/ClosedGroupAvatar'; -import { useDisableDrag } from '../../hooks/useDisableDrag'; -import styled from 'styled-components'; -import { SessionIcon } from '../icon'; -import { useSelector } from 'react-redux'; -import { isMessageSelectionMode } from '../../state/selectors/conversations'; export enum AvatarSize { XS = 28, @@ -90,7 +89,7 @@ const AvatarImage = (props: { datatestId?: string; handleImageError: () => any; }) => { - const { avatarPath, base64Data, name, imageBroken, datatestId, handleImageError } = props; + const { avatarPath, base64Data, imageBroken, datatestId, handleImageError } = props; const disableDrag = useDisableDrag(); @@ -103,7 +102,6 @@ const AvatarImage = (props: { {window.i18n('contactAvatarAlt', @@ -173,4 +171,4 @@ const AvatarInner = (props: Props) => { ); }; -export const Avatar = React.memo(AvatarInner, isEqual); +export const Avatar = AvatarInner; diff --git a/ts/components/conversation/message/message-content/MessageAuthorText.tsx b/ts/components/conversation/message/message-content/MessageAuthorText.tsx index b7a390507..a77d8342d 100644 --- a/ts/components/conversation/message/message-content/MessageAuthorText.tsx +++ b/ts/components/conversation/message/message-content/MessageAuthorText.tsx @@ -1,9 +1,13 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import styled from 'styled-components'; -import { MessageRenderingProps } from '../../../../models/messageType'; import { PubKey } from '../../../../session/types'; -import { getMessageAuthorProps } from '../../../../state/selectors/conversations'; +import { + useAuthorName, + useAuthorProfileName, + useFirstMessageOfSeries, + useMessageAuthor, + useMessageDirection, +} from '../../../../state/selectors'; import { useSelectedIsGroup, useSelectedIsPublic, @@ -11,11 +15,6 @@ import { import { Flex } from '../../../basic/Flex'; import { ContactName } from '../../ContactName'; -export type MessageAuthorSelectorProps = Pick< - MessageRenderingProps, - 'authorName' | 'authorProfileName' | 'sender' | 'direction' | 'firstMessageOfSeries' ->; - type Props = { messageId: string; }; @@ -25,15 +24,17 @@ const StyledAuthorContainer = styled(Flex)` `; export const MessageAuthorText = (props: Props) => { - const selected = useSelector(state => getMessageAuthorProps(state as any, props.messageId)); - const isPublic = useSelectedIsPublic(); const isGroup = useSelectedIsGroup(); + const authorProfileName = useAuthorProfileName(props.messageId); + const authorName = useAuthorName(props.messageId); + const sender = useMessageAuthor(props.messageId); + const direction = useMessageDirection(props.messageId); + const firstMessageOfSeries = useFirstMessageOfSeries(props.messageId); - if (!selected) { + if (!props.messageId || !sender || !direction) { return null; } - const { authorName, sender, authorProfileName, direction, firstMessageOfSeries } = selected; const title = authorName ? authorName : sender; diff --git a/ts/components/conversation/message/message-content/MessageAvatar.tsx b/ts/components/conversation/message/message-content/MessageAvatar.tsx index 378f99813..87f183983 100644 --- a/ts/components/conversation/message/message-content/MessageAvatar.tsx +++ b/ts/components/conversation/message/message-content/MessageAvatar.tsx @@ -9,10 +9,18 @@ import { getSodiumRenderer } from '../../../../session/crypto'; import { PubKey } from '../../../../session/types'; import { openConversationWithMessages } from '../../../../state/ducks/conversations'; import { updateUserDetailsModal } from '../../../../state/ducks/modalDialog'; -import { getMessageAvatarProps } from '../../../../state/selectors/conversations'; +import { + useAuthorAvatarPath, + useAuthorName, + useAuthorProfileName, + useLastMessageOfSeries, + useMessageAuthor, + useMessageSenderIsAdmin, +} from '../../../../state/selectors/'; import { getSelectedCanWrite, useSelectedConversationKey, + useSelectedIsPublic, } from '../../../../state/selectors/selectedConversation'; import { Avatar, AvatarSize, CrownIcon } from '../../../avatar/Avatar'; // tslint:disable: use-simple-attributes @@ -26,13 +34,7 @@ const StyledAvatar = styled.div` export type MessageAvatarSelectorProps = Pick< MessageRenderingProps, - | 'authorAvatarPath' - | 'authorName' - | 'sender' - | 'authorProfileName' - | 'isSenderAdmin' - | 'isPublic' - | 'lastMessageOfSeries' + 'sender' | 'isSenderAdmin' | 'lastMessageOfSeries' >; type Props = { messageId: string; noAvatar: boolean }; @@ -41,26 +43,18 @@ export const MessageAvatar = (props: Props) => { const { messageId, noAvatar } = props; const dispatch = useDispatch(); - const avatarProps = useSelector(state => getMessageAvatarProps(state as any, messageId)); const selectedConvoKey = useSelectedConversationKey(); const isTypingEnabled = useSelector(getSelectedCanWrite); - - if (!avatarProps) { - return null; - } - - const { - authorAvatarPath, - authorName, - sender, - authorProfileName, - isSenderAdmin, - lastMessageOfSeries, - isPublic, - } = avatarProps; - - if (noAvatar) { + const isPublic = useSelectedIsPublic(); + const authorName = useAuthorName(messageId); + const authorProfileName = useAuthorProfileName(messageId); + const authorAvatarPath = useAuthorAvatarPath(messageId); + const sender = useMessageAuthor(messageId); + const lastMessageOfSeries = useLastMessageOfSeries(messageId); + const isSenderAdmin = useMessageSenderIsAdmin(messageId); + + if (noAvatar || !sender) { return null; } diff --git a/ts/components/conversation/message/message-content/MessageContent.tsx b/ts/components/conversation/message/message-content/MessageContent.tsx index 13f23cc23..35dc7e646 100644 --- a/ts/components/conversation/message/message-content/MessageContent.tsx +++ b/ts/components/conversation/message/message-content/MessageContent.tsx @@ -10,8 +10,8 @@ import { getMessageContentSelectorProps, getQuotedMessageToAnimate, getShouldHighlightMessage, - useMessageIsDeleted, } from '../../../../state/selectors/conversations'; +import { useMessageIsDeleted } from '../../../../state/selectors'; import { ScrollToLoadedMessageContext } from '../../SessionMessagesListContainer'; import { MessageAttachment } from './MessageAttachment'; import { MessageLinkPreview } from './MessageLinkPreview'; diff --git a/ts/components/conversation/message/message-content/MessageLinkPreview.tsx b/ts/components/conversation/message/message-content/MessageLinkPreview.tsx index 6243ee330..164721ca2 100644 --- a/ts/components/conversation/message/message-content/MessageLinkPreview.tsx +++ b/ts/components/conversation/message/message-content/MessageLinkPreview.tsx @@ -4,12 +4,14 @@ import { isImageAttachment } from '../../../../types/Attachment'; import { Image } from '../../Image'; import { MessageRenderingProps } from '../../../../models/messageType'; import { useDispatch, useSelector } from 'react-redux'; -import { - getIsMessageSelectionMode, - getMessageLinkPreviewProps, -} from '../../../../state/selectors/conversations'; +import { getIsMessageSelectionMode } from '../../../../state/selectors/conversations'; import { SessionIcon } from '../../../icon'; import { showLinkVisitWarningDialog } from '../../../dialog/SessionConfirm'; +import { + useMessageAttachments, + useMessageDirection, + useMessageLinkPreview, +} from '../../../../state/selectors'; export type MessageLinkPreviewSelectorProps = Pick< MessageRenderingProps, @@ -24,14 +26,15 @@ type Props = { const linkPreviewsImageSize = 100; export const MessageLinkPreview = (props: Props) => { - const selected = useSelector(state => getMessageLinkPreviewProps(state as any, props.messageId)); const dispatch = useDispatch(); + const direction = useMessageDirection(props.messageId); + const attachments = useMessageAttachments(props.messageId); + const previews = useMessageLinkPreview(props.messageId); const isMessageSelectionMode = useSelector(getIsMessageSelectionMode); - if (!selected) { + if (!props.messageId) { return null; } - const { direction, attachments, previews } = selected; // Attachments take precedence over Link Previews if (attachments && attachments.length) { diff --git a/ts/components/conversation/message/message-content/MessageQuote.tsx b/ts/components/conversation/message/message-content/MessageQuote.tsx index 82c1d73d7..31991c690 100644 --- a/ts/components/conversation/message/message-content/MessageQuote.tsx +++ b/ts/components/conversation/message/message-content/MessageQuote.tsx @@ -5,7 +5,6 @@ import { MessageRenderingProps } from '../../../../models/messageType'; import { PubKey } from '../../../../session/types'; import { openConversationToSpecificMessage } from '../../../../state/ducks/conversations'; import { - getMessageQuoteProps, isMessageDetailView, isMessageSelectionMode, } from '../../../../state/selectors/conversations'; @@ -13,6 +12,7 @@ import { Quote } from './Quote'; import { ToastUtils } from '../../../../session/utils'; import { Data } from '../../../../data/data'; import { MessageModel } from '../../../../models/message'; +import { useMessageDirection, useMessageQuote } from '../../../../state/selectors'; // tslint:disable: use-simple-attributes @@ -23,13 +23,11 @@ type Props = { export type MessageQuoteSelectorProps = Pick; export const MessageQuote = (props: Props) => { - const selected = useSelector(state => getMessageQuoteProps(state as any, props.messageId)); + const quote = useMessageQuote(props.messageId); + const direction = useMessageDirection(props.messageId); const multiSelectMode = useSelector(isMessageSelectionMode); const isMessageDetailViewMode = useSelector(isMessageDetailView); - const quote = selected ? selected.quote : undefined; - const direction = selected ? selected.direction : undefined; - const onQuoteClick = useCallback( async (event: React.MouseEvent) => { event.preventDefault(); @@ -76,7 +74,7 @@ export const MessageQuote = (props: Props) => { }, [quote, multiSelectMode, props.messageId] ); - if (!selected) { + if (!props.messageId) { return null; } diff --git a/ts/components/conversation/message/message-content/MessageStatus.tsx b/ts/components/conversation/message/message-content/MessageStatus.tsx index be2182514..9ff95f502 100644 --- a/ts/components/conversation/message/message-content/MessageStatus.tsx +++ b/ts/components/conversation/message/message-content/MessageStatus.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { MessageRenderingProps } from '../../../../models/messageType'; -import { getMessageStatusProps } from '../../../../state/selectors/conversations'; import { OutgoingMessageStatus } from './OutgoingMessageStatus'; +import { useMessageDirection, useMessageStatus } from '../../../../state/selectors'; type Props = { isCorrectSide: boolean; @@ -14,12 +13,12 @@ export type MessageStatusSelectorProps = Pick { const { isCorrectSide, dataTestId } = props; + const direction = useMessageDirection(props.messageId); + const status = useMessageStatus(props.messageId); - const selected = useSelector(state => getMessageStatusProps(state as any, props.messageId)); - if (!selected) { + if (!props.messageId) { return null; } - const { status, direction } = selected; if (!isCorrectSide) { return null; diff --git a/ts/components/conversation/message/message-item/MessageDetail.tsx b/ts/components/conversation/message/message-item/MessageDetail.tsx index eb1f9b33c..4933ad74a 100644 --- a/ts/components/conversation/message/message-item/MessageDetail.tsx +++ b/ts/components/conversation/message/message-item/MessageDetail.tsx @@ -10,14 +10,12 @@ import { closeMessageDetailsView, ContactPropsMessageDetail, } from '../../../../state/ducks/conversations'; -import { - getMessageDetailsViewProps, - getMessageIsDeletable, -} from '../../../../state/selectors/conversations'; +import { getMessageDetailsViewProps } from '../../../../state/selectors/conversations'; import { ContactName } from '../../ContactName'; // tslint:disable-next-line: no-submodule-imports import useKey from 'react-use/lib/useKey'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../../../basic/SessionButton'; +import { useMessageIsDeletable } from '../../../../state/selectors'; const AvatarItem = (props: { pubkey: string }) => { const { pubkey } = props; @@ -98,9 +96,7 @@ export const MessageDetail = () => { const { i18n } = window; const messageDetailProps = useSelector(getMessageDetailsViewProps); - const isDeletable = useSelector(state => - getMessageIsDeletable(state as any, messageDetailProps?.messageId || '') - ); + const isDeletable = useMessageIsDeletable(messageDetailProps?.messageId); const dispatch = useDispatch(); diff --git a/ts/components/icon/SessionIcon.tsx b/ts/components/icon/SessionIcon.tsx index dda4a5741..967d69d03 100644 --- a/ts/components/icon/SessionIcon.tsx +++ b/ts/components/icon/SessionIcon.tsx @@ -122,7 +122,7 @@ const animation = (props: { }; //tslint:disable no-unnecessary-callback-wrapper -const Svg = React.memo(styled.svg` +const Svg = styled.svg` width: ${props => props.width}; transform: ${props => `rotate(${props.iconRotation}deg)`}; animation: ${props => animation(props)}; @@ -134,7 +134,7 @@ const Svg = React.memo(styled.svg` fill: ${props => (props.iconColor ? props.iconColor : '--button-icon-stroke-color')}; padding: ${props => (props.iconPadding ? props.iconPadding : '')}; transition: inherit; -`); +`; // tslint:enable no-unnecessary-callback-wrapper const SessionSvg = (props: { diff --git a/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx b/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx index ccd66c89e..b200ef693 100644 --- a/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx +++ b/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx @@ -140,4 +140,4 @@ const ConversationListItem = (props: Props) => { ); }; -export const MemoConversationListItemWithDetails = React.memo(ConversationListItem, _.isEqual); +export const MemoConversationListItemWithDetails = ConversationListItem; diff --git a/ts/components/menu/ConversationListItemContextMenu.tsx b/ts/components/menu/ConversationListItemContextMenu.tsx index 13450c1a6..c5ac79725 100644 --- a/ts/components/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/menu/ConversationListItemContextMenu.tsx @@ -77,13 +77,7 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) => ); }; -function propsAreEqual(prev: PropsContextConversationItem, next: PropsContextConversationItem) { - return _.isEqual(prev, next); -} -export const MemoConversationListItemContextMenu = React.memo( - ConversationListItemContextMenu, - propsAreEqual -); +export const MemoConversationListItemContextMenu = ConversationListItemContextMenu; export const PinConversationMenuItem = (): JSX.Element | null => { const conversationId = useConvoIdFromContext(); diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 6443020b0..0b58d801d 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -12,6 +12,7 @@ import { sortBy, throttle, uniq, + xor, } from 'lodash'; import { SignalService } from '../protobuf'; import { getMessageQueue } from '../session'; @@ -41,7 +42,6 @@ import { toHex } from '../session/utils/String'; import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout'; import { actions as conversationActions, - conversationChanged, conversationsChanged, markConversationFullyRead, MessageModelPropsWithoutConvoProps, @@ -163,9 +163,7 @@ export class ConversationModel extends Backbone.Model { this.typingRefreshTimer = null; this.typingPauseTimer = null; - window.inboxStore?.dispatch( - conversationChanged({ id: this.id, data: this.getConversationModelProps() }) - ); + window.inboxStore?.dispatch(conversationsChanged([this.getConversationModelProps()])); } public idForLogging() { @@ -394,13 +392,13 @@ export class ConversationModel extends Backbone.Model { * @returns true if the groupAdmins where not the same (and thus updated) */ public async updateGroupAdmins(groupAdmins: Array, shouldCommit: boolean) { - const sortedExistingAdmins = uniq(sortBy(this.getGroupAdmins())); const sortedNewAdmins = uniq(sortBy(groupAdmins)); - if (isEqual(sortedExistingAdmins, sortedNewAdmins)) { + // check if there is any difference betwewen the two, if yes, override it with what we got. + if (!xor(this.getGroupAdmins(), groupAdmins).length) { return false; } - this.set({ groupAdmins }); + this.set({ groupAdmins: sortedNewAdmins }); if (shouldCommit) { await this.commit(); } diff --git a/ts/models/message.ts b/ts/models/message.ts index c0281c84a..850a2ea67 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -77,6 +77,7 @@ import { PropsForGroupUpdateLeft, PropsForGroupUpdateName, PropsForMessageWithoutConvoProps, + ReduxQuoteType, } from '../state/ducks/conversations'; import { AttachmentTypeWithPath, isVoiceMessage } from '../types/Attachment'; import { getAttachmentMetadata } from '../types/message/initializeAttachmentMetadata'; @@ -615,15 +616,7 @@ export class MessageModel extends Backbone.Model { } const firstAttachment = quote.attachments && quote.attachments[0]; - const quoteProps: { - referencedMessageNotFound?: boolean; - sender: string; - messageId: string; - authorName: string; - text?: string; - attachment?: any; - isFromMe?: boolean; - } = { + const quoteProps: ReduxQuoteType = { sender: author, messageId: id, authorName: authorName || 'Unknown', diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 0dddc0e1a..602954e4d 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -432,10 +432,7 @@ export class ConversationController { this.conversations.remove(conversation); window?.inboxStore?.dispatch( - conversationActions.conversationChanged({ - id: convoId, - data: conversation.getConversationModelProps(), - }) + conversationActions.conversationsChanged([conversation.getConversationModelProps()]) ); } window.inboxStore?.dispatch(conversationActions.conversationRemoved(convoId)); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 6d01f18a4..08ef82769 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -161,6 +161,17 @@ export type PropsForAttachment = { } | null; }; +export type ReduxQuoteType = { + text?: string; + attachment?: QuotedAttachmentType; + isFromMe?: boolean; + sender: string; + authorProfileName?: string; + authorName?: string; + messageId?: string; + referencedMessageNotFound?: boolean; +} | null; + export type PropsForMessageWithoutConvoProps = { id: string; // messageId direction: MessageModelType; @@ -177,16 +188,7 @@ export type PropsForMessageWithoutConvoProps = { reacts?: ReactionList; reactsIndex?: number; previews?: Array; - quote?: { - text?: string; - attachment?: QuotedAttachmentType; - isFromMe?: boolean; - sender: string; - authorProfileName?: string; - authorName?: string; - messageId?: string; - referencedMessageNotFound?: boolean; - } | null; + quote?: ReduxQuoteType; messageHash?: string; isDeleted?: boolean; isUnread?: boolean; @@ -197,12 +199,8 @@ export type PropsForMessageWithoutConvoProps = { }; export type PropsForMessageWithConvoProps = PropsForMessageWithoutConvoProps & { - authorName: string | null; - authorProfileName: string | null; conversationType: ConversationTypeEnum; - authorAvatarPath: string | null; isPublic: boolean; - isOpenGroupV2: boolean; isKickedFromGroup: boolean; weAreAdmin: boolean; isSenderAdmin: boolean; @@ -634,16 +632,7 @@ const conversationsSlice = createSlice({ }, }; }, - conversationChanged( - state: ConversationsStateType, - action: PayloadAction<{ - id: string; - data: ReduxConversationType; - }> - ) { - const { payload } = action; - return applyConversationChanged(state, payload); - }, + conversationsChanged( state: ConversationsStateType, action: PayloadAction> @@ -652,12 +641,7 @@ const conversationsSlice = createSlice({ let updatedState = state; if (payload.length) { - payload.forEach(convoProps => { - updatedState = applyConversationChanged(updatedState, { - id: convoProps.id, - data: convoProps, - }); - }); + updatedState = applyConversationsChanged(updatedState, payload); } return updatedState; @@ -972,47 +956,47 @@ const conversationsSlice = createSlice({ }, }); -function applyConversationChanged( +function applyConversationsChanged( state: ConversationsStateType, - payload: { id: string; data: ReduxConversationType } + payload: Array ) { - const { id, data } = payload; const { conversationLookup, selectedConversation } = state; - const existing = conversationLookup[id]; - // In the change case we only modify the lookup if we already had that conversation - if (!existing) { - return state; - } + for (let index = 0; index < payload.length; index++) { + const convoProps = payload[index]; + const { id } = convoProps; + // In the `change` case we only modify the lookup if we already had that conversation + const existing = conversationLookup[id]; + + if (!existing) { + continue; + } + + if ( + state.selectedConversation && + convoProps.isPrivate && + convoProps.id === selectedConversation && + convoProps.priority && + convoProps.priority < CONVERSATION_PRIORITIES.default + ) { + // A private conversation hidden cannot be a selected. + // When opening a hidden conversation, we unhide it so it can be selected again. + state.selectedConversation = undefined; + } - let selected = selectedConversation; - if ( - data && - data.isPrivate && - data.id === selectedConversation && - data.priority && - data.priority < CONVERSATION_PRIORITIES.default - ) { - // A private conversation hidden cannot be a selected. - // When opening a hidden conversation, we unhide it so it can be selected again. - selected = undefined; + state.conversationLookup[id] = { + ...convoProps, + isInitialFetchingInProgress: existing.isInitialFetchingInProgress, + }; } - return { - ...state, - selectedConversation: selected, - conversationLookup: { - ...conversationLookup, - [id]: { ...data, isInitialFetchingInProgress: existing.isInitialFetchingInProgress }, - }, - }; + return state; } export const { actions, reducer } = conversationsSlice; export const { // conversation and messages list conversationAdded, - conversationChanged, conversationsChanged, conversationRemoved, removeAllConversations, diff --git a/ts/state/ducks/sogsRoomInfo.tsx b/ts/state/ducks/sogsRoomInfo.tsx index 84e9959c3..c1f915c09 100644 --- a/ts/state/ducks/sogsRoomInfo.tsx +++ b/ts/state/ducks/sogsRoomInfo.tsx @@ -1,9 +1,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { sortBy, uniq } from 'lodash'; +import { sortBy, uniq, xor } from 'lodash'; import { getCanWriteOutsideRedux, getCurrentSubscriberCountOutsideRedux, - getModeratorsOutsideRedux, } from '../selectors/sogsRoomInfo'; type RoomInfo = { @@ -53,8 +52,16 @@ const sogsRoomInfosSlice = createSlice({ }, setModerators(state, action: PayloadAction<{ convoId: string; moderators: Array }>) { addEmptyEntryIfNeeded(state, action.payload.convoId); + const existing = state.rooms[action.payload.convoId].moderators; + const newMods = sortBy(uniq(action.payload.moderators)); - state.rooms[action.payload.convoId].moderators = sortBy(uniq(action.payload.moderators)); + // check if there is any changes (order excluded) between those two arrays + const xord = xor(existing, newMods); + if (!xord.length) { + return state; + } + + state.rooms[action.payload.convoId].moderators = newMods; return state; }, @@ -93,10 +100,6 @@ function setCanWriteOutsideRedux(convoId: string, canWrite: boolean) { * @param moderators the updated list of moderators */ function setModeratorsOutsideRedux(convoId: string, moderators: Array) { - const currentMods = getModeratorsOutsideRedux(convoId); - if (sortBy(uniq(currentMods)) === sortBy(uniq(moderators))) { - return; - } window.inboxStore?.dispatch( setModerators({ convoId, diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 0829ee743..172469c4e 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -14,14 +14,9 @@ import { StateType } from '../reducer'; import { ReplyingToMessageProps } from '../../components/conversation/composition/CompositionBox'; import { MessageAttachmentSelectorProps } from '../../components/conversation/message/message-content/MessageAttachment'; -import { MessageAuthorSelectorProps } from '../../components/conversation/message/message-content/MessageAuthorText'; -import { MessageAvatarSelectorProps } from '../../components/conversation/message/message-content/MessageAvatar'; import { MessageContentSelectorProps } from '../../components/conversation/message/message-content/MessageContent'; import { MessageContentWithStatusSelectorProps } from '../../components/conversation/message/message-content/MessageContentWithStatus'; import { MessageContextMenuSelectorProps } from '../../components/conversation/message/message-content/MessageContextMenu'; -import { MessageLinkPreviewSelectorProps } from '../../components/conversation/message/message-content/MessageLinkPreview'; -import { MessageQuoteSelectorProps } from '../../components/conversation/message/message-content/MessageQuote'; -import { MessageStatusSelectorProps } from '../../components/conversation/message/message-content/MessageStatus'; import { MessageTextSelectorProps } from '../../components/conversation/message/message-content/MessageText'; import { GenericReadableMessageSelectorProps } from '../../components/conversation/message/message-item/GenericReadableMessage'; import { LightBoxOptions } from '../../components/conversation/SessionConversation'; @@ -40,21 +35,17 @@ import { getIntl } from './user'; import { filter, isEmpty, isNumber, pick, sortBy } from 'lodash'; import { MessageReactsSelectorProps } from '../../components/conversation/message/message-content/MessageReactions'; -import { getModeratorsOutsideRedux } from './sogsRoomInfo'; import { getSelectedConversation, getSelectedConversationKey } from './selectedConversation'; -import { useSelector } from 'react-redux'; +import { getModeratorsOutsideRedux } from './sogsRoomInfo'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; -export const getConversationLookup = createSelector( - getConversations, - (state: ConversationsStateType): ConversationLookupType => { - return state.conversationLookup; - } -); +export const getConversationLookup = (state: StateType): ConversationLookupType => { + return state.conversations.conversationLookup; +}; export const getConversationsCount = createSelector(getConversationLookup, (state): number => { - return Object.values(state).length; + return Object.keys(state).length; }); export const getOurPrimaryConversation = createSelector( @@ -63,10 +54,9 @@ export const getOurPrimaryConversation = createSelector( state.conversationLookup[Storage.get('primaryDevicePubKey') as string] ); -const getMessagesOfSelectedConversation = createSelector( - getConversations, - (state: ConversationsStateType): Array => state.messages -); +const getMessagesOfSelectedConversation = ( + state: StateType +): Array => state.conversations.messages; // Redux recommends to do filtered and deriving state in a selector rather than ourself export const getSortedMessagesOfSelectedConversation = createSelector( @@ -97,12 +87,9 @@ export const hasSelectedConversationIncomingMessages = createSelector( } ); -export const getFirstUnreadMessageId = createSelector( - getConversations, - (state: ConversationsStateType): string | undefined => { - return state.firstUnreadMessageId; - } -); +export const getFirstUnreadMessageId = (state: StateType): string | undefined => { + return state.conversations.firstUnreadMessageId; +}; export type MessagePropsType = | 'group-notification' @@ -489,76 +476,47 @@ export const getGlobalUnreadMessageCount = createSelector(getLeftPaneLists, (sta return state.globalUnreadCount; }); -export const isMessageDetailView = createSelector( - getConversations, - (state: ConversationsStateType): boolean => state.messageDetailProps !== undefined -); +export const isMessageDetailView = (state: StateType): boolean => + state.conversations.messageDetailProps !== undefined; -export const getMessageDetailsViewProps = createSelector( - getConversations, - (state: ConversationsStateType): MessagePropsDetails | undefined => state.messageDetailProps -); +export const getMessageDetailsViewProps = (state: StateType): MessagePropsDetails | undefined => + state.conversations.messageDetailProps; -export const isRightPanelShowing = createSelector( - getConversations, - (state: ConversationsStateType): boolean => state.showRightPanel -); +export const isRightPanelShowing = (state: StateType): boolean => + state.conversations.showRightPanel; -export const isMessageSelectionMode = createSelector( - getConversations, - (state: ConversationsStateType): boolean => Boolean(state.selectedMessageIds.length > 0) -); +export const isMessageSelectionMode = (state: StateType): boolean => + state.conversations.selectedMessageIds.length > 0; -export const getSelectedMessageIds = createSelector( - getConversations, - (state: ConversationsStateType): Array => state.selectedMessageIds -); +export const getSelectedMessageIds = (state: StateType): Array => + state.conversations.selectedMessageIds; -export const getIsMessageSelectionMode = createSelector( - getSelectedMessageIds, - (state: Array): boolean => Boolean(state.length) -); +export const getIsMessageSelectionMode = (state: StateType): boolean => + Boolean(getSelectedMessageIds(state).length); -export const getLightBoxOptions = createSelector( - getConversations, - (state: ConversationsStateType): LightBoxOptions | undefined => state.lightBox -); +export const getLightBoxOptions = (state: StateType): LightBoxOptions | undefined => + state.conversations.lightBox; -export const getQuotedMessage = createSelector( - getConversations, - (state: ConversationsStateType): ReplyingToMessageProps | undefined => state.quotedMessage -); +export const getQuotedMessage = (state: StateType): ReplyingToMessageProps | undefined => + state.conversations.quotedMessage; -export const areMoreMessagesBeingFetched = createSelector( - getConversations, - (state: ConversationsStateType): boolean => state.areMoreMessagesBeingFetched || false -); +export const areMoreMessagesBeingFetched = (state: StateType): boolean => + state.conversations.areMoreMessagesBeingFetched || false; -export const getShowScrollButton = createSelector( - getConversations, - (state: ConversationsStateType): boolean => state.showScrollButton || false -); +export const getShowScrollButton = (state: StateType): boolean => + state.conversations.showScrollButton || false; -export const getQuotedMessageToAnimate = createSelector( - getConversations, - (state: ConversationsStateType): string | undefined => state.animateQuotedMessageId || undefined -); +export const getQuotedMessageToAnimate = (state: StateType): string | undefined => + state.conversations.animateQuotedMessageId || undefined; -export const getShouldHighlightMessage = createSelector( - getConversations, - (state: ConversationsStateType): boolean => - Boolean(state.animateQuotedMessageId && state.shouldHighlightMessage) -); +export const getShouldHighlightMessage = (state: StateType): boolean => + Boolean(state.conversations.animateQuotedMessageId && state.conversations.shouldHighlightMessage); -export const getNextMessageToPlayId = createSelector( - getConversations, - (state: ConversationsStateType): string | undefined => state.nextMessageToPlayId || undefined -); +export const getNextMessageToPlayId = (state: StateType): string | undefined => + state.conversations.nextMessageToPlayId || undefined; -export const getMentionsInput = createSelector( - getConversations, - (state: ConversationsStateType): MentionsMembersType => state.mentionMembers -); +export const getMentionsInput = (state: StateType): MentionsMembersType => + state.conversations.mentionMembers; /// Those calls are just related to ordering messages in the redux store. @@ -621,12 +579,9 @@ function sortMessages( * This returns the most recent message id in the database. This is not the most recent message shown, * but the most recent one, which could still not be loaded. */ -export const getMostRecentMessageId = createSelector( - getConversations, - (state: ConversationsStateType): string | null => { - return state.mostRecentMessageId; - } -); +export const getMostRecentMessageId = (state: StateType): string | null => { + return state.conversations.mostRecentMessageId; +}; export const getOldestMessageId = createSelector( getSortedMessagesOfSelectedConversation, @@ -674,20 +629,22 @@ export const isFirstUnreadMessageIdAbove = createSelector( } ); -const getMessageId = (_whatever: any, id: string) => id; +const getMessageId = (_whatever: any, id: string | undefined) => id; // tslint:disable: cyclomatic-complexity export const getMessagePropsByMessageId = createSelector( getSortedMessagesOfSelectedConversation, - getConversationLookup, + getSelectedConversation, getMessageId, - ( messages: Array, - conversations, + selectedConvo, id ): MessageModelPropsWithConvoProps | undefined => { + if (!id) { + return undefined; + } const foundMessageProps: SortedMessageModelProps | undefined = messages?.find( m => m?.propsForMessage?.id === id ); @@ -697,27 +654,20 @@ export const getMessagePropsByMessageId = createSelector( } const sender = foundMessageProps?.propsForMessage?.sender; - // foundMessageConversation is the conversation this message is - const foundMessageConversation = conversations[foundMessageProps.propsForMessage.convoId]; - if (!foundMessageConversation || !sender) { - return undefined; - } - - const foundSenderConversation = conversations[sender]; - if (!foundSenderConversation) { + // we can only show messages when the convo is selected. + if (!selectedConvo || !sender) { return undefined; } const ourPubkey = UserUtils.getOurPubKeyStrFromCache(); - const isGroup = !foundMessageConversation.isPrivate; - const isPublic = foundMessageConversation.isPublic; + const isGroup = !selectedConvo.isPrivate; + const isPublic = selectedConvo.isPublic; - const groupAdmins = (isGroup && foundMessageConversation.groupAdmins) || []; + const groupAdmins = (isGroup && selectedConvo.groupAdmins) || []; const weAreAdmin = groupAdmins.includes(ourPubkey) || false; const weAreModerator = - (isPublic && getModeratorsOutsideRedux(foundMessageConversation.id).includes(ourPubkey)) || - false; + (isPublic && getModeratorsOutsideRedux(selectedConvo.id).includes(ourPubkey)) || false; // A message is deletable if // either we sent it, // or the convo is not a public one (in this case, we will only be able to delete for us) @@ -732,33 +682,20 @@ export const getMessagePropsByMessageId = createSelector( sender === ourPubkey || (isPublic && (weAreAdmin || weAreModerator)) || false; const isSenderAdmin = groupAdmins.includes(sender); - const senderIsUs = sender === ourPubkey; - - const authorName = - foundSenderConversation.nickname || foundSenderConversation.displayNameInProfile || null; - const authorProfileName = senderIsUs - ? window.i18n('you') - : foundSenderConversation.nickname || - foundSenderConversation.displayNameInProfile || - window.i18n('anonymous'); const messageProps: MessageModelPropsWithConvoProps = { ...foundMessageProps, propsForMessage: { ...foundMessageProps.propsForMessage, - isBlocked: !!foundMessageConversation.isBlocked, + isBlocked: !!selectedConvo.isBlocked, isPublic: !!isPublic, - isOpenGroupV2: !!isPublic, isSenderAdmin, isDeletable, isDeletableForEveryone, weAreAdmin, - conversationType: foundMessageConversation.type, + conversationType: selectedConvo.type, sender, - authorAvatarPath: foundSenderConversation.avatarPath || null, - isKickedFromGroup: foundMessageConversation.isKickedFromGroup || false, - authorProfileName: authorProfileName || 'Unknown', - authorName, + isKickedFromGroup: selectedConvo.isKickedFromGroup || false, }, }; @@ -766,30 +703,6 @@ export const getMessagePropsByMessageId = createSelector( } ); -export const getMessageAvatarProps = createSelector(getMessagePropsByMessageId, (props): - | MessageAvatarSelectorProps - | undefined => { - if (!props || isEmpty(props)) { - return undefined; - } - - const messageAvatarProps: MessageAvatarSelectorProps = { - lastMessageOfSeries: props.lastMessageOfSeries, - ...pick(props.propsForMessage, [ - 'authorAvatarPath', - 'authorName', - 'sender', - 'authorProfileName', - 'conversationType', - 'direction', - 'isPublic', - 'isSenderAdmin', - ]), - }; - - return messageAvatarProps; -}); - export const getMessageReactsProps = createSelector(getMessagePropsByMessageId, (props): | MessageReactsSelectorProps | undefined => { @@ -800,7 +713,6 @@ export const getMessageReactsProps = createSelector(getMessagePropsByMessageId, const msgProps: MessageReactsSelectorProps = pick(props.propsForMessage, [ 'convoId', 'conversationType', - 'isPublic', 'reacts', 'serverId', ]); @@ -825,46 +737,6 @@ export const getMessageReactsProps = createSelector(getMessagePropsByMessageId, return msgProps; }); -export const getMessageLinkPreviewProps = createSelector(getMessagePropsByMessageId, (props): - | MessageLinkPreviewSelectorProps - | undefined => { - if (!props || isEmpty(props)) { - return undefined; - } - - const msgProps: MessageLinkPreviewSelectorProps = pick(props.propsForMessage, [ - 'direction', - 'attachments', - 'previews', - ]); - - return msgProps; -}); - -export const getMessageQuoteProps = createSelector(getMessagePropsByMessageId, (props): - | MessageQuoteSelectorProps - | undefined => { - if (!props || isEmpty(props)) { - return undefined; - } - - const msgProps: MessageQuoteSelectorProps = pick(props.propsForMessage, ['direction', 'quote']); - - return msgProps; -}); - -export const getMessageStatusProps = createSelector(getMessagePropsByMessageId, (props): - | MessageStatusSelectorProps - | undefined => { - if (!props || isEmpty(props)) { - return undefined; - } - - const msgProps: MessageStatusSelectorProps = pick(props.propsForMessage, ['direction', 'status']); - - return msgProps; -}); - export const getMessageTextProps = createSelector(getMessagePropsByMessageId, (props): | MessageTextSelectorProps | undefined => { @@ -883,11 +755,6 @@ export const getMessageTextProps = createSelector(getMessagePropsByMessageId, (p return msgProps; }); -export const useMessageIsDeleted = (messageId: string): boolean => { - const props = useSelector((state: StateType) => getMessagePropsByMessageId(state, messageId)); - return props?.propsForMessage.isDeleted || false; -}; - /** * TODO probably not something which should be memoized with createSelector as we rememoize it for each message (and override the previous one). Not sure what is the right way to do a lookup. But maybe something like having the messages as a record and do a simple lookup to grab the details. * And the the sorting would be done in a memoized selector @@ -915,32 +782,6 @@ export const getMessageContextMenuProps = createSelector(getMessagePropsByMessag return msgProps; }); -export const getMessageAuthorProps = createSelector(getMessagePropsByMessageId, (props): - | MessageAuthorSelectorProps - | undefined => { - if (!props || isEmpty(props)) { - return undefined; - } - - const msgProps: MessageAuthorSelectorProps = { - firstMessageOfSeries: props.firstMessageOfSeries, - ...pick(props.propsForMessage, ['authorName', 'sender', 'authorProfileName', 'direction']), - }; - - return msgProps; -}); - -export const getMessageIsDeletable = createSelector( - getMessagePropsByMessageId, - (props): boolean => { - if (!props || isEmpty(props)) { - return false; - } - - return props.propsForMessage.isDeletable; - } -); - export const getMessageAttachmentProps = createSelector(getMessagePropsByMessageId, (props): | MessageAttachmentSelectorProps | undefined => { @@ -1038,22 +879,14 @@ export const getGenericReadableMessageSelectorProps = createSelector( } ); -export const getOldTopMessageId = createSelector( - getConversations, - (state: ConversationsStateType): string | null => state.oldTopMessageId || null -); +export const getOldTopMessageId = (state: StateType): string | null => + state.conversations.oldTopMessageId || null; -// TODOLATER get rid of all the unneeded createSelector calls +export const getOldBottomMessageId = (state: StateType): string | null => + state.conversations.oldBottomMessageId || null; -export const getOldBottomMessageId = createSelector( - getConversations, - (state: ConversationsStateType): string | null => state.oldBottomMessageId || null -); - -export const getIsSelectedConvoInitialLoadingInProgress = createSelector( - getSelectedConversation, - (convo: ReduxConversationType | undefined): boolean => Boolean(convo?.isInitialFetchingInProgress) -); +export const getIsSelectedConvoInitialLoadingInProgress = (state: StateType): boolean => + Boolean(getSelectedConversation(state)?.isInitialFetchingInProgress); export function getCurrentlySelectedConversationOutsideRedux() { return window?.inboxStore?.getState().conversations.selectedConversation as string | undefined; diff --git a/ts/state/selectors/index.ts b/ts/state/selectors/index.ts index 42b6bfca9..304e6986a 100644 --- a/ts/state/selectors/index.ts +++ b/ts/state/selectors/index.ts @@ -25,3 +25,5 @@ export { UserConfigSelectors, UserSelectors, }; + +export * from './messages'; diff --git a/ts/state/selectors/messages.ts b/ts/state/selectors/messages.ts new file mode 100644 index 000000000..5f4618f8b --- /dev/null +++ b/ts/state/selectors/messages.ts @@ -0,0 +1,112 @@ +import { useSelector } from 'react-redux'; +import { UserUtils } from '../../session/utils'; +import { + MessageModelPropsWithConvoProps, + ReduxConversationType, + PropsForAttachment, + ReduxQuoteType, + LastMessageStatusType, +} from '../ducks/conversations'; +import { StateType } from '../reducer'; +import { getMessagePropsByMessageId } from './conversations'; + +const useMessageIdProps = (messageId: string | undefined) => { + return useSelector((state: StateType) => getMessagePropsByMessageId(state, messageId)); +}; + +const useSenderConvoProps = ( + msgProps: MessageModelPropsWithConvoProps | undefined +): ReduxConversationType | undefined => { + return useSelector((state: StateType) => { + const sender = msgProps?.propsForMessage.sender; + if (!sender) { + return undefined; + } + return state.conversations.conversationLookup[sender] || undefined; + }); +}; + +export const useAuthorProfileName = (messageId: string): string | null => { + const msg = useMessageIdProps(messageId); + const senderProps = useSenderConvoProps(msg); + if (!msg || !senderProps) { + return null; + } + + const senderIsUs = msg.propsForMessage.sender === UserUtils.getOurPubKeyStrFromCache(); + + const authorProfileName = senderIsUs + ? window.i18n('you') + : senderProps.nickname || senderProps.displayNameInProfile || window.i18n('anonymous'); + return authorProfileName || window.i18n('unknown'); +}; + +export const useAuthorName = (messageId: string): string | null => { + const msg = useMessageIdProps(messageId); + const senderProps = useSenderConvoProps(msg); + if (!msg || !senderProps) { + return null; + } + + const authorName = senderProps.nickname || senderProps.displayNameInProfile || null; + return authorName; +}; + +export const useAuthorAvatarPath = (messageId: string): string | null => { + const msg = useMessageIdProps(messageId); + const senderProps = useSenderConvoProps(msg); + if (!msg || !senderProps) { + return null; + } + + return senderProps.avatarPath || null; +}; + +export const useMessageIsDeleted = (messageId: string): boolean => { + const props = useMessageIdProps(messageId); + return props?.propsForMessage.isDeleted || false; +}; + +export const useFirstMessageOfSeries = (messageId: string | undefined): boolean => { + return useMessageIdProps(messageId)?.firstMessageOfSeries || false; +}; + +export const useLastMessageOfSeries = (messageId: string | undefined): boolean => { + return useMessageIdProps(messageId)?.lastMessageOfSeries || false; +}; + +export const useMessageAuthor = (messageId: string | undefined): string | undefined => { + return useMessageIdProps(messageId)?.propsForMessage.sender; +}; + +export const useMessageDirection = (messageId: string | undefined): string | undefined => { + return useMessageIdProps(messageId)?.propsForMessage.direction; +}; + +export const useMessageLinkPreview = (messageId: string | undefined): any[] | undefined => { + return useMessageIdProps(messageId)?.propsForMessage.previews; +}; + +export const useMessageAttachments = ( + messageId: string | undefined +): Array | undefined => { + return useMessageIdProps(messageId)?.propsForMessage.attachments; +}; + +export const useMessageSenderIsAdmin = (messageId: string | undefined): boolean => { + return useMessageIdProps(messageId)?.propsForMessage.isSenderAdmin || false; +}; + +export const useMessageIsDeletable = (messageId: string | undefined): boolean => { + return useMessageIdProps(messageId)?.propsForMessage.isDeletable || false; +}; + +export const useMessageQuote = (messageId: string | undefined): ReduxQuoteType | undefined => { + return useMessageIdProps(messageId)?.propsForMessage.quote; +}; + +export const useMessageStatus = ( + messageId: string | undefined +): LastMessageStatusType | undefined => { + return useMessageIdProps(messageId)?.propsForMessage.status; +}; diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts index 45206f698..c0758fed2 100644 --- a/ts/state/selectors/search.ts +++ b/ts/state/selectors/search.ts @@ -10,13 +10,11 @@ import { getSelectedConversationKey } from './selectedConversation'; export const getSearch = (state: StateType): SearchStateType => state.search; -export const getQuery = createSelector(getSearch, (state: SearchStateType): string => state.query); +export const getQuery = (state: StateType): string => getSearch(state).query; -export const isSearching = createSelector(getSearch, (state: SearchStateType) => { - const { query } = state; - - return Boolean(query && query.trim().length > 1); -}); +export const isSearching = (state: StateType) => { + return !!getSearch(state)?.query?.trim(); +}; export const getSearchResults = createSelector( [getSearch, getConversationLookup, getSelectedConversationKey],