From 97b91565623e4c7b63821abae1b42e64f0233388 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 6 Jul 2021 17:14:00 +1000 Subject: [PATCH] improve performamce by memoizing avatar and menus --- ts/components/Avatar.tsx | 5 +- ts/components/ConversationListItem.tsx | 375 ++++++++++-------- ts/components/MessageSearchResult.tsx | 2 +- ts/components/SearchResults.tsx | 9 +- .../conversation/ConversationHeader.tsx | 315 ++++++++------- ts/components/conversation/Timestamp.tsx | 8 +- .../conversation/message/MessageMetadata.tsx | 2 - .../message/OutgoingMessageStatus.tsx | 20 +- .../session/LeftPaneContactSection.tsx | 4 +- .../session/LeftPaneMessageSection.tsx | 4 +- ts/components/session/icon/SessionIcon.tsx | 2 +- .../session/menu/ConversationHeaderMenu.tsx | 69 ++-- .../menu/ConversationListItemContextMenu.tsx | 54 +-- ts/components/session/menu/Menu.tsx | 4 - ts/session/constants.ts | 2 +- ts/state/ducks/conversations.ts | 4 +- ts/state/ducks/timerOptions.tsx | 4 +- ts/test/state/selectors/conversations_test.ts | 10 + 18 files changed, 489 insertions(+), 404 deletions(-) diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 313d165b1..d0e9f8f2c 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -4,6 +4,7 @@ import classNames from 'classnames'; import { AvatarPlaceHolder, ClosedGroupAvatar } from './AvatarPlaceHolder'; import { ConversationAvatar } from './session/usingClosedConversationDetails'; import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch'; +import _ from 'underscore'; export enum AvatarSize { XS = 28, @@ -84,7 +85,7 @@ const AvatarImage = (props: { ); }; -export const Avatar = (props: Props) => { +const AvatarInner = (props: Props) => { const { avatarPath, base64Data, size, memberAvatars, name } = props; const [imageBroken, setImageBroken] = useState(false); // contentType is not important @@ -130,3 +131,5 @@ export const Avatar = (props: Props) => { ); }; + +export const Avatar = React.memo(AvatarInner, _.isEqual); diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 7c3594054..720202f9b 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -13,15 +13,17 @@ import { ConversationAvatar, usingClosedConversationDetails, } from './session/usingClosedConversationDetails'; -import { - ConversationListItemContextMenu, - PropsContextConversationItem, -} from './session/menu/ConversationListItemContextMenu'; +import { MemoConversationListItemContextMenu } from './session/menu/ConversationListItemContextMenu'; import { createPortal } from 'react-dom'; import { OutgoingMessageStatus } from './conversation/message/OutgoingMessageStatus'; -import { DefaultTheme, withTheme } from 'styled-components'; +import { DefaultTheme, useTheme } from 'styled-components'; import { PubKey } from '../session/types'; -import { ConversationType, openConversationExternal } from '../state/ducks/conversations'; +import { + ConversationType, + LastMessageType, + openConversationExternal, +} from '../state/ducks/conversations'; +import _ from 'underscore'; export interface ConversationListItemProps extends ConversationType { index?: number; // used to force a refresh when one conversation is removed on top of the list @@ -30,7 +32,6 @@ export interface ConversationListItemProps extends ConversationType { type PropsHousekeeping = { style?: Object; - theme: DefaultTheme; }; type Props = ConversationListItemProps & PropsHousekeeping; @@ -39,184 +40,234 @@ const Portal = ({ children }: { children: any }) => { return createPortal(children, document.querySelector('.inbox.index') as Element); }; -class ConversationListItem extends React.PureComponent { - public constructor(props: Props) { - super(props); - } +const AvatarItem = (props: { + avatarPath?: string; + phoneNumber: string; + memberAvatars?: Array; + name?: string; + profileName?: string; +}) => { + const { avatarPath, name, phoneNumber, profileName, memberAvatars } = props; - public renderAvatar() { - const { avatarPath, name, phoneNumber, profileName, memberAvatars } = this.props; + const userName = name || profileName || phoneNumber; - const userName = name || profileName || phoneNumber; + return ( +
+ +
+ ); +}; - return ( -
- -
- ); - } +const UserItem = (props: { + name?: string; + profileName?: string; + isMe: boolean; + phoneNumber: string; +}) => { + const { name, phoneNumber, profileName, isMe } = props; - public renderHeader() { - const { unreadCount, mentionedUs, activeAt } = this.props; + const shortenedPubkey = PubKey.shorten(phoneNumber); - let atSymbol = null; - let unreadCountDiv = null; - if (unreadCount > 0) { - atSymbol = mentionedUs ?

@

: null; - unreadCountDiv =

{unreadCount}

; - } + const displayedPubkey = profileName ? shortenedPubkey : phoneNumber; + const displayName = isMe ? window.i18n('noteToSelf') : profileName; - return ( -
-
0 ? 'module-conversation-list-item__header__name--with-unread' : null - )} - > - {this.renderUser()} -
- {unreadCountDiv} - {atSymbol} - { -
0 ? 'module-conversation-list-item__header__date--has-unread' : null - )} - > - { - - } -
- } -
- ); + let shouldShowPubkey = false; + if ((!name || name.length === 0) && (!displayName || displayName.length === 0)) { + shouldShowPubkey = true; } - public renderMessage() { - const { lastMessage, isTyping, unreadCount } = this.props; + return ( +
+ +
+ ); +}; - if (!lastMessage && !isTyping) { - return null; - } - const text = lastMessage && lastMessage.text ? lastMessage.text : ''; +const MessageItem = (props: { + isTyping: boolean; + lastMessage?: LastMessageType; + unreadCount: number; +}) => { + const { lastMessage, isTyping, unreadCount } = props; - if (isEmpty(text)) { - return null; - } + const theme = useTheme(); - return ( -
-
0 ? 'module-conversation-list-item__message__text--has-unread' : null - )} - > - {isTyping ? ( - - ) : ( - - )} -
- {lastMessage && lastMessage.status ? ( - - ) : null} -
- ); + if (!lastMessage && !isTyping) { + return null; } + const text = lastMessage && lastMessage.text ? lastMessage.text : ''; - public render() { - const { phoneNumber, unreadCount, id, isSelected, isBlocked, style, mentionedUs } = this.props; - const triggerId = `conversation-item-${phoneNumber}-ctxmenu`; - const key = `conversation-item-${phoneNumber}`; + if (isEmpty(text)) { + return null; + } - return ( -
+ return ( +
+
0 ? 'module-conversation-list-item__message__text--has-unread' : null + )} + > + {isTyping ? ( + + ) : ( + + )} +
+ {lastMessage && lastMessage.status ? ( + + ) : null} +
+ ); +}; + +const HeaderItem = (props: { + unreadCount: number; + isMe: boolean; + mentionedUs: boolean; + activeAt?: number; + name?: string; + profileName?: string; + phoneNumber: string; +}) => { + const { unreadCount, mentionedUs, activeAt, isMe, phoneNumber, profileName, name } = props; + + let atSymbol = null; + let unreadCountDiv = null; + if (unreadCount > 0) { + atSymbol = mentionedUs ?

@

: null; + unreadCountDiv =

{unreadCount}

; + } + + return ( +
+
0 ? 'module-conversation-list-item__header__name--with-unread' : null + )} + > + +
+ {unreadCountDiv} + {atSymbol} + {
{ - window.inboxStore?.dispatch(openConversationExternal(id)); - }} - onContextMenu={(e: any) => { - contextMenu.show({ - id: triggerId, - event: e, - }); - }} - style={style} className={classNames( - 'module-conversation-list-item', - unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null, - unreadCount > 0 && mentionedUs ? 'module-conversation-list-item--mentioned-us' : null, - isSelected ? 'module-conversation-list-item--is-selected' : null, - isBlocked ? 'module-conversation-list-item--is-blocked' : null + 'module-conversation-list-item__header__date', + unreadCount > 0 ? 'module-conversation-list-item__header__date--has-unread' : null )} > - {this.renderAvatar()} -
- {this.renderHeader()} - {this.renderMessage()} -
+ {}
- - - -
- ); - } - - private getMenuProps(triggerId: string): PropsContextConversationItem { - return { - triggerId, - ...this.props, - }; - } - - private renderUser() { - const { name, phoneNumber, profileName, isMe } = this.props; - - const shortenedPubkey = PubKey.shorten(phoneNumber); - - const displayedPubkey = profileName ? shortenedPubkey : phoneNumber; - const displayName = isMe ? window.i18n('noteToSelf') : profileName; + } +
+ ); +}; - let shouldShowPubkey = false; - if ((!name || name.length === 0) && (!displayName || displayName.length === 0)) { - shouldShowPubkey = true; - } +const ConversationListItem = (props: Props) => { + console.warn('ConversationListItem', props.id.substr(-1), ': ', props); + const { + activeAt, + phoneNumber, + unreadCount, + id, + isSelected, + isBlocked, + style, + mentionedUs, + isMe, + name, + profileName, + memberAvatars, + isTyping, + lastMessage, + hasNickname, + isKickedFromGroup, + left, + type, + isPublic, + avatarPath, + } = props; + const triggerId = `conversation-item-${phoneNumber}-ctxmenu`; + const key = `conversation-item-${phoneNumber}`; - return ( -
- +
{ + window.inboxStore?.dispatch(openConversationExternal(id)); + }} + onContextMenu={(e: any) => { + contextMenu.show({ + id: triggerId, + event: e, + }); + }} + style={style} + className={classNames( + 'module-conversation-list-item', + unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null, + unreadCount > 0 && mentionedUs ? 'module-conversation-list-item--mentioned-us' : null, + isSelected ? 'module-conversation-list-item--is-selected' : null, + isBlocked ? 'module-conversation-list-item--is-blocked' : null + )} + > + +
+ + +
- ); - } -} + + + +
+ ); +}; -export const ConversationListItemWithDetails = usingClosedConversationDetails( - withTheme(ConversationListItem) +export const MemoConversationListItemWithDetails = usingClosedConversationDetails( + React.memo(ConversationListItem, _.isEqual) ); diff --git a/ts/components/MessageSearchResult.tsx b/ts/components/MessageSearchResult.tsx index 8370e0bce..6474b899d 100644 --- a/ts/components/MessageSearchResult.tsx +++ b/ts/components/MessageSearchResult.tsx @@ -108,7 +108,7 @@ class MessageSearchResultInner extends React.PureComponent {
{this.renderFrom()}
- +
diff --git a/ts/components/SearchResults.tsx b/ts/components/SearchResults.tsx index 79a571ca6..cd11f73d8 100644 --- a/ts/components/SearchResults.tsx +++ b/ts/components/SearchResults.tsx @@ -1,6 +1,9 @@ import React from 'react'; import { PropsForSearchResults } from '../state/ducks/conversations'; -import { ConversationListItemProps, ConversationListItemWithDetails } from './ConversationListItem'; +import { + ConversationListItemProps, + MemoConversationListItemWithDetails, +} from './ConversationListItem'; import { MessageSearchResult } from './MessageSearchResult'; export type SearchResultsProps = { @@ -46,7 +49,7 @@ export class SearchResults extends React.Component { {window.i18n('conversationsHeader')}
{conversations.map(conversation => ( - {
{header}
{items.map(contact => ( - ; currentNotificationSetting: ConversationNotificationSettingType; - hasNickname?: boolean; + hasNickname: boolean; isBlocked: boolean; @@ -68,32 +65,137 @@ interface Props { onGoBack: () => void; memberAvatars?: Array; // this is added by usingClosedConversationDetails - theme: DefaultTheme; } -class ConversationHeaderInner extends React.Component { - public constructor(props: Props) { - super(props); +const SelectionOverlay = (props: { + onDeleteSelectedMessages: () => void; + onCloseOverlay: () => void; + isPublic: boolean; +}) => { + const { onDeleteSelectedMessages, onCloseOverlay, isPublic } = props; + const { i18n } = window; + const theme = useTheme(); - autoBind(this); + const isServerDeletable = isPublic; + const deleteMessageButtonText = i18n(isServerDeletable ? 'deleteForEveryone' : 'delete'); + + return ( +
+
+ +
+ +
+ +
+
+ ); +}; + +const TripleDotsMenu = (props: { triggerId: string; showBackButton: boolean }) => { + const { showBackButton } = props; + const theme = useTheme(); + if (showBackButton) { + return <>; } + return ( +
{ + contextMenu.show({ + id: props.triggerId, + event: e, + }); + }} + > + +
+ ); +}; - public renderBackButton() { - const { onGoBack, showBackButton } = this.props; +const ExpirationLength = (props: { expirationSettingName?: string }) => { + const { expirationSettingName } = props; - if (!showBackButton) { - return null; - } + if (!expirationSettingName) { + return null; + } - return ( - +
+
{expirationSettingName}
+
+ ); +}; + +const AvatarHeader = (props: { + avatarPath?: string; + memberAvatars?: Array; + name?: string; + phoneNumber: string; + profileName?: string; + showBackButton: boolean; + onAvatarClick?: (pubkey: string) => void; +}) => { + const { avatarPath, memberAvatars, name, phoneNumber, profileName } = props; + const userName = name || profileName || phoneNumber; + + return ( + + { + // do not allow right panel to appear if another button is shown on the SessionConversation + if (props.onAvatarClick && !props.showBackButton) { + props.onAvatarClick(phoneNumber); + } + }} + memberAvatars={memberAvatars} + pubkey={phoneNumber} /> - ); + + ); +}; + +const BackButton = (props: { onGoBack: () => void; showBackButton: boolean }) => { + const { onGoBack, showBackButton } = props; + const theme = useTheme(); + if (!showBackButton) { + return null; + } + + return ( + + ); +}; + +class ConversationHeaderInner extends React.Component { + public constructor(props: Props) { + super(props); + + autoBind(this); } public renderTitle() { @@ -147,146 +249,65 @@ class ConversationHeaderInner extends React.Component { ); } - public renderAvatar() { - const { avatarPath, memberAvatars, name, phoneNumber, profileName } = this.props; - - const userName = name || profileName || phoneNumber; - - return ( - - { - this.onAvatarClick(phoneNumber); - }} - memberAvatars={memberAvatars} - pubkey={phoneNumber} - /> - - ); - } - - public renderExpirationLength() { - const { expirationSettingName } = this.props; - - if (!expirationSettingName) { - return null; - } - - return ( -
-
-
- {expirationSettingName} -
-
- ); - } - - public renderSelectionOverlay() { - const { onDeleteSelectedMessages, onCloseOverlay, isPublic } = this.props; - const { i18n } = window; - - const isServerDeletable = isPublic; - const deleteMessageButtonText = i18n(isServerDeletable ? 'deleteForEveryone' : 'delete'); - - return ( -
-
- -
- -
- -
-
- ); - } - public render() { - const { isKickedFromGroup, selectionMode } = this.props; + const { isKickedFromGroup, selectionMode, expirationSettingName, showBackButton } = this.props; const triggerId = 'conversation-header'; + console.warn('conversation header render', this.props); return (
- {this.renderBackButton()} + +
- {this.renderTripleDotsMenu(triggerId)} + {this.renderTitle()}
- {!isKickedFromGroup && this.renderExpirationLength()} - - {!selectionMode && this.renderAvatar()} - - + {!isKickedFromGroup && } + + {!selectionMode && ( + + )} + +
- {selectionMode && this.renderSelectionOverlay()} -
- ); - } - - public onAvatarClick(userPubKey: string) { - // do not allow right panel to appear if another button is shown on the SessionConversation - if (this.props.onAvatarClick && !this.props.showBackButton) { - this.props.onAvatarClick(userPubKey); - } - } - - public highlightMessageSearch() { - // This is a temporary fix. In future we want to search - // messages in the current conversation - ($('.session-search-input input') as any).focus(); - } - - private getHeaderMenuProps(triggerId: string): PropsConversationHeaderMenu { - return { - triggerId, - conversationId: this.props.id, - ...this.props, - }; - } - - private renderTripleDotsMenu(triggerId: string) { - const { showBackButton } = this.props; - if (showBackButton) { - return <>; - } - return ( -
{ - contextMenu.show({ - id: triggerId, - event: e, - }); - }} - > - + {selectionMode && ( + + )}
); } } export const ConversationHeaderWithDetails = usingClosedConversationDetails( - withTheme(ConversationHeaderInner) + ConversationHeaderInner ); diff --git a/ts/components/conversation/Timestamp.tsx b/ts/components/conversation/Timestamp.tsx index 276513500..b85f5ec6e 100644 --- a/ts/components/conversation/Timestamp.tsx +++ b/ts/components/conversation/Timestamp.tsx @@ -1,10 +1,9 @@ import React, { useState } from 'react'; -import classNames from 'classnames'; import moment from 'moment'; import { formatRelativeTime } from '../../util/formatRelativeTime'; import { useInterval } from '../../hooks/useInterval'; -import styled, { DefaultTheme } from 'styled-components'; +import styled, { useTheme } from 'styled-components'; import { OpacityMetadataComponent } from './message/MessageMetadata'; type Props = { @@ -13,7 +12,6 @@ type Props = { module?: string; withImageNoCaption?: boolean; isConversationListItem?: boolean; - theme: DefaultTheme; }; const UPDATE_FREQUENCY = 60 * 1000; @@ -52,6 +50,8 @@ export const Timestamp = (props: Props) => { setLastUpdated(Date.now()); }; + const theme = useTheme(); + useInterval(update, UPDATE_FREQUENCY); const { module, timestamp, withImageNoCaption, extended } = props; @@ -79,7 +79,7 @@ export const Timestamp = (props: Props) => { dateString = dateString.replace('minutes', 'mins').replace('minute', 'min'); } - const timestampColor = withImageNoCaption ? 'white' : props.theme.colors.textColor; + const timestampColor = withImageNoCaption ? 'white' : theme.colors.textColor; const title = moment(timestamp).format('llll'); if (props.isConversationListItem) { return ( diff --git a/ts/components/conversation/message/MessageMetadata.tsx b/ts/components/conversation/message/MessageMetadata.tsx index 6a27b2286..01eaae99a 100644 --- a/ts/components/conversation/message/MessageMetadata.tsx +++ b/ts/components/conversation/message/MessageMetadata.tsx @@ -91,7 +91,6 @@ export const MessageMetadata = (props: Props) => { extended={true} withImageNoCaption={withImageNoCaption} isConversationListItem={false} - theme={theme} /> )} { {showStatus ? ( { +const MessageStatusSending = (props: { iconColor: string }) => { return ( @@ -24,12 +23,11 @@ const MessageStatusSending = (props: { theme: DefaultTheme; iconColor: string }) ); }; -const MessageStatusSent = (props: { theme: DefaultTheme; iconColor: string }) => { +const MessageStatusSent = (props: { iconColor: string }) => { return ( @@ -37,12 +35,11 @@ const MessageStatusSent = (props: { theme: DefaultTheme; iconColor: string }) => ); }; -const MessageStatusRead = (props: { theme: DefaultTheme; iconColor: string }) => { +const MessageStatusRead = (props: { iconColor: string }) => { return ( @@ -50,12 +47,12 @@ const MessageStatusRead = (props: { theme: DefaultTheme; iconColor: string }) => ); }; -const MessageStatusError = (props: { theme: DefaultTheme }) => { +const MessageStatusError = () => { + const theme = useTheme(); return ( @@ -65,7 +62,6 @@ const MessageStatusError = (props: { theme: DefaultTheme }) => { export const OutgoingMessageStatus = (props: { status?: MessageDeliveryStatus; - theme: DefaultTheme; iconColor: string; isInMessageView?: boolean; }) => { @@ -80,7 +76,7 @@ export const OutgoingMessageStatus = (props: { if (props.isInMessageView) { return null; } - return ; + return ; default: return null; } diff --git a/ts/components/session/LeftPaneContactSection.tsx b/ts/components/session/LeftPaneContactSection.tsx index 826db465c..a3eabb4aa 100644 --- a/ts/components/session/LeftPaneContactSection.tsx +++ b/ts/components/session/LeftPaneContactSection.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { ConversationListItemWithDetails } from '../ConversationListItem'; +import { MemoConversationListItemWithDetails } from '../ConversationListItem'; import { RowRendererParamsType } from '../LeftPane'; import { AutoSizer, List } from 'react-virtualized'; import { ConversationType as ReduxConversationType } from '../../state/ducks/conversations'; @@ -38,7 +38,7 @@ export class LeftPaneContactSection extends React.Component { const item = directContacts[index]; return ( - { const conversation = conversations[index]; return ( - { const iconDimensions = getIconDimensionFromIconSize(iconSize); const iconDef = icons[iconType]; const ratio = iconDef?.ratio || 1; - if (!theme) { + if (!themeToUse) { window?.log?.error('Missing theme props in SessionIcon'); } diff --git a/ts/components/session/menu/ConversationHeaderMenu.tsx b/ts/components/session/menu/ConversationHeaderMenu.tsx index 836d1dd5c..e25249010 100644 --- a/ts/components/session/menu/ConversationHeaderMenu.tsx +++ b/ts/components/session/menu/ConversationHeaderMenu.tsx @@ -16,27 +16,27 @@ import { getRemoveModeratorsMenuItem, getUpdateGroupNameMenuItem, } from './Menu'; -import { NotificationForConvoOption, TimerOption } from '../../conversation/ConversationHeader'; +import { NotificationForConvoOption } from '../../conversation/ConversationHeader'; import { ConversationNotificationSettingType } from '../../../models/conversation'; +import _ from 'lodash'; export type PropsConversationHeaderMenu = { conversationId: string; triggerId: string; isMe: boolean; - isPublic?: boolean; - isKickedFromGroup?: boolean; - left?: boolean; + isPublic: boolean; + isKickedFromGroup: boolean; + left: boolean; isGroup: boolean; isAdmin: boolean; notificationForConvo: Array; currentNotificationSetting: ConversationNotificationSettingType; isPrivate: boolean; isBlocked: boolean; - theme: any; - hasNickname?: boolean; + hasNickname: boolean; }; -export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { +const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { const { conversationId, triggerId, @@ -54,32 +54,35 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { } = props; return ( - <> - - {getDisappearingMenuItem(isPublic, isKickedFromGroup, left, isBlocked, conversationId)} - {getNotificationForConvoMenuItem( - isKickedFromGroup, - left, - isBlocked, - notificationForConvo, - currentNotificationSetting, - conversationId - )} - {getBlockMenuItem(isMe, isPrivate, isBlocked, conversationId)} + + {getDisappearingMenuItem(isPublic, isKickedFromGroup, left, isBlocked, conversationId)} + {getNotificationForConvoMenuItem( + isKickedFromGroup, + left, + isBlocked, + notificationForConvo, + currentNotificationSetting, + conversationId + )} + {getBlockMenuItem(isMe, isPrivate, isBlocked, conversationId)} - {getCopyMenuItem(isPublic, isGroup, conversationId)} - {getMarkAllReadMenuItem(conversationId)} - {getChangeNicknameMenuItem(isMe, isGroup, conversationId)} - {getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)} - {getDeleteMessagesMenuItem(isPublic, conversationId)} - {getAddModeratorsMenuItem(isAdmin, isKickedFromGroup, conversationId)} - {getRemoveModeratorsMenuItem(isAdmin, isKickedFromGroup, conversationId)} - {getUpdateGroupNameMenuItem(isAdmin, isKickedFromGroup, left, conversationId)} - {getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)} - {/* TODO: add delete group */} - {getInviteContactMenuItem(isGroup, isPublic, conversationId)} - {getDeleteContactMenuItem(isMe, isGroup, isPublic, left, isKickedFromGroup, conversationId)} - - + {getCopyMenuItem(isPublic, isGroup, conversationId)} + {getMarkAllReadMenuItem(conversationId)} + {getChangeNicknameMenuItem(isMe, isGroup, conversationId)} + {getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)} + {getDeleteMessagesMenuItem(isPublic, conversationId)} + {getAddModeratorsMenuItem(isAdmin, isKickedFromGroup, conversationId)} + {getRemoveModeratorsMenuItem(isAdmin, isKickedFromGroup, conversationId)} + {getUpdateGroupNameMenuItem(isAdmin, isKickedFromGroup, left, conversationId)} + {getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)} + {/* TODO: add delete group */} + {getInviteContactMenuItem(isGroup, isPublic, conversationId)} + {getDeleteContactMenuItem(isMe, isGroup, isPublic, left, isKickedFromGroup, conversationId)} + ); }; + +function propsAreEqual(prev: PropsConversationHeaderMenu, next: PropsConversationHeaderMenu) { + return _.isEqual(prev, next); +} +export const MemoConversationHeaderMenu = React.memo(ConversationHeaderMenu, propsAreEqual); diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index b00546fdb..e3c6fe40e 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -1,5 +1,6 @@ -import React, { useState } from 'react'; +import React from 'react'; import { animation, Menu } from 'react-contexify'; +import _ from 'underscore'; import { ConversationTypeEnum } from '../../../models/conversation'; import { @@ -15,21 +16,20 @@ import { } from './Menu'; export type PropsContextConversationItem = { - id: string; + conversationId: string; triggerId: string; type: ConversationTypeEnum; isMe: boolean; - isPublic?: boolean; - isBlocked?: boolean; - hasNickname?: boolean; - isKickedFromGroup?: boolean; - left?: boolean; - theme?: any; + isPublic: boolean; + isBlocked: boolean; + hasNickname: boolean; + isKickedFromGroup: boolean; + left: boolean; }; -export const ConversationListItemContextMenu = (props: PropsContextConversationItem) => { +const ConversationListItemContextMenu = (props: PropsContextConversationItem) => { const { - id: conversationId, + conversationId, triggerId, isBlocked, isMe, @@ -38,25 +38,29 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI type, left, isKickedFromGroup, - theme, } = props; const isGroup = type === 'group'; - return ( - <> - - {getBlockMenuItem(isMe, type === ConversationTypeEnum.PRIVATE, isBlocked, conversationId)} - {getCopyMenuItem(isPublic, isGroup, conversationId)} - {getMarkAllReadMenuItem(conversationId)} - {getChangeNicknameMenuItem(isMe, isGroup, conversationId)} - {getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)} + + {getBlockMenuItem(isMe, type === ConversationTypeEnum.PRIVATE, isBlocked, conversationId)} + {getCopyMenuItem(isPublic, isGroup, conversationId)} + {getMarkAllReadMenuItem(conversationId)} + {getChangeNicknameMenuItem(isMe, isGroup, conversationId)} + {getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)} - {getDeleteMessagesMenuItem(isPublic, conversationId)} - {getInviteContactMenuItem(isGroup, isPublic, conversationId)} - {getDeleteContactMenuItem(isMe, isGroup, isPublic, left, isKickedFromGroup, conversationId)} - {getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)} - - + {getDeleteMessagesMenuItem(isPublic, conversationId)} + {getInviteContactMenuItem(isGroup, isPublic, conversationId)} + {getDeleteContactMenuItem(isMe, isGroup, isPublic, left, isKickedFromGroup, conversationId)} + {getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)} + ); }; + +function propsAreEqual(prev: PropsContextConversationItem, next: PropsContextConversationItem) { + return _.isEqual(prev, next); +} +export const MemoConversationListItemContextMenu = React.memo( + ConversationListItemContextMenu, + propsAreEqual +); diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 83ef7f8f1..371f5f30f 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -41,10 +41,6 @@ function showNotificationConvo( return !left && !isKickedFromGroup && !isBlocked; } -function showMemberMenu(isPublic: boolean, isGroup: boolean): boolean { - return !isPublic && isGroup; -} - function showBlock(isMe: boolean, isPrivate: boolean): boolean { return !isMe && isPrivate; } diff --git a/ts/session/constants.ts b/ts/session/constants.ts index df97e93d2..f65b4e708 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -32,7 +32,7 @@ export const PROTOCOLS = { // User Interface export const CONVERSATION = { DEFAULT_MEDIA_FETCH_COUNT: 50, - DEFAULT_DOCUMENTS_FETCH_COUNT: 150, + DEFAULT_DOCUMENTS_FETCH_COUNT: 100, DEFAULT_MESSAGE_FETCH_COUNT: 30, MAX_MESSAGE_FETCH_COUNT: 1000, // Maximum voice message duraton of 5 minutes diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 78234e87a..c71b18690 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -168,7 +168,7 @@ export interface ConversationType { id: string; name?: string; profileName?: string; - hasNickname?: boolean; + hasNickname: boolean; index?: number; activeAt?: number; @@ -176,7 +176,7 @@ export interface ConversationType { phoneNumber: string; type: ConversationTypeEnum; isMe: boolean; - isPublic?: boolean; + isPublic: boolean; unreadCount: number; mentionedUs: boolean; isSelected: boolean; diff --git a/ts/state/ducks/timerOptions.tsx b/ts/state/ducks/timerOptions.tsx index 1f7295688..ca161a53e 100644 --- a/ts/state/ducks/timerOptions.tsx +++ b/ts/state/ducks/timerOptions.tsx @@ -19,8 +19,8 @@ const timerOptionSlice = createSlice({ name: 'timerOptions', initialState: initialTimerOptionsState, reducers: { - updateTimerOptions: (_state, action: PayloadAction) => { - return { timerOptions: action.payload }; + updateTimerOptions: (state, action: PayloadAction) => { + return { ...state, timerOptions: action.payload }; }, }, }); diff --git a/ts/test/state/selectors/conversations_test.ts b/ts/test/state/selectors/conversations_test.ts index 99810915e..a629095d7 100644 --- a/ts/test/state/selectors/conversations_test.ts +++ b/ts/test/state/selectors/conversations_test.ts @@ -27,6 +27,8 @@ describe('state/selectors/conversations', () => { isBlocked: false, isKickedFromGroup: false, left: false, + hasNickname: false, + isPublic: false, }, id2: { id: 'id2', @@ -43,6 +45,8 @@ describe('state/selectors/conversations', () => { isBlocked: false, isKickedFromGroup: false, left: false, + hasNickname: false, + isPublic: false, }, id3: { id: 'id3', @@ -59,6 +63,8 @@ describe('state/selectors/conversations', () => { isBlocked: false, isKickedFromGroup: false, left: false, + hasNickname: false, + isPublic: false, }, id4: { id: 'id4', @@ -74,6 +80,8 @@ describe('state/selectors/conversations', () => { isBlocked: false, isKickedFromGroup: false, left: false, + hasNickname: false, + isPublic: false, }, id5: { id: 'id5', @@ -89,6 +97,8 @@ describe('state/selectors/conversations', () => { isBlocked: false, isKickedFromGroup: false, left: false, + hasNickname: false, + isPublic: false, }, }; const comparator = _getConversationComparator(i18n);