improve performamce by memoizing avatar and menus

This commit is contained in:
Audric Ackermann 2021-07-06 17:14:00 +10:00
parent 87a8385629
commit 97b9156562
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4
18 changed files with 503 additions and 418 deletions

View File

@ -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) => {
</div>
);
};
export const Avatar = React.memo(AvatarInner, _.isEqual);

View File

@ -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<Props> {
public constructor(props: Props) {
super(props);
const AvatarItem = (props: {
avatarPath?: string;
phoneNumber: string;
memberAvatars?: Array<ConversationAvatar>;
name?: string;
profileName?: string;
}) => {
const { avatarPath, name, phoneNumber, profileName, memberAvatars } = props;
const userName = name || profileName || phoneNumber;
return (
<div className="module-conversation-list-item__avatar-container">
<Avatar
avatarPath={avatarPath}
name={userName}
size={AvatarSize.S}
memberAvatars={memberAvatars}
pubkey={phoneNumber}
/>
</div>
);
};
const UserItem = (props: {
name?: string;
profileName?: string;
isMe: boolean;
phoneNumber: string;
}) => {
const { name, phoneNumber, profileName, isMe } = 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;
}
public renderAvatar() {
const { avatarPath, name, phoneNumber, profileName, memberAvatars } = this.props;
return (
<div className="module-conversation__user">
<ContactName
phoneNumber={displayedPubkey}
name={name}
profileName={displayName}
module="module-conversation__user"
boldProfileName={true}
shouldShowPubkey={shouldShowPubkey}
/>
</div>
);
};
const userName = name || profileName || phoneNumber;
const MessageItem = (props: {
isTyping: boolean;
lastMessage?: LastMessageType;
unreadCount: number;
}) => {
const { lastMessage, isTyping, unreadCount } = props;
return (
<div className="module-conversation-list-item__avatar-container">
<Avatar
const theme = useTheme();
if (!lastMessage && !isTyping) {
return null;
}
const text = lastMessage && lastMessage.text ? lastMessage.text : '';
if (isEmpty(text)) {
return null;
}
return (
<div className="module-conversation-list-item__message">
<div
className={classNames(
'module-conversation-list-item__message__text',
unreadCount > 0 ? 'module-conversation-list-item__message__text--has-unread' : null
)}
>
{isTyping ? (
<TypingAnimation />
) : (
<MessageBody isGroup={true} text={text} disableJumbomoji={true} disableLinks={true} />
)}
</div>
{lastMessage && lastMessage.status ? (
<OutgoingMessageStatus
status={lastMessage.status}
iconColor={theme.colors.textColorSubtle}
/>
) : null}
</div>
);
};
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 ? <p className="at-symbol">@</p> : null;
unreadCountDiv = <p className="module-conversation-list-item__unread-count">{unreadCount}</p>;
}
return (
<div className="module-conversation-list-item__header">
<div
className={classNames(
'module-conversation-list-item__header__name',
unreadCount > 0 ? 'module-conversation-list-item__header__name--with-unread' : null
)}
>
<UserItem isMe={isMe} phoneNumber={phoneNumber} name={name} profileName={profileName} />
</div>
{unreadCountDiv}
{atSymbol}
{
<div
className={classNames(
'module-conversation-list-item__header__date',
unreadCount > 0 ? 'module-conversation-list-item__header__date--has-unread' : null
)}
>
{<Timestamp timestamp={activeAt} extended={false} isConversationListItem={true} />}
</div>
}
</div>
);
};
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 (
<div key={key}>
<div
role="button"
onClick={() => {
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
)}
>
<AvatarItem
phoneNumber={phoneNumber}
avatarPath={avatarPath}
name={userName}
size={AvatarSize.S}
memberAvatars={memberAvatars}
pubkey={phoneNumber}
/>
</div>
);
}
public renderHeader() {
const { unreadCount, mentionedUs, activeAt } = this.props;
let atSymbol = null;
let unreadCountDiv = null;
if (unreadCount > 0) {
atSymbol = mentionedUs ? <p className="at-symbol">@</p> : null;
unreadCountDiv = <p className="module-conversation-list-item__unread-count">{unreadCount}</p>;
}
return (
<div className="module-conversation-list-item__header">
<div
className={classNames(
'module-conversation-list-item__header__name',
unreadCount > 0 ? 'module-conversation-list-item__header__name--with-unread' : null
)}
>
{this.renderUser()}
</div>
{unreadCountDiv}
{atSymbol}
{
<div
className={classNames(
'module-conversation-list-item__header__date',
unreadCount > 0 ? 'module-conversation-list-item__header__date--has-unread' : null
)}
>
{
<Timestamp
timestamp={activeAt}
extended={false}
isConversationListItem={true}
theme={this.props.theme}
/>
}
</div>
}
</div>
);
}
public renderMessage() {
const { lastMessage, isTyping, unreadCount } = this.props;
if (!lastMessage && !isTyping) {
return null;
}
const text = lastMessage && lastMessage.text ? lastMessage.text : '';
if (isEmpty(text)) {
return null;
}
return (
<div className="module-conversation-list-item__message">
<div
className={classNames(
'module-conversation-list-item__message__text',
unreadCount > 0 ? 'module-conversation-list-item__message__text--has-unread' : null
)}
>
{isTyping ? (
<TypingAnimation />
) : (
<MessageBody isGroup={true} text={text} disableJumbomoji={true} disableLinks={true} />
)}
</div>
{lastMessage && lastMessage.status ? (
<OutgoingMessageStatus
status={lastMessage.status}
iconColor={this.props.theme.colors.textColorSubtle}
theme={this.props.theme}
/>
) : null}
</div>
);
}
public render() {
const { phoneNumber, unreadCount, id, isSelected, isBlocked, style, mentionedUs } = this.props;
const triggerId = `conversation-item-${phoneNumber}-ctxmenu`;
const key = `conversation-item-${phoneNumber}`;
return (
<div key={key}>
<div
role="button"
onClick={() => {
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
)}
>
{this.renderAvatar()}
<div className="module-conversation-list-item__content">
{this.renderHeader()}
{this.renderMessage()}
</div>
</div>
<Portal>
<ConversationListItemContextMenu {...this.getMenuProps(triggerId)} />
</Portal>
</div>
);
}
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;
}
return (
<div className="module-conversation__user">
<ContactName
phoneNumber={displayedPubkey}
profileName={profileName}
name={name}
profileName={displayName}
module="module-conversation__user"
boldProfileName={true}
shouldShowPubkey={shouldShowPubkey}
/>
<div className="module-conversation-list-item__content">
<HeaderItem
mentionedUs={mentionedUs}
unreadCount={unreadCount}
activeAt={activeAt}
isMe={isMe}
phoneNumber={phoneNumber}
name={name}
profileName={profileName}
/>
<MessageItem isTyping={isTyping} unreadCount={unreadCount} lastMessage={lastMessage} />
</div>
</div>
);
}
}
<Portal>
<MemoConversationListItemContextMenu
triggerId={triggerId}
conversationId={id}
hasNickname={hasNickname}
isBlocked={isBlocked}
isKickedFromGroup={isKickedFromGroup}
isMe={isMe}
isPublic={isPublic}
left={left}
type={type}
/>
</Portal>
</div>
);
};
export const ConversationListItemWithDetails = usingClosedConversationDetails(
withTheme(ConversationListItem)
export const MemoConversationListItemWithDetails = usingClosedConversationDetails(
React.memo(ConversationListItem, _.isEqual)
);

View File

@ -108,7 +108,7 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
<div className="module-message-search-result__header">
{this.renderFrom()}
<div className="module-message-search-result__header__timestamp">
<Timestamp timestamp={receivedAt} theme={this.props.theme} />
<Timestamp timestamp={receivedAt} />
</div>
</div>
<div className="module-message-search-result__body">

View File

@ -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<Props> {
{window.i18n('conversationsHeader')}
</div>
{conversations.map(conversation => (
<ConversationListItemWithDetails
<MemoConversationListItemWithDetails
key={conversation.phoneNumber}
{...conversation}
onClick={openConversationExternal}
@ -82,7 +85,7 @@ export class SearchResults extends React.Component<Props> {
<div className="module-search-results__contacts">
<div className="module-search-results__contacts-header">{header}</div>
{items.map(contact => (
<ConversationListItemWithDetails
<MemoConversationListItemWithDetails
key={contact.phoneNumber}
{...contact}
onClick={openConversationExternal}

View File

@ -9,12 +9,9 @@ import {
ConversationAvatar,
usingClosedConversationDetails,
} from '../session/usingClosedConversationDetails';
import {
ConversationHeaderMenu,
PropsConversationHeaderMenu,
} from '../session/menu/ConversationHeaderMenu';
import { MemoConversationHeaderMenu } from '../session/menu/ConversationHeaderMenu';
import { contextMenu } from 'react-contexify';
import { DefaultTheme, withTheme } from 'styled-components';
import { useTheme } from 'styled-components';
import { ConversationNotificationSettingType } from '../../models/conversation';
import autoBind from 'auto-bind';
@ -54,7 +51,7 @@ interface Props {
showBackButton: boolean;
notificationForConvo: Array<NotificationForConvoOption>;
currentNotificationSetting: ConversationNotificationSettingType;
hasNickname?: boolean;
hasNickname: boolean;
isBlocked: boolean;
@ -68,9 +65,132 @@ interface Props {
onGoBack: () => void;
memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
theme: DefaultTheme;
}
const SelectionOverlay = (props: {
onDeleteSelectedMessages: () => void;
onCloseOverlay: () => void;
isPublic: boolean;
}) => {
const { onDeleteSelectedMessages, onCloseOverlay, isPublic } = props;
const { i18n } = window;
const theme = useTheme();
const isServerDeletable = isPublic;
const deleteMessageButtonText = i18n(isServerDeletable ? 'deleteForEveryone' : 'delete');
return (
<div className="message-selection-overlay">
<div className="close-button">
<SessionIconButton
iconType={SessionIconType.Exit}
iconSize={SessionIconSize.Medium}
onClick={onCloseOverlay}
theme={theme}
/>
</div>
<div className="button-group">
<SessionButton
buttonType={SessionButtonType.Default}
buttonColor={SessionButtonColor.Danger}
text={deleteMessageButtonText}
onClick={onDeleteSelectedMessages}
/>
</div>
</div>
);
};
const TripleDotsMenu = (props: { triggerId: string; showBackButton: boolean }) => {
const { showBackButton } = props;
const theme = useTheme();
if (showBackButton) {
return <></>;
}
return (
<div
role="button"
onClick={(e: any) => {
contextMenu.show({
id: props.triggerId,
event: e,
});
}}
>
<SessionIconButton
iconType={SessionIconType.Ellipses}
iconSize={SessionIconSize.Medium}
theme={theme}
/>
</div>
);
};
const ExpirationLength = (props: { expirationSettingName?: string }) => {
const { expirationSettingName } = props;
if (!expirationSettingName) {
return null;
}
return (
<div className="module-conversation-header__expiration">
<div className="module-conversation-header__expiration__clock-icon" />
<div className="module-conversation-header__expiration__setting">{expirationSettingName}</div>
</div>
);
};
const AvatarHeader = (props: {
avatarPath?: string;
memberAvatars?: Array<ConversationAvatar>;
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 (
<span className="module-conversation-header__avatar">
<Avatar
avatarPath={avatarPath}
name={userName}
size={AvatarSize.S}
onAvatarClick={() => {
// 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}
/>
</span>
);
};
const BackButton = (props: { onGoBack: () => void; showBackButton: boolean }) => {
const { onGoBack, showBackButton } = props;
const theme = useTheme();
if (!showBackButton) {
return null;
}
return (
<SessionIconButton
iconType={SessionIconType.Chevron}
iconSize={SessionIconSize.Large}
iconRotation={90}
onClick={onGoBack}
theme={theme}
/>
);
};
class ConversationHeaderInner extends React.Component<Props> {
public constructor(props: Props) {
super(props);
@ -78,24 +198,6 @@ class ConversationHeaderInner extends React.Component<Props> {
autoBind(this);
}
public renderBackButton() {
const { onGoBack, showBackButton } = this.props;
if (!showBackButton) {
return null;
}
return (
<SessionIconButton
iconType={SessionIconType.Chevron}
iconSize={SessionIconSize.Large}
iconRotation={90}
onClick={onGoBack}
theme={this.props.theme}
/>
);
}
public renderTitle() {
const {
phoneNumber,
@ -147,146 +249,65 @@ class ConversationHeaderInner extends React.Component<Props> {
);
}
public renderAvatar() {
const { avatarPath, memberAvatars, name, phoneNumber, profileName } = this.props;
const userName = name || profileName || phoneNumber;
return (
<span className="module-conversation-header__avatar">
<Avatar
avatarPath={avatarPath}
name={userName}
size={AvatarSize.S}
onAvatarClick={() => {
this.onAvatarClick(phoneNumber);
}}
memberAvatars={memberAvatars}
pubkey={phoneNumber}
/>
</span>
);
}
public renderExpirationLength() {
const { expirationSettingName } = this.props;
if (!expirationSettingName) {
return null;
}
return (
<div className="module-conversation-header__expiration">
<div className="module-conversation-header__expiration__clock-icon" />
<div className="module-conversation-header__expiration__setting">
{expirationSettingName}
</div>
</div>
);
}
public renderSelectionOverlay() {
const { onDeleteSelectedMessages, onCloseOverlay, isPublic } = this.props;
const { i18n } = window;
const isServerDeletable = isPublic;
const deleteMessageButtonText = i18n(isServerDeletable ? 'deleteForEveryone' : 'delete');
return (
<div className="message-selection-overlay">
<div className="close-button">
<SessionIconButton
iconType={SessionIconType.Exit}
iconSize={SessionIconSize.Medium}
onClick={onCloseOverlay}
theme={this.props.theme}
/>
</div>
<div className="button-group">
<SessionButton
buttonType={SessionButtonType.Default}
buttonColor={SessionButtonColor.Danger}
text={deleteMessageButtonText}
onClick={onDeleteSelectedMessages}
/>
</div>
</div>
);
}
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 (
<div className="module-conversation-header">
<div className="conversation-header--items-wrapper">
{this.renderBackButton()}
<BackButton onGoBack={this.props.onGoBack} showBackButton={this.props.showBackButton} />
<div className="module-conversation-header__title-container">
<div className="module-conversation-header__title-flex">
{this.renderTripleDotsMenu(triggerId)}
<TripleDotsMenu triggerId={triggerId} showBackButton={showBackButton} />
{this.renderTitle()}
</div>
</div>
{!isKickedFromGroup && this.renderExpirationLength()}
{!isKickedFromGroup && <ExpirationLength expirationSettingName={expirationSettingName} />}
{!selectionMode && this.renderAvatar()}
{!selectionMode && (
<AvatarHeader
onAvatarClick={this.props.onAvatarClick}
phoneNumber={this.props.phoneNumber}
showBackButton={this.props.showBackButton}
avatarPath={this.props.avatarPath}
memberAvatars={this.props.memberAvatars}
name={this.props.name}
profileName={this.props.profileName}
/>
)}
<ConversationHeaderMenu {...this.getHeaderMenuProps(triggerId)} />
<MemoConversationHeaderMenu
conversationId={this.props.id}
triggerId={triggerId}
isMe={this.props.isMe}
isPublic={this.props.isPublic}
isGroup={this.props.isGroup}
isKickedFromGroup={isKickedFromGroup}
isAdmin={this.props.isAdmin}
isBlocked={this.props.isBlocked}
isPrivate={this.props.isPrivate}
left={this.props.left}
hasNickname={this.props.hasNickname}
notificationForConvo={this.props.notificationForConvo}
currentNotificationSetting={this.props.currentNotificationSetting}
/>
</div>
{selectionMode && this.renderSelectionOverlay()}
</div>
);
}
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 (
<div
role="button"
onClick={(e: any) => {
contextMenu.show({
id: triggerId,
event: e,
});
}}
>
<SessionIconButton
iconType={SessionIconType.Ellipses}
iconSize={SessionIconSize.Medium}
theme={this.props.theme}
/>
{selectionMode && (
<SelectionOverlay
isPublic={this.props.isPublic}
onCloseOverlay={this.props.onCloseOverlay}
onDeleteSelectedMessages={this.props.onDeleteSelectedMessages}
/>
)}
</div>
);
}
}
export const ConversationHeaderWithDetails = usingClosedConversationDetails(
withTheme(ConversationHeaderInner)
ConversationHeaderInner
);

View File

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

View File

@ -91,7 +91,6 @@ export const MessageMetadata = (props: Props) => {
extended={true}
withImageNoCaption={withImageNoCaption}
isConversationListItem={false}
theme={theme}
/>
)}
<MetadataBadges
@ -115,7 +114,6 @@ export const MessageMetadata = (props: Props) => {
{showStatus ? (
<OutgoingMessageStatus
iconColor={messageStatusColor}
theme={theme}
status={status}
// do not show the error status, another component is shown on the right of the message itself here
isInMessageView={true}

View File

@ -1,5 +1,5 @@
import React from 'react';
import styled, { DefaultTheme } from 'styled-components';
import styled, { DefaultTheme, useTheme } from 'styled-components';
import { MessageDeliveryStatus } from '../../../models/messageType';
import { SessionIcon, SessionIconSize, SessionIconType } from '../../session/icon';
import { OpacityMetadataComponent } from './MessageMetadata';
@ -10,13 +10,12 @@ const MessageStatusSendingContainer = styled(props => <OpacityMetadataComponent
margin-inline-start: 5px;
`;
const MessageStatusSending = (props: { theme: DefaultTheme; iconColor: string }) => {
const MessageStatusSending = (props: { iconColor: string }) => {
return (
<MessageStatusSendingContainer>
<SessionIcon
rotateDuration={2}
iconColor={props.iconColor}
theme={props.theme}
iconType={SessionIconType.Sending}
iconSize={SessionIconSize.Tiny}
/>
@ -24,12 +23,11 @@ const MessageStatusSending = (props: { theme: DefaultTheme; iconColor: string })
);
};
const MessageStatusSent = (props: { theme: DefaultTheme; iconColor: string }) => {
const MessageStatusSent = (props: { iconColor: string }) => {
return (
<MessageStatusSendingContainer>
<SessionIcon
iconColor={props.iconColor}
theme={props.theme}
iconType={SessionIconType.CircleCheck}
iconSize={SessionIconSize.Tiny}
/>
@ -37,12 +35,11 @@ const MessageStatusSent = (props: { theme: DefaultTheme; iconColor: string }) =>
);
};
const MessageStatusRead = (props: { theme: DefaultTheme; iconColor: string }) => {
const MessageStatusRead = (props: { iconColor: string }) => {
return (
<MessageStatusSendingContainer>
<SessionIcon
iconColor={props.iconColor}
theme={props.theme}
iconType={SessionIconType.DoubleCheckCircleFilled}
iconSize={SessionIconSize.Tiny}
/>
@ -50,12 +47,12 @@ const MessageStatusRead = (props: { theme: DefaultTheme; iconColor: string }) =>
);
};
const MessageStatusError = (props: { theme: DefaultTheme }) => {
const MessageStatusError = () => {
const theme = useTheme();
return (
<MessageStatusSendingContainer>
<SessionIcon
iconColor={props.theme.colors.destructive}
theme={props.theme}
iconColor={theme.colors.destructive}
iconType={SessionIconType.Error}
iconSize={SessionIconSize.Tiny}
/>
@ -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 <MessageStatusError {...props} />;
return <MessageStatusError />;
default:
return null;
}

View File

@ -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<Props> {
const item = directContacts[index];
return (
<ConversationListItemWithDetails
<MemoConversationListItemWithDetails
key={item.id}
style={style}
{...item}

View File

@ -5,7 +5,7 @@ import { AutoSizer, List } from 'react-virtualized';
import { MainViewController } from '../MainViewController';
import {
ConversationListItemProps,
ConversationListItemWithDetails,
MemoConversationListItemWithDetails,
} from '../ConversationListItem';
import { ConversationType as ReduxConversationType } from '../../state/ducks/conversations';
import { SearchResults, SearchResultsProps } from '../SearchResults';
@ -90,7 +90,7 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
const conversation = conversations[index];
return (
<ConversationListItemWithDetails
<MemoConversationListItemWithDetails
key={key}
style={style}
{...conversation}

View File

@ -188,7 +188,7 @@ export const SessionIcon = (props: SessionIconProps) => {
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');
}

View File

@ -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<NotificationForConvoOption>;
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 (
<>
<Menu id={triggerId} animation={animation.fade}>
{getDisappearingMenuItem(isPublic, isKickedFromGroup, left, isBlocked, conversationId)}
{getNotificationForConvoMenuItem(
isKickedFromGroup,
left,
isBlocked,
notificationForConvo,
currentNotificationSetting,
conversationId
)}
{getBlockMenuItem(isMe, isPrivate, isBlocked, conversationId)}
<Menu id={triggerId} animation={animation.fade}>
{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)}
</Menu>
</>
{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)}
</Menu>
);
};
function propsAreEqual(prev: PropsConversationHeaderMenu, next: PropsConversationHeaderMenu) {
return _.isEqual(prev, next);
}
export const MemoConversationHeaderMenu = React.memo(ConversationHeaderMenu, propsAreEqual);

View File

@ -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 (
<>
<Menu id={triggerId} animation={animation.fade}>
{getBlockMenuItem(isMe, type === ConversationTypeEnum.PRIVATE, isBlocked, conversationId)}
{getCopyMenuItem(isPublic, isGroup, conversationId)}
{getMarkAllReadMenuItem(conversationId)}
{getChangeNicknameMenuItem(isMe, isGroup, conversationId)}
{getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)}
<Menu id={triggerId} animation={animation.fade}>
{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)}
</Menu>
</>
{getDeleteMessagesMenuItem(isPublic, conversationId)}
{getInviteContactMenuItem(isGroup, isPublic, conversationId)}
{getDeleteContactMenuItem(isMe, isGroup, isPublic, left, isKickedFromGroup, conversationId)}
{getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)}
</Menu>
);
};
function propsAreEqual(prev: PropsContextConversationItem, next: PropsContextConversationItem) {
return _.isEqual(prev, next);
}
export const MemoConversationListItemContextMenu = React.memo(
ConversationListItemContextMenu,
propsAreEqual
);

View File

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

View File

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

View File

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

View File

@ -19,8 +19,8 @@ const timerOptionSlice = createSlice({
name: 'timerOptions',
initialState: initialTimerOptionsState,
reducers: {
updateTimerOptions: (_state, action: PayloadAction<TimerOptionsArray>) => {
return { timerOptions: action.payload };
updateTimerOptions: (state, action: PayloadAction<TimerOptionsArray>) => {
return { ...state, timerOptions: action.payload };
},
},
});

View File

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