From 7f76ab274c09be5ad4702c03289cb8da1c5db779 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 8 Jul 2021 16:11:43 +1000 Subject: [PATCH] added a hook to fetch avatar of closed group members --- ts/components/ConversationListItem.tsx | 69 ++--- ts/components/MessageSearchResult.tsx | 179 ++++++----- ts/components/SearchResults.tsx | 15 +- .../conversation/ConversationHeader.tsx | 289 +++++++++++------- .../session/LeftPaneContactSection.tsx | 8 +- .../session/LeftPaneMessageSection.tsx | 8 +- .../conversation/SessionCompositionBox.tsx | 15 +- .../conversation/SessionConversation.tsx | 139 +-------- .../conversation/SessionMessagesList.tsx | 16 +- .../session/menu/ConversationHeaderMenu.tsx | 10 +- .../usingClosedConversationDetails.tsx | 15 +- ts/hooks/useMembersAvatar.tsx | 55 ++++ ts/models/conversation.ts | 23 +- ts/session/utils/Groups.ts | 2 +- ts/state/ducks/conversations.ts | 16 +- ts/state/selectors/conversations.ts | 93 +++--- ts/state/smart/MessageSearchResult.tsx | 23 -- .../session/unit/sending/MessageQueue_test.ts | 2 +- ts/test/state/selectors/conversations_test.ts | 61 ++++ 19 files changed, 541 insertions(+), 497 deletions(-) create mode 100644 ts/hooks/useMembersAvatar.tsx delete mode 100644 ts/state/smart/MessageSearchResult.tsx diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 720202f9b..22c0190cd 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -9,14 +9,11 @@ import { Timestamp } from './conversation/Timestamp'; import { ContactName } from './conversation/ContactName'; import { TypingAnimation } from './conversation/TypingAnimation'; -import { - ConversationAvatar, - usingClosedConversationDetails, -} from './session/usingClosedConversationDetails'; +import { ConversationAvatar } from './session/usingClosedConversationDetails'; import { MemoConversationListItemContextMenu } from './session/menu/ConversationListItemContextMenu'; import { createPortal } from 'react-dom'; import { OutgoingMessageStatus } from './conversation/message/OutgoingMessageStatus'; -import { DefaultTheme, useTheme } from 'styled-components'; +import { useTheme } from 'styled-components'; import { PubKey } from '../session/types'; import { ConversationType, @@ -24,11 +21,10 @@ import { openConversationExternal, } from '../state/ducks/conversations'; import _ from 'underscore'; +import { useMembersAvatars } from '../hooks/useMembersAvatar'; +import { useDispatch } from 'react-redux'; -export interface ConversationListItemProps extends ConversationType { - index?: number; // used to force a refresh when one conversation is removed on top of the list - memberAvatars?: Array; // this is added by usingClosedConversationDetails -} +export interface ConversationListItemProps extends ConversationType {} type PropsHousekeeping = { style?: Object; @@ -42,14 +38,14 @@ const Portal = ({ children }: { children: any }) => { const AvatarItem = (props: { avatarPath?: string; - phoneNumber: string; + conversationId: string; memberAvatars?: Array; name?: string; profileName?: string; }) => { - const { avatarPath, name, phoneNumber, profileName, memberAvatars } = props; + const { avatarPath, name, conversationId, profileName, memberAvatars } = props; - const userName = name || profileName || phoneNumber; + const userName = name || profileName || conversationId; return (
@@ -58,7 +54,7 @@ const AvatarItem = (props: { name={userName} size={AvatarSize.S} memberAvatars={memberAvatars} - pubkey={phoneNumber} + pubkey={conversationId} />
); @@ -68,13 +64,13 @@ const UserItem = (props: { name?: string; profileName?: string; isMe: boolean; - phoneNumber: string; + conversationId: string; }) => { - const { name, phoneNumber, profileName, isMe } = props; + const { name, conversationId, profileName, isMe } = props; - const shortenedPubkey = PubKey.shorten(phoneNumber); + const shortenedPubkey = PubKey.shorten(conversationId); - const displayedPubkey = profileName ? shortenedPubkey : phoneNumber; + const displayedPubkey = profileName ? shortenedPubkey : conversationId; const displayName = isMe ? window.i18n('noteToSelf') : profileName; let shouldShowPubkey = false; @@ -145,9 +141,9 @@ const HeaderItem = (props: { activeAt?: number; name?: string; profileName?: string; - phoneNumber: string; + conversationId: string; }) => { - const { unreadCount, mentionedUs, activeAt, isMe, phoneNumber, profileName, name } = props; + const { unreadCount, mentionedUs, activeAt, isMe, conversationId, profileName, name } = props; let atSymbol = null; let unreadCountDiv = null; @@ -164,7 +160,12 @@ const HeaderItem = (props: { unreadCount > 0 ? 'module-conversation-list-item__header__name--with-unread' : null )} > - + {unreadCountDiv} {atSymbol} @@ -183,12 +184,11 @@ const HeaderItem = (props: { }; const ConversationListItem = (props: Props) => { - console.warn('ConversationListItem', props.id.substr(-1), ': ', props); + // console.warn('ConversationListItem', props.id.substr(-1), ': ', props); const { activeAt, - phoneNumber, unreadCount, - id, + id: conversationId, isSelected, isBlocked, style, @@ -196,7 +196,6 @@ const ConversationListItem = (props: Props) => { isMe, name, profileName, - memberAvatars, isTyping, lastMessage, hasNickname, @@ -206,15 +205,19 @@ const ConversationListItem = (props: Props) => { isPublic, avatarPath, } = props; - const triggerId = `conversation-item-${phoneNumber}-ctxmenu`; - const key = `conversation-item-${phoneNumber}`; + const triggerId = `conversation-item-${conversationId}-ctxmenu`; + const key = `conversation-item-${conversationId}`; + + const membersAvatar = useMembersAvatars(props); + + const dispatch = useDispatch(); return (
{ - window.inboxStore?.dispatch(openConversationExternal(id)); + dispatch(openConversationExternal(conversationId)); }} onContextMenu={(e: any) => { contextMenu.show({ @@ -232,9 +235,9 @@ const ConversationListItem = (props: Props) => { )} > @@ -244,7 +247,7 @@ const ConversationListItem = (props: Props) => { unreadCount={unreadCount} activeAt={activeAt} isMe={isMe} - phoneNumber={phoneNumber} + conversationId={conversationId} name={name} profileName={profileName} /> @@ -254,7 +257,7 @@ const ConversationListItem = (props: Props) => { { ); }; -export const MemoConversationListItemWithDetails = usingClosedConversationDetails( - React.memo(ConversationListItem, _.isEqual) -); +export const MemoConversationListItemWithDetails = React.memo(ConversationListItem, _.isEqual); diff --git a/ts/components/MessageSearchResult.tsx b/ts/components/MessageSearchResult.tsx index 6474b899d..1787c1801 100644 --- a/ts/components/MessageSearchResult.tsx +++ b/ts/components/MessageSearchResult.tsx @@ -6,118 +6,113 @@ import { MessageBodyHighlight } from './MessageBodyHighlight'; import { Timestamp } from './conversation/Timestamp'; import { ContactName } from './conversation/ContactName'; -import { DefaultTheme, withTheme } from 'styled-components'; -import { PropsForSearchResults } from '../state/ducks/conversations'; +import { + FindAndFormatContactType, + openConversationExternal, + PropsForSearchResults, +} from '../state/ducks/conversations'; +import { useDispatch } from 'react-redux'; type PropsHousekeeping = { isSelected?: boolean; - theme: DefaultTheme; - onClick: (conversationId: string, messageId?: string) => void; }; type Props = PropsForSearchResults & PropsHousekeeping; -class MessageSearchResultInner extends React.PureComponent { - public renderFromName() { - const { from, to } = this.props; - - if (from.isMe && to.isMe) { - return ( - - {window.i18n('noteToSelf')} - - ); - } - if (from.isMe) { - return ( - {window.i18n('you')} - ); - } +const FromName = (props: { from: FindAndFormatContactType; to: FindAndFormatContactType }) => { + const { from, to } = props; + if (from.isMe && to.isMe) { return ( - // tslint:disable: use-simple-attributes - + + {window.i18n('noteToSelf')} + ); } - - public renderFrom() { - const { to } = this.props; - const fromName = this.renderFromName(); - - if (!to.isMe) { - return ( -
- {fromName} {window.i18n('to')}{' '} - - - -
- ); - } - - return
{fromName}
; + if (from.isMe) { + return {window.i18n('you')}; } - public renderAvatar() { - const { from } = this.props; - const userName = from.profileName || from.phoneNumber; + return ( + // tslint:disable: use-simple-attributes + + ); +}; + +const From = (props: { from: FindAndFormatContactType; to: FindAndFormatContactType }) => { + const { to, from } = props; + const fromName = ; + if (!to.isMe) { return ( - +
+ {fromName} {window.i18n('to')}{' '} + + + +
); } - public render() { - const { from, id, isSelected, conversationId, onClick, receivedAt, snippet, to } = this.props; + return
{fromName}
; +}; + +const AvatarItem = (props: { from: FindAndFormatContactType }) => { + const { from } = props; + const userName = from.profileName || from.phoneNumber; + + return ( + + ); +}; +export const MessageSearchResult = (props: Props) => { + const { from, id, isSelected, conversationId, receivedAt, snippet, to } = props; - if (!from || !to) { - return null; - } + const dispatch = useDispatch(); - return ( -
{ - if (onClick) { - onClick(conversationId, id); - } - }} - className={classNames( - 'module-message-search-result', - isSelected ? 'module-message-search-result--is-selected' : null - )} - > - {this.renderAvatar()} -
-
- {this.renderFrom()} -
- -
-
-
- + if (!from || !to) { + return null; + } + + return ( +
{ + dispatch(openConversationExternal(conversationId, id)); + }} + className={classNames( + 'module-message-search-result', + isSelected ? 'module-message-search-result--is-selected' : null + )} + > + +
+
+ +
+
+
+ +
- ); - } -} - -export const MessageSearchResult = withTheme(MessageSearchResultInner); +
+ ); +}; diff --git a/ts/components/SearchResults.tsx b/ts/components/SearchResults.tsx index ca4e3ba28..078c4260e 100644 --- a/ts/components/SearchResults.tsx +++ b/ts/components/SearchResults.tsx @@ -49,10 +49,7 @@ export class SearchResults extends React.Component { {window.i18n('conversationsHeader')}
{conversations.map(conversation => ( - + ))}
) : null} @@ -66,11 +63,7 @@ export class SearchResults extends React.Component {
)} {messages.map(message => ( - + ))}
) : null} @@ -78,13 +71,11 @@ export class SearchResults extends React.Component { ); } private renderContacts(header: string, items: Array) { - const { openConversationExternal } = this.props; - return (
{header}
{items.map(contact => ( - + ))}
); diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index dbcf3f742..f370c6389 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -5,15 +5,18 @@ import { Avatar, AvatarSize } from '../Avatar'; import { SessionIconButton, SessionIconSize, SessionIconType } from '../session/icon'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../session/SessionButton'; -import { - ConversationAvatar, - usingClosedConversationDetails, -} from '../session/usingClosedConversationDetails'; +import { ConversationAvatar } from '../session/usingClosedConversationDetails'; import { MemoConversationHeaderMenu } from '../session/menu/ConversationHeaderMenu'; import { contextMenu } from 'react-contexify'; import { useTheme } from 'styled-components'; import { ConversationNotificationSettingType } from '../../models/conversation'; -import autoBind from 'auto-bind'; +import { + getConversationHeaderProps, + getConversationHeaderTitleProps, + getSelectedConversation, +} from '../../state/selectors/conversations'; +import { useSelector } from 'react-redux'; +import { useMembersAvatars } from '../../hooks/useMembersAvatar'; export interface TimerOption { name: string; @@ -25,7 +28,7 @@ export interface NotificationForConvoOption { value: ConversationNotificationSettingType; } -interface Props { +export type ConversationHeaderProps = { id: string; name?: string; @@ -37,7 +40,7 @@ interface Props { isGroup: boolean; isPrivate: boolean; isPublic: boolean; - isAdmin: boolean; + weAreAdmin: boolean; // We might not always have the full list of members, // e.g. for open groups where we could have thousands @@ -48,7 +51,7 @@ interface Props { subscriberCount?: number; expirationSettingName?: string; - showBackButton: boolean; + // showBackButton: boolean; notificationForConvo: Array; currentNotificationSetting: ConversationNotificationSettingType; hasNickname: boolean; @@ -57,15 +60,15 @@ interface Props { isKickedFromGroup: boolean; left: boolean; - selectionMode: boolean; // is the UI on the message selection mode or not + // selectionMode: boolean; // is the UI on the message selection mode or not - onCloseOverlay: () => void; - onDeleteSelectedMessages: () => void; - onAvatarClick?: (pubkey: string) => void; - onGoBack: () => void; + // onCloseOverlay: () => void; + // onDeleteSelectedMessages: () => void; + // onAvatarClick?: (pubkey: string) => void; + // onGoBack: () => void; - memberAvatars?: Array; // this is added by usingClosedConversationDetails -} + // memberAvatars?: Array; // this is added by usingClosedConversationDetails +}; const SelectionOverlay = (props: { onDeleteSelectedMessages: () => void; @@ -191,123 +194,173 @@ const BackButton = (props: { onGoBack: () => void; showBackButton: boolean }) => ); }; -class ConversationHeaderInner extends React.Component { - public constructor(props: Props) { - super(props); +export type ConversationHeaderTitleProps = { + phoneNumber: string; + profileName?: string; + isMe: boolean; + isGroup: boolean; + isPublic: boolean; + members: Array; + subscriberCount?: number; + isKickedFromGroup: boolean; + name?: string; +}; + +const ConversationHeaderTitle = () => { + const headerTitleProps = useSelector(getConversationHeaderTitleProps); + if (!headerTitleProps) { + return null; + } + + const { + phoneNumber, + profileName, + isGroup, + isPublic, + members, + subscriberCount, + isMe, + isKickedFromGroup, + name, + } = headerTitleProps; + + const { i18n } = window; - autoBind(this); + if (isMe) { + return
{i18n('noteToSelf')}
; } - public renderTitle() { - const { - phoneNumber, - profileName, - isGroup, - isPublic, - members, - subscriberCount, - isMe, - isKickedFromGroup, - name, - } = this.props; - const { i18n } = window; - - if (isMe) { - return
{i18n('noteToSelf')}
; + const memberCount: number = (() => { + if (!isGroup) { + return 0; } - const memberCount: number = (() => { - if (!isGroup) { - return 0; - } - - if (isPublic) { - return subscriberCount || 0; - } else { - return members.length; - } - })(); - - let text = ''; - if (isGroup && memberCount > 0) { - const count = String(memberCount); - text = i18n('members', [count]); + if (isPublic) { + return subscriberCount || 0; + } else { + return members.length; } + })(); - const textEl = - text === '' || isKickedFromGroup ? null : ( - {text} - ); - - const title = profileName || name || phoneNumber; + let text = ''; + if (isGroup && memberCount > 0) { + const count = String(memberCount); + text = i18n('members', [count]); + } - return ( -
- {title} - {textEl} -
+ const textEl = + text === '' || isKickedFromGroup ? null : ( + {text} ); + + const title = profileName || name || phoneNumber; + + return ( +
+ {title} + {textEl} +
+ ); +}; + +export type ConversationHeaderNonReduxProps = { + showBackButton: boolean; + selectionMode: boolean; + onDeleteSelectedMessages: () => void; + onCloseOverlay: () => void; + onAvatarClick: () => void; + onGoBack: () => void; +}; + +export const ConversationHeaderWithDetails = ( + headerPropsNonRedux: ConversationHeaderNonReduxProps +) => { + const headerProps = useSelector(getConversationHeaderProps); + const selectedConversation = useSelector(getSelectedConversation); + const memberDetails = useMembersAvatars(selectedConversation); + + if (!headerProps) { + return null; } + const { + isKickedFromGroup, + expirationSettingName, + phoneNumber, + avatarPath, + name, + profileName, + id, + isMe, + isPublic, + notificationForConvo, + currentNotificationSetting, + hasNickname, + weAreAdmin, + isBlocked, + left, + isPrivate, + isGroup, + } = headerProps; + + const { + onGoBack, + onAvatarClick, + onCloseOverlay, + onDeleteSelectedMessages, + showBackButton, + selectionMode, + } = headerPropsNonRedux; + const triggerId = 'conversation-header'; - public render() { - const { isKickedFromGroup, selectionMode, expirationSettingName, showBackButton } = this.props; - const triggerId = 'conversation-header'; - console.warn('conversation header render', this.props); - - return ( -
-
- - -
-
- - {this.renderTitle()} -
+ return ( +
+
+ + +
+
+ +
- {!isKickedFromGroup && } - - {!selectionMode && ( - - )} - -
- - {selectionMode && ( - } + + {!selectionMode && ( + )} + +
- ); - } -} -export const ConversationHeaderWithDetails = usingClosedConversationDetails( - ConversationHeaderInner -); + {selectionMode && ( + + )} +
+ ); +}; diff --git a/ts/components/session/LeftPaneContactSection.tsx b/ts/components/session/LeftPaneContactSection.tsx index 0408b10c3..225aa63a4 100644 --- a/ts/components/session/LeftPaneContactSection.tsx +++ b/ts/components/session/LeftPaneContactSection.tsx @@ -37,13 +37,7 @@ export class LeftPaneContactSection extends React.Component { const { directContacts } = this.props; const item = directContacts[index]; - return ( - - ); + return ; }; private renderContacts() { diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index f42b0c4dc..6d86f13a7 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -89,13 +89,7 @@ export class LeftPaneMessageSection extends React.Component { const conversation = conversations[index]; - return ( - - ); + return ; }; public renderList(): JSX.Element | Array { diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index fd0a94882..f137dda26 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -37,6 +37,8 @@ import { getMentionsInput } from '../../../state/selectors/mentionsInput'; import { updateConfirmModal } from '../../../state/ducks/modalDialog'; import { SessionButtonColor } from '../SessionButton'; import { SessionConfirmDialogProps } from '../SessionConfirm'; +import { showLeftPaneSection, showSettingsSection } from '../../../state/ducks/section'; +import { pushAudioPermissionNeeded } from '../../../session/utils/Toast'; export interface ReplyingToMessageProps { convoId: string; @@ -62,9 +64,6 @@ export interface StagedAttachmentType extends AttachmentType { interface Props { sendMessage: any; - onMessageSending: any; - onMessageSuccess: any; - onMessageFailure: any; onLoadVoiceNoteView: any; onExitVoiceNoteView: any; @@ -84,8 +83,6 @@ interface Props { clearAttachments: () => any; removeAttachment: (toRemove: AttachmentType) => void; onChoseAttachments: (newAttachments: Array) => void; - showLeftPaneSection: (section: SectionType) => void; - showSettingsSection: (category: SessionSettingCategory) => void; theme: DefaultTheme; } @@ -834,7 +831,6 @@ export class SessionCompositionBox extends React.Component { const { stagedLinkPreview } = this.state; // Send message - this.props.onMessageSending(); const extractedQuotedMessageProps = _.pick( quotedMessageProps, 'id', @@ -861,8 +857,6 @@ export class SessionCompositionBox extends React.Component { {} ); - // Message sending sucess - this.props.onMessageSuccess(); this.props.clearAttachments(); // Empty composition box and stagedAttachments @@ -875,7 +869,6 @@ export class SessionCompositionBox extends React.Component { } catch (e) { // Message sending failed window?.log?.error(e); - this.props.onMessageFailure(); } } @@ -939,8 +932,8 @@ export class SessionCompositionBox extends React.Component { } ToastUtils.pushAudioPermissionNeeded(() => { - this.props.showLeftPaneSection(SectionType.Settings); - this.props.showSettingsSection(SessionSettingCategory.Privacy); + window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings)); + window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy)); }); } diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 6bdfdef6a..b9a0b2235 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -7,7 +7,10 @@ import { SessionCompositionBox, StagedAttachmentType } from './SessionCompositio import { Constants } from '../../../session'; import _ from 'lodash'; import { AttachmentUtil, GoogleChrome } from '../../../util'; -import { ConversationHeaderWithDetails } from '../../conversation/ConversationHeader'; +import { + ConversationHeaderNonReduxProps, + ConversationHeaderWithDetails, +} from '../../conversation/ConversationHeader'; import { SessionRightPanelWithDetails } from './SessionRightPanel'; import { SessionTheme } from '../../../state/ducks/SessionTheme'; import { DefaultTheme } from 'styled-components'; @@ -32,26 +35,12 @@ import { getMessageById, getPubkeysInPublicConversation } from '../../../data/da import autoBind from 'auto-bind'; import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager'; import { deleteOpenGroupMessages } from '../../../interactions/conversationInteractions'; -import { - ConversationNotificationSetting, - ConversationNotificationSettingType, - ConversationTypeEnum, -} from '../../../models/conversation'; +import { ConversationTypeEnum } from '../../../models/conversation'; import { updateMentionsMembers } from '../../../state/ducks/mentionsInput'; import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage'; import { SessionButtonColor } from '../SessionButton'; interface State { - // Message sending progress - messageProgressVisible: boolean; - sendingProgress: number; - prevSendingProgress: number; - // Sending failed: -1 - // Not send yet: 0 - // Sending message: 1 - // Sending success: 2 - sendingProgressStatus: -1 | 0 | 1 | 2; - unreadCount: number; selectedMessages: Array; @@ -99,10 +88,6 @@ export class SessionConversation extends React.Component { const unreadCount = this.props.selectedConversation?.unreadCount || 0; this.state = { - messageProgressVisible: false, - sendingProgress: 0, - prevSendingProgress: 0, - sendingProgressStatus: 0, unreadCount, selectedMessages: [], showOverlay: false, @@ -248,13 +233,6 @@ export class SessionConversation extends React.Component { return (
{this.renderHeader()}
- {/* */}
{ selectedConversation={selectedConversation} sendMessage={sendMessageFn} stagedAttachments={stagedAttachments} - onMessageSending={this.onMessageSending} - onMessageSuccess={this.onMessageSuccess} - onMessageFailure={this.onMessageFailure} onLoadVoiceNoteView={this.onLoadVoiceNoteView} onExitVoiceNoteView={this.onExitVoiceNoteView} - showLeftPaneSection={actions.showLeftPaneSection} - showSettingsSection={actions.showSettingsSection} quotedMessageProps={quotedMessageProps} removeQuotedMessage={() => { void this.replyToMessage(undefined); @@ -340,76 +313,28 @@ export class SessionConversation extends React.Component { }); } - public getHeaderProps() { - const { selectedConversationKey, ourNumber } = this.props; - const { selectedMessages, messageDetailShowProps } = this.state; - const conversation = getConversationController().getOrThrow(selectedConversationKey); - const expireTimer = conversation.get('expireTimer'); - const expirationSettingName = expireTimer - ? window.Whisper.ExpirationTimerOptions.getName(expireTimer || 0) - : null; - - const members = conversation.get('members') || []; - - // exclude mentions_only settings for private chats as this does not make much sense - const notificationForConvo = ConversationNotificationSetting.filter(n => - conversation.isPrivate() ? n !== 'mentions_only' : true - ).map((n: ConversationNotificationSettingType) => { - // this link to the notificationForConvo_all, notificationForConvo_mentions_only, ... - return { value: n, name: window.i18n(`notificationForConvo_${n}`) }; - }); + public getHeaderProps(): ConversationHeaderNonReduxProps { + console.warn('generating new header props'); const headerProps = { - id: conversation.id, - name: conversation.getName(), - phoneNumber: conversation.getNumber(), - profileName: conversation.getProfileName(), - avatarPath: conversation.getAvatarPath(), - isMe: conversation.isMe(), - isBlocked: conversation.isBlocked(), - isGroup: !conversation.isPrivate(), - isPrivate: conversation.isPrivate(), - isPublic: conversation.isPublic(), - isAdmin: conversation.isAdmin(ourNumber), - members, - subscriberCount: conversation.get('subscriberCount'), - isKickedFromGroup: conversation.get('isKickedFromGroup'), - left: conversation.get('left'), - expirationSettingName, - showBackButton: Boolean(messageDetailShowProps), - notificationForConvo, - currentNotificationSetting: conversation.get('triggerNotificationsFor'), - hasNickname: !!conversation.getNickname(), - selectionMode: !!selectedMessages.length, - + showBackButton: Boolean(this.state.messageDetailShowProps), + selectionMode: !!this.state.selectedMessages.length, onDeleteSelectedMessages: this.deleteSelectedMessages, - - onCloseOverlay: () => { - this.setState({ selectedMessages: [] }); - }, + onCloseOverlay: this.resetSelection, + onAvatarClick: this.toggleRightPanel, onGoBack: () => { this.setState({ messageDetailShowProps: undefined, }); }, - - onAvatarClick: (pubkey: any) => { - this.toggleRightPanel(); - }, }; return headerProps; } public getMessagesListProps() { - const { - selectedConversation, - selectedConversationKey, - ourNumber, - messagesProps, - actions, - } = this.props; + const { selectedConversation, selectedConversationKey, ourNumber, messagesProps } = this.props; const { quotedMessageTimestamp, selectedMessages } = this.state; return { @@ -422,7 +347,6 @@ export class SessionConversation extends React.Component { conversation: selectedConversation as ConversationType, selectMessage: this.selectMessage, deleteMessage: this.deleteMessage, - fetchMessagesForConversation: actions.fetchMessagesForConversation, replyToMessage: this.replyToMessage, showMessageDetails: this.showMessageDetails, onClickAttachment: this.onClickAttachment, @@ -475,45 +399,6 @@ export class SessionConversation extends React.Component { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~ MESSAGE HANDLING ~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - public updateSendingProgress(value: number, status: -1 | 0 | 1 | 2) { - // If you're sending a new message, reset previous value to zero - const prevSendingProgress = status === 1 ? 0 : this.state.sendingProgress; - - this.setState({ - sendingProgress: value, - prevSendingProgress, - sendingProgressStatus: status, - }); - } - - public resetSendingProgress() { - this.setState({ - sendingProgress: 0, - prevSendingProgress: 0, - sendingProgressStatus: 0, - }); - } - - public onMessageSending() { - // Set sending state 5% to show message sending - const initialValue = 5; - this.updateSendingProgress(initialValue, 1); - if (this.state.quotedMessageTimestamp) { - this.setState({ - quotedMessageTimestamp: undefined, - quotedMessageProps: undefined, - }); - } - } - - public onMessageSuccess() { - this.updateSendingProgress(100, 2); - } - - public onMessageFailure() { - this.updateSendingProgress(100, -1); - } - public async deleteMessagesById(messageIds: Array, askUserForConfirmation: boolean) { // Get message objects const { selectedConversationKey, selectedConversation, messagesProps } = this.props; diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index be573cab0..f7b7f12f2 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -41,13 +41,6 @@ interface Props { messageContainerRef: React.RefObject; selectMessage: (messageId: string) => void; deleteMessage: (messageId: string) => void; - fetchMessagesForConversation: ({ - conversationKey, - count, - }: { - conversationKey: string; - count: number; - }) => void; replyToMessage: (messageId: number) => Promise; showMessageDetails: (messageProps: any) => void; onClickAttachment: (attachment: any, message: any) => void; @@ -428,7 +421,7 @@ export class SessionMessagesList extends React.Component { private async handleScroll() { const messageContainer = this.messageContainerRef?.current; - const { fetchMessagesForConversation, conversationKey } = this.props; + const { conversationKey } = this.props; if (!messageContainer) { return; } @@ -472,7 +465,9 @@ export class SessionMessagesList extends React.Component { const oldLen = messagesProps.length; const previousTopMessage = messagesProps[oldLen - 1]?.propsForMessage.id; - fetchMessagesForConversation({ conversationKey, count: numMessages }); + window.inboxStore?.dispatch( + fetchMessagesForConversation({ conversationKey, count: numMessages }) + ); if (previousTopMessage && oldLen !== messagesProps.length) { this.scrollToMessage(previousTopMessage); } @@ -632,3 +627,6 @@ export class SessionMessagesList extends React.Component { return scrollHeight - scrollTop - clientHeight; } } +function fetchMessagesForConversation(arg0: { conversationKey: string; count: number }): any { + throw new Error('Function not implemented.'); +} diff --git a/ts/components/session/menu/ConversationHeaderMenu.tsx b/ts/components/session/menu/ConversationHeaderMenu.tsx index e25249010..2a5325d8a 100644 --- a/ts/components/session/menu/ConversationHeaderMenu.tsx +++ b/ts/components/session/menu/ConversationHeaderMenu.tsx @@ -28,7 +28,7 @@ export type PropsConversationHeaderMenu = { isKickedFromGroup: boolean; left: boolean; isGroup: boolean; - isAdmin: boolean; + weAreAdmin: boolean; notificationForConvo: Array; currentNotificationSetting: ConversationNotificationSettingType; isPrivate: boolean; @@ -44,7 +44,7 @@ const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { isPublic, isGroup, isKickedFromGroup, - isAdmin, + weAreAdmin, isBlocked, isPrivate, left, @@ -71,9 +71,9 @@ const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { {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)} + {getAddModeratorsMenuItem(weAreAdmin, isKickedFromGroup, conversationId)} + {getRemoveModeratorsMenuItem(weAreAdmin, isKickedFromGroup, conversationId)} + {getUpdateGroupNameMenuItem(weAreAdmin, isKickedFromGroup, left, conversationId)} {getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)} {/* TODO: add delete group */} {getInviteContactMenuItem(isGroup, isPublic, conversationId)} diff --git a/ts/components/session/usingClosedConversationDetails.tsx b/ts/components/session/usingClosedConversationDetails.tsx index 4aba88613..0742fdfdd 100644 --- a/ts/components/session/usingClosedConversationDetails.tsx +++ b/ts/components/session/usingClosedConversationDetails.tsx @@ -3,7 +3,6 @@ import { PubKey } from '../../session/types'; import React from 'react'; import * as _ from 'lodash'; import { getConversationController } from '../../session/conversations'; -import { ConversationTypeEnum } from '../../models/conversation'; export type ConversationAvatar = { avatarPath?: string; @@ -25,24 +24,24 @@ export function usingClosedConversationDetails(WrappedComponent: any) { } public componentDidMount() { - void this.fetchClosedConversationDetails(); + this.fetchClosedConversationDetails(); } public componentWillReceiveProps() { - void this.fetchClosedConversationDetails(); + this.fetchClosedConversationDetails(); } public render() { return ; } - private async fetchClosedConversationDetails() { + private fetchClosedConversationDetails() { const { isPublic, type, conversationType, isGroup, phoneNumber, id } = this.props; if (!isPublic && (conversationType === 'group' || type === 'group' || isGroup)) { const groupId = id || phoneNumber; const ourPrimary = UserUtils.getOurPubKeyFromCache(); - let members = await GroupUtils.getGroupMembers(PubKey.cast(groupId)); + let members = GroupUtils.getGroupMembers(PubKey.cast(groupId)); const ourself = members.find(m => m.key !== ourPrimary.key); // add ourself back at the back, so it's shown only if only 1 member and we are still a member @@ -53,11 +52,7 @@ export function usingClosedConversationDetails(WrappedComponent: any) { } // no need to forward more than 2 conversations for rendering the group avatar members = members.slice(0, 2); - const memberConvos = await Promise.all( - members.map(async m => - getConversationController().getOrCreateAndWait(m.key, ConversationTypeEnum.PRIVATE) - ) - ); + const memberConvos = _.compact(members.map(m => getConversationController().get(m.key))); const memberAvatars = memberConvos.map(m => { return { avatarPath: m.getAvatar()?.url || undefined, diff --git a/ts/hooks/useMembersAvatar.tsx b/ts/hooks/useMembersAvatar.tsx new file mode 100644 index 000000000..47ed402ba --- /dev/null +++ b/ts/hooks/useMembersAvatar.tsx @@ -0,0 +1,55 @@ +import _ from 'lodash'; +import { useEffect, useState } from 'react'; +import { getConversationController } from '../session/conversations'; +import { UserUtils } from '../session/utils'; +import { ConversationType } from '../state/ducks/conversations'; + +export function useMembersAvatars(conversation: ConversationType | undefined) { + const [membersAvatars, setMembersAvatars] = useState< + | Array<{ + avatarPath: string | undefined; + id: string; + name: string; + }> + | undefined + >(undefined); + + useEffect( + () => { + if (!conversation) { + setMembersAvatars(undefined); + return; + } + const { isPublic, isGroup, members: convoMembers } = conversation; + if (!isPublic && isGroup) { + const ourPrimary = UserUtils.getOurPubKeyStrFromCache(); + + const ourself = convoMembers.find(m => m !== ourPrimary); + // add ourself back at the back, so it's shown only if only 1 member and we are still a member + let membersFiltered = convoMembers.filter(m => m !== ourPrimary); + membersFiltered.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + if (ourself) { + membersFiltered.push(ourPrimary); + } + // no need to forward more than 2 conversations for rendering the group avatar + membersFiltered = membersFiltered.slice(0, 2); + const memberConvos = _.compact( + membersFiltered.map(m => getConversationController().get(m)) + ); + const memberAvatars = memberConvos.map(m => { + return { + avatarPath: m.getAvatar()?.url || undefined, + id: m.id as string, + name: (m.get('name') || m.get('profileName') || m.id) as string, + }; + }); + setMembersAvatars(memberAvatars); + } else { + setMembersAvatars(undefined); + } + }, + conversation ? [conversation.members, conversation.id] : [] + ); + + return membersAvatars; +} diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 2a23cbb90..c71505222 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -381,10 +381,21 @@ export class ConversationModel extends Backbone.Model { } return this.get('moderators'); } + public getProps(): ReduxConversationType { const groupAdmins = this.getGroupAdmins(); - const members = this.isGroup() && !this.isPublic() ? this.get('members') : undefined; + const members = this.isGroup() && !this.isPublic() ? this.get('members') : []; + + // exclude mentions_only settings for private chats as this does not make much sense + const notificationForConvo = ConversationNotificationSetting.filter(n => + this.isPrivate() ? n !== 'mentions_only' : true + ).map((n: ConversationNotificationSettingType) => { + // this link to the notificationForConvo_all, notificationForConvo_mentions_only, ... + return { value: n, name: window.i18n(`notificationForConvo_${n}`) }; + }); + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); + // isSelected is overriden by redux return { isSelected: false, @@ -392,6 +403,12 @@ export class ConversationModel extends Backbone.Model { activeAt: this.get('active_at'), avatarPath: this.getAvatarPath() || undefined, type: this.isPrivate() ? ConversationTypeEnum.PRIVATE : ConversationTypeEnum.GROUP, + weAreAdmin: this.isAdmin(ourNumber), + isGroup: !this.isPrivate(), + currentNotificationSetting: this.get('triggerNotificationsFor'), + + notificationForConvo, + isPrivate: this.isPrivate(), isMe: this.isMe(), isPublic: this.isPublic(), isTyping: !!this.typingTimer, @@ -401,7 +418,7 @@ export class ConversationModel extends Backbone.Model { unreadCount: this.get('unreadCount') || 0, mentionedUs: this.get('mentionedUs') || false, isBlocked: this.isBlocked(), - phoneNumber: this.id, + phoneNumber: this.getNumber(), lastMessage: { status: this.get('lastMessageStatus'), text: this.get('lastMessage'), @@ -411,6 +428,8 @@ export class ConversationModel extends Backbone.Model { left: !!this.get('left'), groupAdmins, members, + expireTimer: this.get('expireTimer') || 0, + subscriberCount: this.get('subscriberCount') || 0, }; } diff --git a/ts/session/utils/Groups.ts b/ts/session/utils/Groups.ts index fc257046a..3db66c20f 100644 --- a/ts/session/utils/Groups.ts +++ b/ts/session/utils/Groups.ts @@ -3,7 +3,7 @@ import { PubKey } from '../types'; import { getConversationController } from '../conversations'; import { fromHexToArray } from './String'; -export async function getGroupMembers(groupId: PubKey): Promise> { +export function getGroupMembers(groupId: PubKey): Array { const groupConversation = getConversationController().get(groupId.key); const groupMembers = groupConversation ? groupConversation.get('members') : undefined; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 8e300fa6f..245305e19 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -5,12 +5,16 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { getConversationController } from '../../session/conversations'; import { MessageModel } from '../../models/message'; import { getMessagesByConversation } from '../../data/data'; -import { ConversationTypeEnum } from '../../models/conversation'; +import { + ConversationNotificationSettingType, + ConversationTypeEnum, +} from '../../models/conversation'; import { MessageDeliveryStatus, MessageModelType, PropsForDataExtractionNotification, } from '../../models/messageType'; +import { NotificationForConvoOption } from '../../components/conversation/ConversationHeader'; export type MessageModelProps = { propsForMessage: PropsForMessage; @@ -176,17 +180,25 @@ export interface ConversationType { type: ConversationTypeEnum; isMe: boolean; isPublic: boolean; + isGroup: boolean; + isPrivate: boolean; + weAreAdmin: boolean; unreadCount: number; mentionedUs: boolean; isSelected: boolean; + expireTimer: number; isTyping: boolean; isBlocked: boolean; isKickedFromGroup: boolean; + subscriberCount: number; left: boolean; avatarPath?: string; // absolute filepath to the avatar groupAdmins?: Array; // admins for closed groups and moderators for open groups - members?: Array; // members for closed groups only + members: Array; // members for closed groups only + + currentNotificationSetting: ConversationNotificationSettingType; + notificationForConvo: Array; } export type ConversationLookupType = { diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index b1d6a6d59..df2d03557 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -13,6 +13,10 @@ import { getIntl, getOurNumber } from './user'; import { BlockedNumberController } from '../../util'; import { ConversationTypeEnum } from '../../models/conversation'; import { LocalizerType } from '../../types/Util'; +import { + ConversationHeaderProps, + ConversationHeaderTitleProps, +} from '../../components/conversation/ConversationHeader'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; @@ -159,35 +163,6 @@ export const _getLeftPaneLists = ( }; }; -export const _getSessionConversationInfo = ( - lookup: ConversationLookupType, - comparator: (left: ConversationType, right: ConversationType) => number, - selectedConversation?: string -): { - conversation: ConversationType | undefined; - selectedConversation?: string; -} => { - const values = Object.values(lookup); - const sorted = values.sort(comparator); - - let conversation; - const max = sorted.length; - - for (let i = 0; i < max; i += 1) { - const conv = sorted[i]; - - if (conv.id === selectedConversation) { - conversation = conv; - break; - } - } - - return { - conversation, - selectedConversation, - }; -}; - export const getLeftPaneLists = createSelector( getConversationLookup, getConversationComparator, @@ -195,13 +170,6 @@ export const getLeftPaneLists = createSelector( _getLeftPaneLists ); -export const getSessionConversationInfo = createSelector( - getConversationLookup, - getConversationComparator, - getSelectedConversationKey, - _getSessionConversationInfo -); - export const getMe = createSelector( [getConversationLookup, getOurNumber], (lookup: ConversationLookupType, ourNumber: string): ConversationType => { @@ -212,3 +180,56 @@ export const getMe = createSelector( export const getUnreadMessageCount = createSelector(getLeftPaneLists, (state): number => { return state.unreadCount; }); + +export const getConversationHeaderTitleProps = createSelector(getSelectedConversation, (state): + | ConversationHeaderTitleProps + | undefined => { + if (!state) { + return undefined; + } + return { + isKickedFromGroup: state.isKickedFromGroup, + phoneNumber: state.phoneNumber, + isMe: state.isMe, + members: state.members || [], + isPublic: state.isPublic, + profileName: state.profileName, + name: state.name, + subscriberCount: state.subscriberCount, + isGroup: state.type === 'group', + }; +}); + +export const getConversationHeaderProps = createSelector(getSelectedConversation, (state): + | ConversationHeaderProps + | undefined => { + if (!state) { + return undefined; + } + + const expirationSettingName = state.expireTimer + ? window.Whisper.ExpirationTimerOptions.getName(state.expireTimer || 0) + : null; + + return { + id: state.id, + isPrivate: state.isPrivate, + notificationForConvo: state.notificationForConvo, + currentNotificationSetting: state.currentNotificationSetting, + isBlocked: state.isBlocked, + left: state.left, + avatarPath: state.avatarPath, + expirationSettingName: expirationSettingName, + hasNickname: state.hasNickname, + weAreAdmin: state.weAreAdmin, + isKickedFromGroup: state.isKickedFromGroup, + phoneNumber: state.phoneNumber, + isMe: state.isMe, + members: state.members || [], + isPublic: state.isPublic, + profileName: state.profileName, + name: state.name, + subscriberCount: state.subscriberCount, + isGroup: state.isGroup, + }; +}); diff --git a/ts/state/smart/MessageSearchResult.tsx b/ts/state/smart/MessageSearchResult.tsx deleted file mode 100644 index 3a45803b0..000000000 --- a/ts/state/smart/MessageSearchResult.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { connect } from 'react-redux'; -import { mapDispatchToProps } from '../actions'; - -import { StateType } from '../reducer'; - -import { MessageSearchResult } from '../../components/MessageSearchResult'; - -type SmartProps = { - id: string; -}; - -function mapStateToProps(state: StateType, ourProps: SmartProps) { - const { id } = ourProps; - const lookup = state.search && state.search.messageLookup; - if (!lookup) { - return null; - } - - return lookup[id]; -} -const smart = connect(mapStateToProps, mapDispatchToProps); - -export const SmartMessageSearchResult = smart(MessageSearchResult); diff --git a/ts/test/session/unit/sending/MessageQueue_test.ts b/ts/test/session/unit/sending/MessageQueue_test.ts index e140b7e6c..e130df192 100644 --- a/ts/test/session/unit/sending/MessageQueue_test.ts +++ b/ts/test/session/unit/sending/MessageQueue_test.ts @@ -187,7 +187,7 @@ describe('MessageQueue', () => { describe('closed groups', () => { it('can send to closed group', async () => { const members = TestUtils.generateFakePubKeys(4).map(p => new PubKey(p.key)); - sandbox.stub(GroupUtils, 'getGroupMembers').resolves(members); + sandbox.stub(GroupUtils, 'getGroupMembers').returns(members); const send = sandbox.stub(messageQueueStub, 'sendToPubKey').resolves(); diff --git a/ts/test/state/selectors/conversations_test.ts b/ts/test/state/selectors/conversations_test.ts index a629095d7..831176444 100644 --- a/ts/test/state/selectors/conversations_test.ts +++ b/ts/test/state/selectors/conversations_test.ts @@ -9,6 +9,7 @@ import { describe('state/selectors/conversations', () => { describe('#getLeftPaneList', () => { + // tslint:disable-next-line: max-func-body-length it('sorts conversations based on timestamp then by intl-friendly title', () => { const i18n = (key: string) => key; const data: ConversationLookupType = { @@ -29,6 +30,18 @@ describe('state/selectors/conversations', () => { left: false, hasNickname: false, isPublic: false, + subscriberCount: 0, + currentNotificationSetting: 'all', + weAreAdmin: false, + isGroup: false, + isPrivate: false, + notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', + groupAdmins: [], + lastMessage: undefined, + members: [], + profileName: 'df', + expireTimer: 0, }, id2: { id: 'id2', @@ -47,6 +60,18 @@ describe('state/selectors/conversations', () => { left: false, hasNickname: false, isPublic: false, + subscriberCount: 0, + currentNotificationSetting: 'all', + weAreAdmin: false, + isGroup: false, + isPrivate: false, + notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', + groupAdmins: [], + lastMessage: undefined, + members: [], + profileName: 'df', + expireTimer: 0, }, id3: { id: 'id3', @@ -65,6 +90,18 @@ describe('state/selectors/conversations', () => { left: false, hasNickname: false, isPublic: false, + subscriberCount: 0, + currentNotificationSetting: 'all', + weAreAdmin: false, + isGroup: false, + isPrivate: false, + notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', + groupAdmins: [], + lastMessage: undefined, + members: [], + profileName: 'df', + expireTimer: 0, }, id4: { id: 'id4', @@ -82,6 +119,18 @@ describe('state/selectors/conversations', () => { left: false, hasNickname: false, isPublic: false, + subscriberCount: 0, + currentNotificationSetting: 'all', + weAreAdmin: false, + isGroup: false, + isPrivate: false, + notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', + groupAdmins: [], + expireTimer: 0, + lastMessage: undefined, + members: [], + profileName: 'df', }, id5: { id: 'id5', @@ -99,6 +148,18 @@ describe('state/selectors/conversations', () => { left: false, hasNickname: false, isPublic: false, + subscriberCount: 0, + expireTimer: 0, + currentNotificationSetting: 'all', + weAreAdmin: false, + isGroup: false, + isPrivate: false, + notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', + groupAdmins: [], + lastMessage: undefined, + members: [], + profileName: 'df', }, }; const comparator = _getConversationComparator(i18n);