From 00503524701b5cc12b3dba514498768fb1237bc8 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 24 Mar 2023 16:48:50 +1100 Subject: [PATCH] chore: move selected convo selectors to another file --- .../calling/DraggableCallContainer.tsx | 4 +- .../conversation/ConversationHeader.tsx | 60 +++--- .../ConversationRequestButtons.tsx | 16 +- .../conversation/ConversationRequestInfo.tsx | 10 +- .../conversation/SessionMessagesList.tsx | 10 +- .../SessionMessagesListContainer.tsx | 6 +- .../conversation/SessionRightPanel.tsx | 85 ++++---- .../conversation/StagedAttachmentList.tsx | 22 +-- .../composition/CompositionBox.tsx | 15 +- .../media-gallery/DocumentListItem.tsx | 11 +- .../message-content/MessageAuthorText.tsx | 13 +- .../message/message-content/MessageAvatar.tsx | 16 +- .../message-content/MessageReactions.tsx | 15 +- .../message/message-content/Quote.tsx | 16 +- .../message/message-item/ReadableMessage.tsx | 4 +- .../notification-bubble/CallNotification.tsx | 15 +- .../message/reactions/Reaction.tsx | 2 +- ts/components/dialog/ReactListModal.tsx | 6 +- .../ConversationListItem.tsx | 11 +- .../overlay/OverlayMessageRequest.tsx | 10 +- ts/components/lightbox/LightboxGallery.tsx | 12 +- ts/components/menu/ConversationHeaderMenu.tsx | 5 +- ts/hooks/useParamSelector.ts | 12 +- ts/hooks/useVideoEventListener.ts | 4 +- ts/interactions/conversationInteractions.ts | 5 +- ts/models/message.ts | 4 +- ts/receiver/configMessage.ts | 105 ++++------ .../conversations/ConversationController.ts | 36 ++-- ts/state/selectors/call.ts | 3 +- ts/state/selectors/conversations.ts | 171 +--------------- ts/state/selectors/search.ts | 3 +- ts/state/selectors/selectedConversation.ts | 185 ++++++++++++++++++ ts/state/selectors/stagedAttachments.ts | 2 +- ts/state/smart/SessionConversation.ts | 14 +- 34 files changed, 464 insertions(+), 444 deletions(-) create mode 100644 ts/state/selectors/selectedConversation.ts diff --git a/ts/components/calling/DraggableCallContainer.tsx b/ts/components/calling/DraggableCallContainer.tsx index 0e44059a3..67564682e 100644 --- a/ts/components/calling/DraggableCallContainer.tsx +++ b/ts/components/calling/DraggableCallContainer.tsx @@ -3,7 +3,6 @@ import { useSelector } from 'react-redux'; import Draggable, { DraggableData, DraggableEvent } from 'react-draggable'; import styled from 'styled-components'; -import { getSelectedConversationKey } from '../../state/selectors/conversations'; import { getHasOngoingCall, getHasOngoingCallWith } from '../../state/selectors/call'; import { openConversationWithMessages } from '../../state/ducks/conversations'; import { Avatar, AvatarSize } from '../avatar/Avatar'; @@ -11,6 +10,7 @@ import { useVideoCallEventsListener } from '../../hooks/useVideoEventListener'; import { VideoLoadingSpinner } from './InConversationCallContainer'; import { getSection } from '../../state/selectors/section'; import { SectionType } from '../../state/ducks/section'; +import { useSelectedConversationKey } from '../../state/selectors/selectedConversation'; export const DraggableCallWindow = styled.div` position: absolute; @@ -58,7 +58,7 @@ const CenteredAvatarInDraggable = styled.div` export const DraggableCallContainer = () => { const ongoingCallProps = useSelector(getHasOngoingCallWith); - const selectedConversationKey = useSelector(getSelectedConversationKey); + const selectedConversationKey = useSelectedConversationKey(); const hasOngoingCall = useSelector(getHasOngoingCall); const selectedSection = useSelector(getSection); diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 9404bf774..9a62986ff 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -6,15 +6,6 @@ import { contextMenu } from 'react-contexify'; import styled from 'styled-components'; import { ConversationNotificationSettingType } from '../../models/conversationAttributes'; import { - getConversationHeaderTitleProps, - getCurrentNotificationSettingText, - getCurrentSubscriberCount, - getIsSelectedActive, - getIsSelectedBlocked, - getIsSelectedNoteToSelf, - getIsSelectedPrivate, - getSelectedConversationIsPublic, - getSelectedConversationKey, getSelectedMessageIds, isMessageDetailView, isMessageSelectionMode, @@ -50,6 +41,19 @@ import { SessionIconButton } from '../icon'; import { ConversationHeaderMenu } from '../menu/ConversationHeaderMenu'; import { Flex } from '../basic/Flex'; import { ExpirationTimerOptions } from '../../util/expiringMessages'; +import { + useSelectedConversationKey, + useSelectedIsActive, + useSelectedIsBlocked, + useSelectedIsGroup, + useSelectedIsKickedFromGroup, + useSelectedisNoteToSelf, + useSelectedIsPrivate, + useSelectedIsPublic, + useSelectedMembers, + useSelectedNotificationSetting, + useSelectedSubscriberCount, +} from '../../state/selectors/selectedConversation'; export interface TimerOption { name: string; @@ -58,8 +62,8 @@ export interface TimerOption { const SelectionOverlay = () => { const selectedMessageIds = useSelector(getSelectedMessageIds); - const selectedConversationKey = useSelector(getSelectedConversationKey); - const isPublic = useSelector(getSelectedConversationIsPublic); + const selectedConversationKey = useSelectedConversationKey(); + const isPublic = useSelectedIsPublic(); const dispatch = useDispatch(); const { i18n } = window; @@ -200,11 +204,11 @@ const BackButton = (props: { onGoBack: () => void; showBackButton: boolean }) => }; const CallButton = () => { - const isPrivate = useSelector(getIsSelectedPrivate); - const isBlocked = useSelector(getIsSelectedBlocked); - const activeAt = useSelector(getIsSelectedActive); - const isMe = useSelector(getIsSelectedNoteToSelf); - const selectedConvoKey = useSelector(getSelectedConversationKey); + const isPrivate = useSelectedIsPrivate(); + const isBlocked = useSelectedIsBlocked(); + const activeAt = useSelectedIsActive(); + const isMe = useSelectedisNoteToSelf(); + const selectedConvoKey = useSelectedConversationKey(); const hasIncomingCall = useSelector(getHasIncomingCall); const hasOngoingCall = useSelector(getHasOngoingCall); @@ -266,18 +270,22 @@ export const ConversationHeaderSubtitle = (props: { text?: string | null }): JSX const ConversationHeaderTitle = () => { const dispatch = useDispatch(); - const headerTitleProps = useSelector(getConversationHeaderTitleProps); - const notificationSetting = useSelector(getCurrentNotificationSettingText); + const notificationSetting = useSelectedNotificationSetting(); const isRightPanelOn = useSelector(isRightPanelShowing); - const convoName = useConversationUsername(headerTitleProps?.conversationKey); - const subscriberCount = useSelector(getCurrentSubscriberCount); + const subscriberCount = useSelectedSubscriberCount(); + const selectedConvoKey = useSelectedConversationKey(); + const convoName = useConversationUsername(selectedConvoKey); + + const isPublic = useSelectedIsPublic(); + const isKickedFromGroup = useSelectedIsKickedFromGroup(); + const isMe = useSelectedisNoteToSelf(); + const isGroup = useSelectedIsGroup(); + const members = useSelectedMembers(); - if (!headerTitleProps) { + if (!selectedConvoKey) { return null; } - const { isGroup, isPublic, members, isMe, isKickedFromGroup } = headerTitleProps; - const { i18n } = window; if (isMe) { @@ -331,15 +339,15 @@ const ConversationHeaderTitle = () => { export const ConversationHeaderWithDetails = () => { const isSelectionMode = useSelector(isMessageSelectionMode); const isMessageDetailOpened = useSelector(isMessageDetailView); - const selectedConvoKey = useSelector(getSelectedConversationKey); + const selectedConvoKey = useSelectedConversationKey(); const dispatch = useDispatch(); + const isKickedFromGroup = useIsKickedFromGroup(selectedConvoKey); + const expireTimerSetting = useExpireTimer(selectedConvoKey); if (!selectedConvoKey) { return null; } - const isKickedFromGroup = useIsKickedFromGroup(selectedConvoKey); - const expireTimerSetting = useExpireTimer(selectedConvoKey); const expirationSettingName = expireTimerSetting ? ExpirationTimerOptions.getName(expireTimerSetting || 0) : undefined; diff --git a/ts/components/conversation/ConversationRequestButtons.tsx b/ts/components/conversation/ConversationRequestButtons.tsx index 1e08f65b3..911eb598b 100644 --- a/ts/components/conversation/ConversationRequestButtons.tsx +++ b/ts/components/conversation/ConversationRequestButtons.tsx @@ -7,10 +7,8 @@ import { declineConversationWithConfirm, } from '../../interactions/conversationInteractions'; import { getConversationController } from '../../session/conversations'; -import { - getSelectedConversation, - hasSelectedConversationIncomingMessages, -} from '../../state/selectors/conversations'; +import { hasSelectedConversationIncomingMessages } from '../../state/selectors/conversations'; +import { useSelectedConversationKey } from '../../state/selectors/selectedConversation'; import { SessionButton, SessionButtonColor } from '../basic/SessionButton'; const handleDeclineConversationRequest = (convoId: string) => { @@ -44,12 +42,12 @@ const ConversationBannerRow = styled.div` `; export const ConversationMessageRequestButtons = () => { - const selectedConversation = useSelector(getSelectedConversation); + const selectedConvoId = useSelectedConversationKey(); const hasIncomingMessages = useSelector(hasSelectedConversationIncomingMessages); - const isIncomingMessageRequest = useIsRequest(selectedConversation?.id); + const isIncomingMessageRequest = useIsRequest(selectedConvoId); - if (!selectedConversation || !hasIncomingMessages) { + if (!selectedConvoId || !hasIncomingMessages) { return null; } @@ -62,7 +60,7 @@ export const ConversationMessageRequestButtons = () => { { - await handleAcceptConversationRequest(selectedConversation.id); + await handleAcceptConversationRequest(selectedConvoId); }} text={window.i18n('accept')} dataTestId="accept-message-request" @@ -71,7 +69,7 @@ export const ConversationMessageRequestButtons = () => { buttonColor={SessionButtonColor.Danger} text={window.i18n('decline')} onClick={() => { - handleDeclineConversationRequest(selectedConversation.id); + handleDeclineConversationRequest(selectedConvoId); }} dataTestId="decline-message-request" /> diff --git a/ts/components/conversation/ConversationRequestInfo.tsx b/ts/components/conversation/ConversationRequestInfo.tsx index d9853e787..15972ac81 100644 --- a/ts/components/conversation/ConversationRequestInfo.tsx +++ b/ts/components/conversation/ConversationRequestInfo.tsx @@ -2,10 +2,8 @@ import React from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { useIsRequest } from '../../hooks/useParamSelector'; -import { - getSelectedConversation, - hasSelectedConversationIncomingMessages, -} from '../../state/selectors/conversations'; +import { hasSelectedConversationIncomingMessages } from '../../state/selectors/conversations'; +import { useSelectedConversationKey } from '../../state/selectors/selectedConversation'; const ConversationRequestTextBottom = styled.div` display: flex; @@ -22,8 +20,8 @@ const ConversationRequestTextInner = styled.div` `; export const ConversationRequestinfo = () => { - const selectedConversation = useSelector(getSelectedConversation); - const isIncomingMessageRequest = useIsRequest(selectedConversation?.id); + const selectedConversation = useSelectedConversationKey(); + const isIncomingMessageRequest = useIsRequest(selectedConversation); const showMsgRequestUI = selectedConversation && isIncomingMessageRequest; const hasIncomingMessages = useSelector(hasSelectedConversationIncomingMessages); diff --git a/ts/components/conversation/SessionMessagesList.tsx b/ts/components/conversation/SessionMessagesList.tsx index 6feb5d4dd..457c705d2 100644 --- a/ts/components/conversation/SessionMessagesList.tsx +++ b/ts/components/conversation/SessionMessagesList.tsx @@ -15,19 +15,19 @@ import { import { getOldBottomMessageId, getOldTopMessageId, - getSelectedConversationKey, getSortedMessagesTypesOfSelectedConversation, } from '../../state/selectors/conversations'; -import { GroupUpdateMessage } from './message/message-item/GroupUpdateMessage'; -import { MessageRequestResponse } from './message/message-item/MessageRequestResponse'; import { MessageDateBreak } from './message/message-item/DateBreak'; import { GroupInvitation } from './message/message-item/GroupInvitation'; +import { GroupUpdateMessage } from './message/message-item/GroupUpdateMessage'; import { Message } from './message/message-item/Message'; +import { MessageRequestResponse } from './message/message-item/MessageRequestResponse'; import { CallNotification } from './message/message-item/notification-bubble/CallNotification'; +import { useSelectedConversationKey } from '../../state/selectors/selectedConversation'; +import { DataExtractionNotification } from './message/message-item/DataExtractionNotification'; import { SessionLastSeenIndicator } from './SessionLastSeenIndicator'; import { TimerNotification } from './TimerNotification'; -import { DataExtractionNotification } from './message/message-item/DataExtractionNotification'; function isNotTextboxEvent(e: KeyboardEvent) { return (e?.target as any)?.type === undefined; @@ -46,7 +46,7 @@ export const SessionMessagesList = (props: { onEndPressed: () => void; }) => { const messagesProps = useSelector(getSortedMessagesTypesOfSelectedConversation); - const convoKey = useSelector(getSelectedConversationKey); + const convoKey = useSelectedConversationKey(); const [didScroll, setDidScroll] = useState(false); const oldTopMessageId = useSelector(getOldTopMessageId); diff --git a/ts/components/conversation/SessionMessagesListContainer.tsx b/ts/components/conversation/SessionMessagesListContainer.tsx index 5d97d2ea4..23effa227 100644 --- a/ts/components/conversation/SessionMessagesListContainer.tsx +++ b/ts/components/conversation/SessionMessagesListContainer.tsx @@ -17,12 +17,14 @@ import { import { StateType } from '../../state/reducer'; import { getQuotedMessageToAnimate, - getSelectedConversation, - getSelectedConversationKey, getSortedMessagesOfSelectedConversation, } from '../../state/selectors/conversations'; import { TypingBubble } from './TypingBubble'; import styled from 'styled-components'; +import { + getSelectedConversation, + getSelectedConversationKey, +} from '../../state/selectors/selectedConversation'; export type SessionMessageListProps = { messageContainerRef: React.RefObject; diff --git a/ts/components/conversation/SessionRightPanel.tsx b/ts/components/conversation/SessionRightPanel.tsx index 2927fbe89..3e48cb468 100644 --- a/ts/components/conversation/SessionRightPanel.tsx +++ b/ts/components/conversation/SessionRightPanel.tsx @@ -17,11 +17,7 @@ import { } from '../../interactions/conversationInteractions'; import { Constants } from '../../session'; import { closeRightPanel } from '../../state/ducks/conversations'; -import { - getCurrentSubscriberCount, - getSelectedConversation, - isRightPanelShowing, -} from '../../state/selectors/conversations'; +import { isRightPanelShowing } from '../../state/selectors/conversations'; import { getTimerOptions } from '../../state/selectors/timerOptions'; import { AttachmentTypeWithPath } from '../../types/Attachment'; import { Avatar, AvatarSize } from '../avatar/Avatar'; @@ -32,6 +28,18 @@ import { MediaItemType } from '../lightbox/LightboxGallery'; import { MediaGallery } from './media-gallery/MediaGallery'; import { getAbsoluteAttachmentPath } from '../../types/MessageAttachment'; import styled from 'styled-components'; +import { + useSelectedConversationKey, + useSelectedDisplayNameInProfile, + useSelectedIsActive, + useSelectedIsBlocked, + useSelectedIsGroup, + useSelectedIsKickedFromGroup, + useSelectedIsLeft, + useSelectedIsPublic, + useSelectedSubscriberCount, + useSelectedWeAreAdmin, +} from '../../state/selectors/selectedConversation'; async function getMediaGalleryProps( conversationId: string @@ -107,13 +115,16 @@ async function getMediaGalleryProps( } const HeaderItem = () => { - const selectedConversation = useSelector(getSelectedConversation); + const selectedConvoKey = useSelectedConversationKey(); const dispatch = useDispatch(); + const isBlocked = useSelectedIsBlocked(); + const isKickedFromGroup = useSelectedIsKickedFromGroup(); + const left = useSelectedIsLeft(); + const isGroup = useSelectedIsGroup(); - if (!selectedConversation) { + if (!selectedConvoKey) { return null; } - const { id, isGroup, isKickedFromGroup, isBlocked, left } = selectedConversation; const showInviteContacts = isGroup && !isKickedFromGroup && !isBlocked && !left; @@ -129,14 +140,14 @@ const HeaderItem = () => { style={{ position: 'absolute' }} dataTestId="back-button-conversation-options" /> - + {showInviteContacts && ( { - if (selectedConversation) { - showInviteContactByConvoId(selectedConversation.id); + if (selectedConvoKey) { + showInviteContactByConvoId(selectedConvoKey); } }} dataTestId="add-user-button" @@ -191,15 +202,24 @@ export const SessionRightPanelWithDetails = () => { const [documents, setDocuments] = useState>([]); const [media, setMedia] = useState>([]); - const selectedConversation = useSelector(getSelectedConversation); + const selectedConvoKey = useSelectedConversationKey(); const isShowing = useSelector(isRightPanelShowing); - const subscriberCount = useSelector(getCurrentSubscriberCount); + const subscriberCount = useSelectedSubscriberCount(); + + const isActive = useSelectedIsActive(); + const displayNameInProfile = useSelectedDisplayNameInProfile(); + const isBlocked = useSelectedIsBlocked(); + const isKickedFromGroup = useSelectedIsKickedFromGroup(); + const left = useSelectedIsLeft(); + const isGroup = useSelectedIsGroup(); + const isPublic = useSelectedIsPublic(); + const weAreAdmin = useSelectedWeAreAdmin(); useEffect(() => { let isRunning = true; - if (isShowing && selectedConversation) { - void getMediaGalleryProps(selectedConversation.id).then(results => { + if (isShowing && selectedConvoKey) { + void getMediaGalleryProps(selectedConvoKey).then(results => { if (isRunning) { if (!_.isEqual(documents, results.documents)) { setDocuments(results.documents); @@ -216,11 +236,11 @@ export const SessionRightPanelWithDetails = () => { isRunning = false; return; }; - }, [isShowing, selectedConversation?.id]); + }, [isShowing, selectedConvoKey]); useInterval(async () => { - if (isShowing && selectedConversation) { - const results = await getMediaGalleryProps(selectedConversation.id); + if (isShowing && selectedConvoKey) { + const results = await getMediaGalleryProps(selectedConvoKey); if (results.documents.length !== documents.length || results.media.length !== media.length) { setDocuments(results.documents); setMedia(results.media); @@ -228,23 +248,12 @@ export const SessionRightPanelWithDetails = () => { } }, 10000); - if (!selectedConversation) { + if (!selectedConvoKey) { return null; } - const { - id, - displayNameInProfile, - isKickedFromGroup, - left, - isPublic, - weAreAdmin, - isBlocked, - isGroup, - activeAt, - } = selectedConversation; const showMemberCount = !!(subscriberCount && subscriberCount > 0); - const commonNoShow = isKickedFromGroup || left || isBlocked || !activeAt; + const commonNoShow = isKickedFromGroup || left || isBlocked || !isActive; const hasDisappearingMessages = !isPublic && !commonNoShow; const leaveGroupString = isPublic ? window.i18n('leaveGroup') @@ -260,7 +269,7 @@ export const SessionRightPanelWithDetails = () => { return { content: option.name, onClick: () => { - void setDisappearingMessagesByConvoId(id, option.value); + void setDisappearingMessagesByConvoId(selectedConvoKey, option.value); }, }; }); @@ -272,10 +281,10 @@ export const SessionRightPanelWithDetails = () => { const deleteConvoAction = isPublic ? () => { - deleteAllMessagesByConvoIdWithConfirmation(id); + deleteAllMessagesByConvoIdWithConfirmation(selectedConvoKey); // TODO this does not delete the public group and showLeaveGroupByConvoId is not only working for closed groups } : () => { - showLeaveGroupByConvoId(id); + showLeaveGroupByConvoId(selectedConvoKey); }; return (
@@ -295,7 +304,7 @@ export const SessionRightPanelWithDetails = () => { className="group-settings-item" role="button" onClick={async () => { - await showUpdateGroupNameByConvoId(id); + await showUpdateGroupNameByConvoId(selectedConvoKey); }} > {isPublic ? window.i18n('editGroup') : window.i18n('editGroupName')} @@ -307,7 +316,7 @@ export const SessionRightPanelWithDetails = () => { className="group-settings-item" role="button" onClick={() => { - showAddModeratorsByConvoId(id); + showAddModeratorsByConvoId(selectedConvoKey); }} > {window.i18n('addModerators')} @@ -316,7 +325,7 @@ export const SessionRightPanelWithDetails = () => { className="group-settings-item" role="button" onClick={() => { - showRemoveModeratorsByConvoId(id); + showRemoveModeratorsByConvoId(selectedConvoKey); }} > {window.i18n('removeModerators')} @@ -329,7 +338,7 @@ export const SessionRightPanelWithDetails = () => { className="group-settings-item" role="button" onClick={async () => { - await showUpdateGroupMembersByConvoId(id); + await showUpdateGroupMembersByConvoId(selectedConvoKey); }} > {window.i18n('groupMembers')} diff --git a/ts/components/conversation/StagedAttachmentList.tsx b/ts/components/conversation/StagedAttachmentList.tsx index ebd2c7aa6..e2c280818 100644 --- a/ts/components/conversation/StagedAttachmentList.tsx +++ b/ts/components/conversation/StagedAttachmentList.tsx @@ -1,21 +1,21 @@ import React from 'react'; -import { isImageTypeSupported, isVideoTypeSupported } from '../../util/GoogleChrome'; -import { Image } from './Image'; -import { StagedGenericAttachment } from './StagedGenericAttachment'; -import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment'; +import { useDispatch } from 'react-redux'; +import { + removeAllStagedAttachmentsInConversation, + removeStagedAttachmentInConversation, +} from '../../state/ducks/stagedAttachments'; +import { useSelectedConversationKey } from '../../state/selectors/selectedConversation'; import { areAllAttachmentsVisual, AttachmentType, getUrl, isVideoAttachment, } from '../../types/Attachment'; -import { useDispatch, useSelector } from 'react-redux'; -import { - removeAllStagedAttachmentsInConversation, - removeStagedAttachmentInConversation, -} from '../../state/ducks/stagedAttachments'; -import { getSelectedConversationKey } from '../../state/selectors/conversations'; +import { isImageTypeSupported, isVideoTypeSupported } from '../../util/GoogleChrome'; +import { Image } from './Image'; +import { StagedGenericAttachment } from './StagedGenericAttachment'; +import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment'; type Props = { attachments: Array; @@ -30,7 +30,7 @@ export const StagedAttachmentList = (props: Props) => { const { attachments, onAddAttachment, onClickAttachment } = props; const dispatch = useDispatch(); - const conversationKey = useSelector(getSelectedConversationKey); + const conversationKey = useSelectedConversationKey(); const onRemoveAllStaged = () => { if (!conversationKey) { diff --git a/ts/components/conversation/composition/CompositionBox.tsx b/ts/components/conversation/composition/CompositionBox.tsx index 448c3f437..2a18a6b50 100644 --- a/ts/components/conversation/composition/CompositionBox.tsx +++ b/ts/components/conversation/composition/CompositionBox.tsx @@ -31,13 +31,7 @@ import { ToastUtils } from '../../../session/utils'; import { ReduxConversationType } from '../../../state/ducks/conversations'; import { removeAllStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments'; import { StateType } from '../../../state/reducer'; -import { - getIsTypingEnabled, - getMentionsInput, - getQuotedMessage, - getSelectedConversation, - getSelectedConversationKey, -} from '../../../state/selectors/conversations'; +import { getMentionsInput, getQuotedMessage } from '../../../state/selectors/conversations'; import { AttachmentUtil } from '../../../util'; import { Flex } from '../../basic/Flex'; import { CaptionEditor } from '../../CaptionEditor'; @@ -57,6 +51,11 @@ import { renderEmojiQuickResultRow, searchEmojiForQuery } from './EmojiQuickResu import { LinkPreviews } from '../../../util/linkPreviews'; import styled from 'styled-components'; import { FixedBaseEmoji } from '../../../types/Reaction'; +import { + getSelectedCanWrite, + getSelectedConversation, + getSelectedConversationKey, +} from '../../../state/selectors/selectedConversation'; export interface ReplyingToMessageProps { convoId: string; @@ -1084,7 +1083,7 @@ const mapStateToProps = (state: StateType) => { quotedMessageProps: getQuotedMessage(state), selectedConversation: getSelectedConversation(state), selectedConversationKey: getSelectedConversationKey(state), - typingEnabled: getIsTypingEnabled(state), + typingEnabled: getSelectedCanWrite(state), }; }; diff --git a/ts/components/conversation/media-gallery/DocumentListItem.tsx b/ts/components/conversation/media-gallery/DocumentListItem.tsx index 72b22dbc8..20cdfb249 100644 --- a/ts/components/conversation/media-gallery/DocumentListItem.tsx +++ b/ts/components/conversation/media-gallery/DocumentListItem.tsx @@ -1,13 +1,12 @@ -import React, { useCallback } from 'react'; import classNames from 'classnames'; +import React, { useCallback } from 'react'; import moment from 'moment'; // tslint:disable-next-line:match-default-export-name import formatFileSize from 'filesize'; -import { useSelector } from 'react-redux'; -import { getSelectedConversationKey } from '../../../state/selectors/conversations'; import { saveAttachmentToDisk } from '../../../util/attachmentsUtil'; import { MediaItemType } from '../../lightbox/LightboxGallery'; +import { useSelectedConversationKey } from '../../../state/selectors/selectedConversation'; type Props = { // Required @@ -24,7 +23,11 @@ export const DocumentListItem = (props: Props) => { const { shouldShowSeparator, fileName, fileSize, timestamp } = props; const defaultShowSeparator = shouldShowSeparator === undefined ? true : shouldShowSeparator; - const selectedConversationKey = useSelector(getSelectedConversationKey) as string; + const selectedConversationKey = useSelectedConversationKey(); + + if (!selectedConversationKey) { + throw new Error('DocumentListItem: selectedConversationKey was not set'); + } const saveAttachmentCallback = useCallback(() => { void saveAttachmentToDisk({ diff --git a/ts/components/conversation/message/message-content/MessageAuthorText.tsx b/ts/components/conversation/message/message-content/MessageAuthorText.tsx index 69fb0bedb..b7a390507 100644 --- a/ts/components/conversation/message/message-content/MessageAuthorText.tsx +++ b/ts/components/conversation/message/message-content/MessageAuthorText.tsx @@ -3,11 +3,11 @@ 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 { - getMessageAuthorProps, - getSelectedConversationIsGroup, - isPublicGroupConversation, -} from '../../../../state/selectors/conversations'; + useSelectedIsGroup, + useSelectedIsPublic, +} from '../../../../state/selectors/selectedConversation'; import { Flex } from '../../../basic/Flex'; import { ContactName } from '../../ContactName'; @@ -27,8 +27,9 @@ const StyledAuthorContainer = styled(Flex)` export const MessageAuthorText = (props: Props) => { const selected = useSelector(state => getMessageAuthorProps(state as any, props.messageId)); - const isPublic = useSelector(isPublicGroupConversation); - const isGroup = useSelector(getSelectedConversationIsGroup); + const isPublic = useSelectedIsPublic(); + const isGroup = useSelectedIsGroup(); + if (!selected) { return null; } diff --git a/ts/components/conversation/message/message-content/MessageAvatar.tsx b/ts/components/conversation/message/message-content/MessageAvatar.tsx index 62728c906..bd06b5b78 100644 --- a/ts/components/conversation/message/message-content/MessageAvatar.tsx +++ b/ts/components/conversation/message/message-content/MessageAvatar.tsx @@ -8,12 +8,12 @@ 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 { - getIsTypingEnabled, - getMessageAvatarProps, - getSelectedConversationIsGroup, - getSelectedConversationKey, -} from '../../../../state/selectors/conversations'; + getSelectedCanWrite, + useSelectedConversationKey, + useSelectedIsGroup, +} from '../../../../state/selectors/selectedConversation'; import { Avatar, AvatarSize, CrownIcon } from '../../../avatar/Avatar'; // tslint:disable: use-simple-attributes @@ -37,10 +37,10 @@ export const MessageAvatar = (props: Props) => { const dispatch = useDispatch(); const avatarProps = useSelector(state => getMessageAvatarProps(state as any, messageId)); - const selectedConvoKey = useSelector(getSelectedConversationKey); - const isSelectedGroup = useSelector(getSelectedConversationIsGroup); + const selectedConvoKey = useSelectedConversationKey(); + const isSelectedGroup = useSelectedIsGroup(); - const isTypingEnabled = useSelector(getIsTypingEnabled); + const isTypingEnabled = useSelector(getSelectedCanWrite); if (!avatarProps) { return null; diff --git a/ts/components/conversation/message/message-content/MessageReactions.tsx b/ts/components/conversation/message/message-content/MessageReactions.tsx index 9f7cf3299..404211d53 100644 --- a/ts/components/conversation/message/message-content/MessageReactions.tsx +++ b/ts/components/conversation/message/message-content/MessageReactions.tsx @@ -1,16 +1,15 @@ +import { isEmpty, isEqual } from 'lodash'; import React, { ReactElement, useEffect, useState } from 'react'; import styled from 'styled-components'; +import { useMessageReactsPropsById } from '../../../../hooks/useParamSelector'; import { MessageRenderingProps } from '../../../../models/messageType'; -import { isEmpty, isEqual } from 'lodash'; +import { useSelectedIsGroup } from '../../../../state/selectors/selectedConversation'; import { SortedReactionList } from '../../../../types/Reaction'; -import { StyledPopupContainer } from '../reactions/ReactionPopup'; -import { Flex } from '../../../basic/Flex'; import { nativeEmojiData } from '../../../../util/emoji'; -import { Reaction, ReactionProps } from '../reactions/Reaction'; +import { Flex } from '../../../basic/Flex'; import { SessionIcon } from '../../../icon'; -import { useMessageReactsPropsById } from '../../../../hooks/useParamSelector'; -import { getSelectedConversationIsGroup } from '../../../../state/selectors/conversations'; -import { useSelector } from 'react-redux'; +import { Reaction, ReactionProps } from '../reactions/Reaction'; +import { StyledPopupContainer } from '../reactions/ReactionPopup'; export const popupXDefault = -81; export const popupYDefault = -90; @@ -164,7 +163,7 @@ export const MessageReactions = (props: Props): ReactElement => { const msgProps = useMessageReactsPropsById(messageId); - const inGroup = useSelector(getSelectedConversationIsGroup); + const inGroup = useSelectedIsGroup(); useEffect(() => { if (msgProps?.sortedReacts && !isEqual(reactions, msgProps?.sortedReacts)) { diff --git a/ts/components/conversation/message/message-content/Quote.tsx b/ts/components/conversation/message/message-content/Quote.tsx index 53cc63b9f..7b8fb5b14 100644 --- a/ts/components/conversation/message/message-content/Quote.tsx +++ b/ts/components/conversation/message/message-content/Quote.tsx @@ -1,22 +1,19 @@ -import React, { useState } from 'react'; import classNames from 'classnames'; +import React, { useState } from 'react'; import * as MIME from '../../../../../ts/types/MIME'; import * as GoogleChrome from '../../../../../ts/util/GoogleChrome'; -import { useSelector } from 'react-redux'; - import { noop } from 'lodash'; import { useDisableDrag } from '../../../../hooks/useDisableDrag'; import { useEncryptedFileFetch } from '../../../../hooks/useEncryptedFileFetch'; import { PubKey } from '../../../../session/types'; import { - getSelectedConversationKey, - isPublicGroupConversation, -} from '../../../../state/selectors/conversations'; + useSelectedIsPrivate, + useSelectedIsPublic, +} from '../../../../state/selectors/selectedConversation'; import { ContactName } from '../../ContactName'; import { MessageBody } from './MessageBody'; -import { useIsPrivate } from '../../../../hooks/useParamSelector'; export type QuotePropsWithoutListener = { attachment?: QuotedAttachmentType; @@ -229,8 +226,7 @@ export const QuoteText = ( ) => { const { text, attachment, isIncoming } = props; - const convoId = useSelector(getSelectedConversationKey); - const isGroup = !useIsPrivate(convoId); + const isGroup = !useSelectedIsPrivate(); if (text) { return ( @@ -345,7 +341,7 @@ export const Quote = (props: QuotePropsWithListener) => { setImageBroken(true); }; - const isPublic = useSelector(isPublicGroupConversation); + const isPublic = useSelectedIsPublic(); if (!validateQuote(props)) { return null; diff --git a/ts/components/conversation/message/message-item/ReadableMessage.tsx b/ts/components/conversation/message/message-item/ReadableMessage.tsx index 4d4927720..e6241c70b 100644 --- a/ts/components/conversation/message/message-item/ReadableMessage.tsx +++ b/ts/components/conversation/message/message-item/ReadableMessage.tsx @@ -17,11 +17,11 @@ import { getMostRecentMessageId, getOldestMessageId, getQuotedMessageToAnimate, - getSelectedConversationKey, getShowScrollButton, getYoungestMessageId, } from '../../../../state/selectors/conversations'; import { getIsAppFocused } from '../../../../state/selectors/section'; +import { useSelectedConversationKey } from '../../../../state/selectors/selectedConversation'; import { ScrollToLoadedMessageContext } from '../../SessionMessagesListContainer'; type ReadableMessageProps = { @@ -63,7 +63,7 @@ export const ReadableMessage = (props: ReadableMessageProps) => { const isAppFocused = useSelector(getIsAppFocused); const dispatch = useDispatch(); - const selectedConversationKey = useSelector(getSelectedConversationKey); + const selectedConversationKey = useSelectedConversationKey(); const loadedMessagesLength = useSelector(getLoadedMessagesLength); const mostRecentMessageId = useSelector(getMostRecentMessageId); const oldestMessageId = useSelector(getOldestMessageId); diff --git a/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx b/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx index e1d8f73ec..c5313f1bc 100644 --- a/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx +++ b/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx @@ -1,12 +1,15 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { PubKey } from '../../../../../session/types'; import { CallNotificationType, PropsForCallNotification, } from '../../../../../state/ducks/conversations'; -import { getSelectedConversation } from '../../../../../state/selectors/conversations'; +import { + useSelectedConversationKey, + useSelectedDisplayNameInProfile, + useSelectedNickname, +} from '../../../../../state/selectors/selectedConversation'; import { LocalizerKeys } from '../../../../../types/LocalizerKeys'; import { SessionIconType } from '../../../../icon'; import { ReadableMessage } from '../ReadableMessage'; @@ -37,13 +40,13 @@ const style: StyleType = { export const CallNotification = (props: PropsForCallNotification) => { const { messageId, receivedAt, isUnread, notificationType } = props; + const selectedConvoId = useSelectedConversationKey(); - const selectedConvoProps = useSelector(getSelectedConversation); + const displayNameInProfile = useSelectedDisplayNameInProfile(); + const nickname = useSelectedNickname(); const displayName = - selectedConvoProps?.nickname || - selectedConvoProps?.displayNameInProfile || - (selectedConvoProps?.id && PubKey.shorten(selectedConvoProps?.id)); + nickname || displayNameInProfile || (selectedConvoId && PubKey.shorten(selectedConvoId)); const styleItem = style[notificationType]; const notificationText = window.i18n(styleItem.notificationTextKey, [displayName || 'Unknown']); diff --git a/ts/components/conversation/message/reactions/Reaction.tsx b/ts/components/conversation/message/reactions/Reaction.tsx index 60dad8207..146eae004 100644 --- a/ts/components/conversation/message/reactions/Reaction.tsx +++ b/ts/components/conversation/message/reactions/Reaction.tsx @@ -76,7 +76,7 @@ export const Reaction = (props: ReactionProps): ReactElement => { const reactionRef = useRef(null); const { docX, elW } = useMouse(reactionRef); - const gutterWidth = 380; + const gutterWidth = 380; // TODO make this a variable which can be shared in CSS and JS const tooltipMidPoint = POPUP_WIDTH / 2; // px const [tooltipPosition, setTooltipPosition] = useState('center'); diff --git a/ts/components/dialog/ReactListModal.tsx b/ts/components/dialog/ReactListModal.tsx index c384d8331..b57a71fe8 100644 --- a/ts/components/dialog/ReactListModal.tsx +++ b/ts/components/dialog/ReactListModal.tsx @@ -1,6 +1,6 @@ import { isEmpty, isEqual } from 'lodash'; import React, { ReactElement, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { Data } from '../../data/data'; import { useMessageReactsPropsById, useWeAreModerator } from '../../hooks/useParamSelector'; @@ -11,7 +11,7 @@ import { updateReactListModal, updateUserDetailsModal, } from '../../state/ducks/modalDialog'; -import { getSelectedConversationIsPublic } from '../../state/selectors/conversations'; +import { useSelectedIsPublic } from '../../state/selectors/selectedConversation'; import { SortedReactionList } from '../../types/Reaction'; import { nativeEmojiData } from '../../util/emoji'; import { Reactions } from '../../util/reactions'; @@ -228,7 +228,7 @@ export const ReactListModal = (props: Props): ReactElement => { const [senders, setSenders] = useState>([]); const msgProps = useMessageReactsPropsById(messageId); - const isPublic = useSelector(getSelectedConversationIsPublic); + const isPublic = useSelectedIsPublic(); const weAreModerator = useWeAreModerator(msgProps?.convoId); const me = UserUtils.getOurPubKeyStrFromCache(); diff --git a/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx b/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx index af4889925..2158a3197 100644 --- a/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx +++ b/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx @@ -5,25 +5,24 @@ import { contextMenu } from 'react-contexify'; import { Avatar, AvatarSize } from '../../avatar/Avatar'; import { createPortal } from 'react-dom'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { openConversationWithMessages, ReduxConversationType, } from '../../../state/ducks/conversations'; import { updateUserDetailsModal } from '../../../state/ducks/modalDialog'; -import _ from 'lodash'; -import { useSelector } from 'react-redux'; +import _, { isNil } from 'lodash'; import { useAvatarPath, useConversationUsername, useHasUnread, useIsBlocked, useIsPrivate, - useIsSelectedConversation, useMentionedUs, } from '../../../hooks/useParamSelector'; import { isSearching } from '../../../state/selectors/search'; +import { useSelectedConversationKey } from '../../../state/selectors/selectedConversation'; import { MemoConversationListItemContextMenu } from '../../menu/ConversationListItemContextMenu'; import { ConversationListItemHeaderItem } from './HeaderItem'; import { MessageItem } from './MessageItem'; @@ -85,7 +84,9 @@ const ConversationListItem = (props: Props) => { let hasUnreadMentionedUs = useMentionedUs(conversationId); let isBlocked = useIsBlocked(conversationId); const isSearch = useSelector(isSearching); - const isSelectedConvo = useIsSelectedConversation(conversationId); + const selectedConvo = useSelectedConversationKey(); + + const isSelectedConvo = conversationId === selectedConvo && !isNil(selectedConvo); if (isSearch) { // force isBlocked and hasUnreadMentionedUs to be false, we just want to display the row without any special style when showing search results diff --git a/ts/components/leftpane/overlay/OverlayMessageRequest.tsx b/ts/components/leftpane/overlay/OverlayMessageRequest.tsx index 36fe4e00f..cb3f09163 100644 --- a/ts/components/leftpane/overlay/OverlayMessageRequest.tsx +++ b/ts/components/leftpane/overlay/OverlayMessageRequest.tsx @@ -3,10 +3,7 @@ import React from 'react'; import { SpacerLG } from '../../basic/Text'; import { useDispatch, useSelector } from 'react-redux'; -import { - getConversationRequests, - getSelectedConversation, -} from '../../../state/selectors/conversations'; +import { getConversationRequests } from '../../../state/selectors/conversations'; import { MemoConversationListItemWithDetails } from '../conversation-list-item/ConversationListItem'; import styled from 'styled-components'; import { SessionButton, SessionButtonColor } from '../../basic/SessionButton'; @@ -20,6 +17,7 @@ import { resetConversationExternal, } from '../../../state/ducks/conversations'; import { updateConfirmModal } from '../../../state/ducks/modalDialog'; +import { useSelectedConversationKey } from '../../../state/selectors/selectedConversation'; const MessageRequestListPlaceholder = styled.div` color: var(--conversation-tab-text-color); @@ -55,7 +53,7 @@ export const OverlayMessageRequest = () => { } const convoRequestCount = useSelector(getConversationRequests).length; const messageRequests = useSelector(getConversationRequests); - const selectedConversation = useSelector(getSelectedConversation); + const selectedConvoId = useSelectedConversationKey(); const buttonText = window.i18n('clearAll'); @@ -94,7 +92,7 @@ export const OverlayMessageRequest = () => { await convoModel.setIsApproved(false); // if we're looking at the convo to decline, close the convo - if (selectedConversation?.id === id) { + if (selectedConvoId === id) { dispatch(resetConversationExternal()); } return true; diff --git a/ts/components/lightbox/LightboxGallery.tsx b/ts/components/lightbox/LightboxGallery.tsx index ce3fa47ef..f4441b95d 100644 --- a/ts/components/lightbox/LightboxGallery.tsx +++ b/ts/components/lightbox/LightboxGallery.tsx @@ -6,12 +6,12 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Lightbox } from './Lightbox'; // tslint:disable-next-line: no-submodule-imports +import { useDispatch } from 'react-redux'; import useKey from 'react-use/lib/useKey'; -import { AttachmentTypeWithPath } from '../../types/Attachment'; -import { useDispatch, useSelector } from 'react-redux'; import { showLightBox } from '../../state/ducks/conversations'; -import { getSelectedConversationKey } from '../../state/selectors/conversations'; +import { useSelectedConversationKey } from '../../state/selectors/selectedConversation'; import { MIME } from '../../types'; +import { AttachmentTypeWithPath } from '../../types/Attachment'; import { saveAttachmentToDisk } from '../../util/attachmentsUtil'; export interface MediaItemType { @@ -33,7 +33,11 @@ type Props = { export const LightboxGallery = (props: Props) => { const { media } = props; const [currentIndex, setCurrentIndex] = useState(-1); - const selectedConversation = useSelector(getSelectedConversationKey) as string; + const selectedConversation = useSelectedConversationKey(); + + if (!selectedConversation) { + throw new Error('LightboxGallery: selectedConversation is undefined'); + } const dispatch = useDispatch(); diff --git a/ts/components/menu/ConversationHeaderMenu.tsx b/ts/components/menu/ConversationHeaderMenu.tsx index 6ee07b4b3..adbb2212f 100644 --- a/ts/components/menu/ConversationHeaderMenu.tsx +++ b/ts/components/menu/ConversationHeaderMenu.tsx @@ -24,9 +24,8 @@ import { } from './Menu'; import _ from 'lodash'; import { ContextConversationId } from '../leftpane/conversation-list-item/ConversationListItem'; -import { getSelectedConversationKey } from '../../state/selectors/conversations'; -import { useSelector } from 'react-redux'; import { SessionContextMenuContainer } from '../SessionContextMenuContainer'; +import { useSelectedConversationKey } from '../../state/selectors/selectedConversation'; export type PropsConversationHeaderMenu = { triggerId: string; @@ -35,7 +34,7 @@ export type PropsConversationHeaderMenu = { export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { const { triggerId } = props; - const selectedConversation = useSelector(getSelectedConversationKey); + const selectedConversation = useSelectedConversationKey(); if (!selectedConversation) { throw new Error('selectedConversation must be set for a header to be visible!'); diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts index 33b608a05..7587cf757 100644 --- a/ts/hooks/useParamSelector.ts +++ b/ts/hooks/useParamSelector.ts @@ -1,13 +1,10 @@ -import { isEmpty, isNil, pick } from 'lodash'; +import { isEmpty, pick } from 'lodash'; import { useSelector } from 'react-redux'; import { ConversationModel } from '../models/conversation'; import { PubKey } from '../session/types'; import { UserUtils } from '../session/utils'; import { StateType } from '../state/reducer'; -import { - getMessageReactsProps, - getSelectedConversationKey, -} from '../state/selectors/conversations'; +import { getMessageReactsProps } from '../state/selectors/conversations'; export function useAvatarPath(convoId: string | undefined) { const convoProps = useConversationPropsById(convoId); @@ -220,8 +217,3 @@ export function useMentionedUs(conversationId?: string): boolean { export function useIsTyping(conversationId?: string): boolean { return useConversationPropsById(conversationId)?.isTyping || false; } - -export function useIsSelectedConversation(conversation?: string): boolean { - const selectedConvo = useSelector(getSelectedConversationKey); - return !isNil(selectedConvo) && !isNil(conversation) && selectedConvo === conversation; -} diff --git a/ts/hooks/useVideoEventListener.ts b/ts/hooks/useVideoEventListener.ts index 515be62d7..b9231207c 100644 --- a/ts/hooks/useVideoEventListener.ts +++ b/ts/hooks/useVideoEventListener.ts @@ -9,11 +9,11 @@ import { InputItem, removeVideoEventsListener, } from '../session/utils/calling/CallManager'; -import { getSelectedConversationKey } from '../state/selectors/conversations'; import { getCallIsInFullScreen, getHasOngoingCallWithPubkey } from '../state/selectors/call'; +import { useSelectedConversationKey } from '../state/selectors/selectedConversation'; export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) { - const selectedConversationKey = useSelector(getSelectedConversationKey); + const selectedConversationKey = useSelectedConversationKey(); const ongoingCallPubkey = useSelector(getHasOngoingCallWithPubkey); const isFullScreen = useSelector(getCallIsInFullScreen); diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 98ec27e62..d2f2129dd 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -190,8 +190,9 @@ export function showLeaveGroupByConvoId(conversationId: string) { const title = window.i18n('leaveGroup'); const message = window.i18n('leaveGroupConfirmation'); - const ourPK = UserUtils.getOurPubKeyStrFromCache(); - const isAdmin = (conversation.get('groupAdmins') || []).includes(ourPK); + const isAdmin = (conversation.get('groupAdmins') || []).includes( + UserUtils.getOurPubKeyStrFromCache() + ); const isClosedGroup = conversation.isClosedGroup() || false; // if this is not a closed group, or we are not admin, we can just show a confirmation dialog diff --git a/ts/models/message.ts b/ts/models/message.ts index a98522403..2e88fbbfe 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -1296,8 +1296,8 @@ export class MessageModel extends Backbone.Model { return window.i18n('titleIsNow', [groupUpdate.name]); } if (groupUpdate.joined && groupUpdate.joined.length) { - const names = groupUpdate.joined.map((pubKey: string) => - getConversationController().getContactProfileNameOrShortenedPubKey(pubKey) + const names = groupUpdate.joined.map( + getConversationController().getContactProfileNameOrShortenedPubKey ); if (names.length > 1) { diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 09944bd57..c2f4eae9b 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -143,43 +143,43 @@ async function handleContactsUpdate(result: IncomingConfResult): Promise 0; - if (shouldBePinned !== Boolean(existingConvo.isPinned())) { - await existingConvo.setIsPinned(shouldBePinned, false); + if (shouldBePinned !== Boolean(contactConvo.isPinned())) { + await contactConvo.setIsPinned(shouldBePinned, false); changes = true; } @@ -188,12 +188,12 @@ async function handleContactsUpdate(result: IncomingConfResult): Promise 0; - if (shouldBePinned !== Boolean(existingConvo.isPinned())) { - await existingConvo.setIsPinned(shouldBePinned, false); + if (shouldBePinned !== Boolean(communityConvo.isPinned())) { + await communityConvo.setIsPinned(shouldBePinned, false); changes = true; } // make sure to write the changes to the database now as the `AvatarDownloadJob` below might take some time before getting run if (changes) { - await existingConvo.commit(); + await communityConvo.commit(); } } } @@ -340,8 +340,8 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) { for (let index = 0; index < allLegacyGroupsInWrapper.length; index++) { const fromWrapper = allLegacyGroupsInWrapper[index]; - const convo = getConversationController().get(fromWrapper.pubkeyHex); - if (!convo) { + const legacyGroupConvo = getConversationController().get(fromWrapper.pubkeyHex); + if (!legacyGroupConvo) { // this should not happen as we made sure to create them before window.log.warn( 'could not find legacy group which should already be there:', @@ -360,8 +360,9 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) { members, admins, activeAt: - !!convo.get('active_at') && convo.get('active_at') < latestEnvelopeTimestamp - ? convo.get('active_at') + !!legacyGroupConvo.get('active_at') && + legacyGroupConvo.get('active_at') < latestEnvelopeTimestamp + ? legacyGroupConvo.get('active_at') : latestEnvelopeTimestamp, weWereJustAdded: false, // TODO to remove }; @@ -369,16 +370,16 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) { await ClosedGroup.updateOrCreateClosedGroup(groupDetails); let changes = false; - if (convo.isPinned() !== fromWrapper.priority > 0) { - await convo.setIsPinned(fromWrapper.priority > 0, false); + if (legacyGroupConvo.isPinned() !== fromWrapper.priority > 0) { + await legacyGroupConvo.setIsPinned(fromWrapper.priority > 0, false); changes = true; } - if (!!convo.isHidden() !== !!fromWrapper.hidden) { - convo.set({ hidden: !!fromWrapper.hidden }); + if (!!legacyGroupConvo.isHidden() !== !!fromWrapper.hidden) { + legacyGroupConvo.set({ hidden: !!fromWrapper.hidden }); changes = true; } - if (convo.get('expireTimer') !== fromWrapper.disappearingTimerSeconds) { - await convo.updateExpireTimer( + if (legacyGroupConvo.get('expireTimer') !== fromWrapper.disappearingTimerSeconds) { + await legacyGroupConvo.updateExpireTimer( fromWrapper.disappearingTimerSeconds, undefined, latestEnvelopeTimestamp, @@ -390,45 +391,23 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) { } if (changes) { - await convo.commit(); + await legacyGroupConvo.commit(); } // save the encryption keypair if needed if (!isEmpty(fromWrapper.encPubkey) && !isEmpty(fromWrapper.encSeckey)) { - const inWrapperKeypair: HexKeyPair = { - publicHex: toHex(fromWrapper.encPubkey), - privateHex: toHex(fromWrapper.encSeckey), - }; + try { + const inWrapperKeypair: HexKeyPair = { + publicHex: toHex(fromWrapper.encPubkey), + privateHex: toHex(fromWrapper.encSeckey), + }; - await addKeyPairToCacheAndDBIfNeeded(fromWrapper.pubkeyHex, inWrapperKeypair); + await addKeyPairToCacheAndDBIfNeeded(fromWrapper.pubkeyHex, inWrapperKeypair); + } catch (e) { + window.log.warn('failed to save keypair for legacugroup', fromWrapper.pubkeyHex); + } } } - - // // if the convos already exists, make sure to update the fields if needed - // for (let index = 0; index < allCommunitiesInWrapper.length; index++) { - // const fromWrapper = allCommunitiesInWrapper[index]; - // const convoId = OpenGroupUtils.getOpenGroupV2ConversationId( - // fromWrapper.baseUrl, - // fromWrapper.roomCasePreserved - // ); - - // const existingConvo = getConversationController().get(convoId); - // if (fromWrapper && existingConvo) { - // let changes = false; - - // //TODO priority means more than just isPinned but has an order logic in it too - // const shouldBePinned = fromWrapper.priority > 0; - // if (shouldBePinned !== Boolean(existingConvo.isPinned())) { - // await existingConvo.setIsPinned(shouldBePinned, false); - // changes = true; - // } - - // // make sure to write the changes to the database now as the `AvatarDownloadJob` below might take some time before getting run - // if (changes) { - // await existingConvo.commit(); - // } - // } - // } } async function handleUserGroupsUpdate(result: IncomingConfResult): Promise { diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 51cae1494..02b8fdc65 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -87,22 +87,19 @@ export class ConversationController { throw new Error('getConversationController().get() needs complete initial fetch'); } - let conversation = this.conversations.get(id); - if (conversation) { - return conversation; + if (this.conversations.get(id)) { + return this.conversations.get(id) as ConversationModel; } - conversation = this.conversations.add({ + const conversation = this.conversations.add({ id, type, }); const create = async () => { try { - const saveDetails = await Data.saveConversation(conversation.attributes); - if (saveDetails) { - await conversation.refreshInMemoryDetails(saveDetails); - } + // this saves to DB and to the required wrapper + await conversation.commit(); } catch (error) { window?.log?.error( 'Conversation save failed! ', @@ -114,25 +111,22 @@ export class ConversationController { throw error; } - return conversation; - }; + window?.inboxStore?.dispatch( + conversationActions.conversationAdded({ + id: conversation.id, + data: conversation.getConversationModelProps(), + }) + ); - conversation.initialPromise = create(); - conversation.initialPromise.then(() => { - if (window?.inboxStore) { - window.inboxStore?.dispatch( - conversationActions.conversationAdded({ - id: conversation.id, - data: conversation.getConversationModelProps(), - }) - ); - } if (!conversation.isPublic() && conversation.isActive()) { // NOTE: we request snodes updating the cache, but ignore the result void getSwarmFor(id); } - }); + return conversation; + }; + + conversation.initialPromise = create(); return conversation; } diff --git a/ts/state/selectors/call.ts b/ts/state/selectors/call.ts index cb4db0357..f6b32e30d 100644 --- a/ts/state/selectors/call.ts +++ b/ts/state/selectors/call.ts @@ -2,7 +2,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { CallStateType, CallStatusEnum } from '../ducks/call'; import { ConversationsStateType, ReduxConversationType } from '../ducks/conversations'; import { StateType } from '../reducer'; -import { getConversations, getSelectedConversationKey } from './conversations'; +import { getConversations } from './conversations'; +import { getSelectedConversationKey } from './selectedConversation'; const getCallState = (state: StateType): CallStateType => state.call; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 29c4ae35c..39e07e0f8 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -1,6 +1,5 @@ import { createSelector } from '@reduxjs/toolkit'; -import { StateType } from '../reducer'; import { ConversationLookupType, ConversationsStateType, @@ -11,12 +10,8 @@ import { ReduxConversationType, SortedMessageModelProps, } from '../ducks/conversations'; +import { StateType } from '../reducer'; -import { getIntl } from './user'; -import { BlockedNumberController } from '../../util'; -import { ConversationModel } from '../../models/conversation'; -import { LocalizerType } from '../../types/Util'; -import { ConversationHeaderTitleProps } from '../../components/conversation/ConversationHeader'; 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'; @@ -30,14 +25,19 @@ import { MessageStatusSelectorProps } from '../../components/conversation/messag import { MessageTextSelectorProps } from '../../components/conversation/message/message-content/MessageText'; import { GenericReadableMessageSelectorProps } from '../../components/conversation/message/message-item/GenericReadableMessage'; import { LightBoxOptions } from '../../components/conversation/SessionConversation'; +import { ConversationModel } from '../../models/conversation'; +import { ConversationTypeEnum, isOpenOrClosedGroup } from '../../models/conversationAttributes'; import { getConversationController } from '../../session/conversations'; import { UserUtils } from '../../session/utils'; +import { LocalizerType } from '../../types/Util'; +import { BlockedNumberController } from '../../util'; import { Storage } from '../../util/storage'; -import { ConversationTypeEnum, isOpenOrClosedGroup } from '../../models/conversationAttributes'; +import { getIntl } from './user'; -import { MessageReactsSelectorProps } from '../../components/conversation/message/message-content/MessageReactions'; import { filter, isEmpty, pick, sortBy } from 'lodash'; -import { getCanWrite, getModeratorsOutsideRedux, getSubscriberCount } from './sogsRoomInfo'; +import { MessageReactsSelectorProps } from '../../components/conversation/message/message-content/MessageReactions'; +import { getModeratorsOutsideRedux } from './sogsRoomInfo'; +import { getSelectedConversation, getSelectedConversationKey } from './selectedConversation'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; @@ -52,80 +52,6 @@ export const getConversationsCount = createSelector(getConversationLookup, (stat return Object.values(state).length; }); -export const getSelectedConversationKey = createSelector( - getConversations, - (state: ConversationsStateType): string | undefined => { - return state.selectedConversation; - } -); - -export const getSelectedConversation = createSelector( - getConversations, - (state: ConversationsStateType): ReduxConversationType | undefined => { - return state.selectedConversation - ? state.conversationLookup[state.selectedConversation] - : undefined; - } -); - -export const getSelectedConversationIsPublic = createSelector( - getSelectedConversation, - (state: ReduxConversationType | undefined): boolean => { - return state?.isPublic || false; - } -); - -export function getIsTypingEnabled(state: StateType) { - const selectedConvoPubkey = getSelectedConversationKey(state); - if (!selectedConvoPubkey) { - return false; - } - const selectedConvo = state.conversations.conversationLookup[selectedConvoPubkey]; - if (!selectedConvo) { - return false; - } - const canWrite = getCanWrite(state, selectedConvoPubkey); - const { isBlocked, isKickedFromGroup, left, isPublic } = selectedConvo; - - return !(isBlocked || isKickedFromGroup || left || (isPublic && !canWrite)); -} - -/** - * Returns true if the current conversation selected is a group conversation. - * Returns false if the current conversation selected is not a group conversation, or none are selected - */ -export const getSelectedConversationIsGroup = createSelector( - getSelectedConversation, - (state: ReduxConversationType | undefined): boolean => { - const type = state?.type; - return type ? isOpenOrClosedGroup(type) : false; - } -); - -/** - * Returns true if the current conversation selected is a closed group and false otherwise. - */ -export const isClosedGroupConversation = createSelector( - getSelectedConversation, - (state: ReduxConversationType | undefined): boolean => { - return ( - (state?.type === ConversationTypeEnum.GROUP && !state.isPublic) || - state?.type === ConversationTypeEnum.GROUPV3 || - false - ); - } -); - -/** - * Returns true if the current conversation selected is a public group and false otherwise. - */ -export const isPublicGroupConversation = createSelector( - getSelectedConversation, - (state: ReduxConversationType | undefined): boolean => { - return (state?.type === ConversationTypeEnum.GROUP && state.isPublic) || false; - } -); - export const getOurPrimaryConversation = createSelector( getConversations, (state: ConversationsStateType): ReduxConversationType => @@ -547,85 +473,6 @@ export const getGlobalUnreadMessageCount = createSelector(getLeftPaneLists, (sta return state.globalUnreadCount; }); -export const getConversationHeaderTitleProps = ( - state: StateType -): ConversationHeaderTitleProps | undefined => { - const convo = getSelectedConversation(state); - if (!convo) { - return undefined; - } - - return { - isKickedFromGroup: !!convo.isKickedFromGroup, - conversationKey: convo.id, - isMe: !!convo.isMe, - members: convo.members || [], - isPublic: !!convo.isPublic, - isGroup: isOpenOrClosedGroup(convo.type), - currentNotificationSetting: convo.currentNotificationSetting, - }; -}; - -export const getCurrentSubscriberCount = (state: StateType): number | undefined => { - const convo = getSelectedConversation(state); - if (!convo) { - return undefined; - } - return getSubscriberCount(state, convo.id); -}; - -/** - * Returns the formatted text for notification setting. - */ -export const getCurrentNotificationSettingText = createSelector(getSelectedConversation, (state): - | string - | undefined => { - if (!state) { - return undefined; - } - switch (state.currentNotificationSetting) { - case 'all': - return window.i18n('notificationForConvo_all'); - case 'mentions_only': - return window.i18n('notificationForConvo_mentions_only'); - case 'disabled': - return window.i18n('notificationForConvo_disabled'); - default: - return window.i18n('notificationForConvo_all'); - } -}); - -export const getIsSelectedPrivate = createSelector( - getSelectedConversation, - (selectedProps): boolean => { - return selectedProps?.isPrivate || false; - } -); - -export const getIsSelectedBlocked = createSelector( - getSelectedConversation, - (selectedProps): boolean => { - return selectedProps?.isBlocked || false; - } -); - -/** - * Returns true if the currently selected conversation is active (has an active_at field > 0) - */ -export const getIsSelectedActive = createSelector( - getSelectedConversation, - (selectedProps): boolean => { - return Boolean(selectedProps?.activeAt); - } -); - -export const getIsSelectedNoteToSelf = createSelector( - getSelectedConversation, - (selectedProps): boolean => { - return selectedProps?.isMe || false; - } -); - export const isMessageDetailView = createSelector( getConversations, (state: ConversationsStateType): boolean => state.messageDetailProps !== undefined diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts index e688c662b..45206f698 100644 --- a/ts/state/selectors/search.ts +++ b/ts/state/selectors/search.ts @@ -4,8 +4,9 @@ import { createSelector } from '@reduxjs/toolkit'; import { StateType } from '../reducer'; import { SearchStateType } from '../ducks/search'; -import { getConversationLookup, getSelectedConversationKey } from './conversations'; +import { getConversationLookup } from './conversations'; import { ConversationLookupType } from '../ducks/conversations'; +import { getSelectedConversationKey } from './selectedConversation'; export const getSearch = (state: StateType): SearchStateType => state.search; diff --git a/ts/state/selectors/selectedConversation.ts b/ts/state/selectors/selectedConversation.ts new file mode 100644 index 000000000..7aad23448 --- /dev/null +++ b/ts/state/selectors/selectedConversation.ts @@ -0,0 +1,185 @@ +import { useSelector } from 'react-redux'; +import { ConversationTypeEnum, isOpenOrClosedGroup } from '../../models/conversationAttributes'; +import { ReduxConversationType } from '../ducks/conversations'; +import { StateType } from '../reducer'; +import { getCanWrite, getSubscriberCount } from './sogsRoomInfo'; + +/** + * Returns the formatted text for notification setting. + */ +const getCurrentNotificationSettingText = (state: StateType): string | undefined => { + if (!state) { + return undefined; + } + const currentNotificationSetting = getSelectedConversation(state)?.currentNotificationSetting; + switch (currentNotificationSetting) { + case 'all': + return window.i18n('notificationForConvo_all'); + case 'mentions_only': + return window.i18n('notificationForConvo_mentions_only'); + case 'disabled': + return window.i18n('notificationForConvo_disabled'); + default: + return window.i18n('notificationForConvo_all'); + } +}; + +const getIsSelectedPrivate = (state: StateType): boolean => { + return Boolean(getSelectedConversation(state)?.isPrivate) || false; +}; + +const getIsSelectedBlocked = (state: StateType): boolean => { + return Boolean(getSelectedConversation(state)?.isBlocked) || false; +}; + +/** + * Returns true if the currently selected conversation is active (has an active_at field > 0) + */ +const getIsSelectedActive = (state: StateType): boolean => { + return Boolean(getSelectedConversation(state)?.activeAt) || false; +}; + +const getIsSelectedNoteToSelf = (state: StateType): boolean => { + return getSelectedConversation(state)?.isMe || false; +}; + +export const getSelectedConversationKey = (state: StateType): string | undefined => { + return state.conversations.selectedConversation; +}; + +export const getSelectedConversation = (state: StateType): ReduxConversationType | undefined => { + const selected = getSelectedConversationKey(state); + return selected ? state.conversations.conversationLookup[selected] : undefined; +}; + +/** + * Returns true if the current conversation selected is a public group and false otherwise. + */ +export const getSelectedConversationIsPublic = (state: StateType): boolean => { + return Boolean(getSelectedConversation(state)?.isPublic) || false; +}; + +/** + * Returns true if the current conversation selected can be typed into + */ +export function getSelectedCanWrite(state: StateType) { + const selectedConvoPubkey = getSelectedConversationKey(state); + if (!selectedConvoPubkey) { + return false; + } + const selectedConvo = getSelectedConversation(state); + if (!selectedConvo) { + return false; + } + const canWrite = getCanWrite(state, selectedConvoPubkey); + const { isBlocked, isKickedFromGroup, left, isPublic } = selectedConvo; + + return !(isBlocked || isKickedFromGroup || left || (isPublic && !canWrite)); +} + +/** + * Returns true if the current conversation selected is a group conversation. + * Returns false if the current conversation selected is not a group conversation, or none are selected + */ +const getSelectedConversationIsGroup = (state: StateType): boolean => { + const selected = getSelectedConversation(state); + if (!selected || !selected.type) { + return false; + } + return selected.type ? isOpenOrClosedGroup(selected.type) : false; +}; + +/** + * Returns true if the current conversation selected is a closed group and false otherwise. + */ +export const isClosedGroupConversation = (state: StateType): boolean => { + const selected = getSelectedConversation(state); + if (!selected) { + return false; + } + return ( + (selected.type === ConversationTypeEnum.GROUP && !selected.isPublic) || + selected.type === ConversationTypeEnum.GROUPV3 || + false + ); +}; + +const getGroupMembers = (state: StateType): Array => { + const selected = getSelectedConversation(state); + if (!selected) { + return []; + } + return selected.members || []; +}; + +const getSelectedSubscriberCount = (state: StateType): number | undefined => { + const convo = getSelectedConversation(state); + if (!convo) { + return undefined; + } + return getSubscriberCount(state, convo.id); +}; + +// ============== SELECTORS RELEVANT TO SELECTED/OPENED CONVERSATION ============== + +export function useSelectedConversationKey() { + return useSelector(getSelectedConversationKey); +} + +export function useSelectedIsGroup() { + return useSelector(getSelectedConversationIsGroup); +} + +export function useSelectedIsPublic() { + return useSelector(getSelectedConversationIsPublic); +} + +export function useSelectedIsPrivate() { + return useSelector(getIsSelectedPrivate); +} + +export function useSelectedIsBlocked() { + return useSelector(getIsSelectedBlocked); +} + +export function useSelectedIsActive() { + return useSelector(getIsSelectedActive); +} + +export function useSelectedisNoteToSelf() { + return useSelector(getIsSelectedNoteToSelf); +} + +export function useSelectedMembers() { + return useSelector(getGroupMembers); +} + +export function useSelectedSubscriberCount() { + return useSelector(getSelectedSubscriberCount); +} + +export function useSelectedNotificationSetting() { + return useSelector(getCurrentNotificationSettingText); +} + +export function useSelectedIsKickedFromGroup() { + return useSelector( + (state: StateType) => Boolean(getSelectedConversation(state)?.isKickedFromGroup) || false + ); +} + +export function useSelectedIsLeft() { + return useSelector((state: StateType) => Boolean(getSelectedConversation(state)?.left) || false); +} + +export function useSelectedNickname() { + return useSelector((state: StateType) => getSelectedConversation(state)?.nickname); +} + +export function useSelectedDisplayNameInProfile() { + return useSelector((state: StateType) => getSelectedConversation(state)?.displayNameInProfile); +} + +export function useSelectedWeAreAdmin() { + return useSelector((state: StateType) => getSelectedConversation(state)?.weAreAdmin || false); +} diff --git a/ts/state/selectors/stagedAttachments.ts b/ts/state/selectors/stagedAttachments.ts index ad48d459f..eedbe881c 100644 --- a/ts/state/selectors/stagedAttachments.ts +++ b/ts/state/selectors/stagedAttachments.ts @@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { StagedAttachmentType } from '../../components/conversation/composition/CompositionBox'; import { StagedAttachmentsStateType } from '../ducks/stagedAttachments'; import { StateType } from '../reducer'; -import { getSelectedConversationKey } from './conversations'; +import { getSelectedConversationKey } from './selectedConversation'; export const getStagedAttachmentsState = (state: StateType): StagedAttachmentsStateType => state.stagedAttachments; diff --git a/ts/state/smart/SessionConversation.ts b/ts/state/smart/SessionConversation.ts index ca9692a6c..c5850f2f1 100644 --- a/ts/state/smart/SessionConversation.ts +++ b/ts/state/smart/SessionConversation.ts @@ -1,21 +1,23 @@ import { connect } from 'react-redux'; +import { SessionConversation } from '../../components/conversation/SessionConversation'; import { mapDispatchToProps } from '../actions'; import { StateType } from '../reducer'; -import { getTheme } from '../selectors/theme'; +import { getHasOngoingCallWithFocusedConvo } from '../selectors/call'; import { getIsSelectedConvoInitialLoadingInProgress, getLightBoxOptions, - getSelectedConversation, - getSelectedConversationKey, getSelectedMessageIds, getSortedMessagesOfSelectedConversation, isMessageDetailView, isRightPanelShowing, } from '../selectors/conversations'; -import { getOurNumber } from '../selectors/user'; +import { + getSelectedConversation, + getSelectedConversationKey, +} from '../selectors/selectedConversation'; import { getStagedAttachmentsForCurrentConversation } from '../selectors/stagedAttachments'; -import { getHasOngoingCallWithFocusedConvo } from '../selectors/call'; -import { SessionConversation } from '../../components/conversation/SessionConversation'; +import { getTheme } from '../selectors/theme'; +import { getOurNumber } from '../selectors/user'; const mapStateToProps = (state: StateType) => { return {