added a hook to fetch avatar of closed group members

This commit is contained in:
Audric Ackermann 2021-07-08 16:11:43 +10:00
parent 016461f506
commit 7f76ab274c
No known key found for this signature in database
GPG key ID: 999F434D76324AD4
19 changed files with 548 additions and 504 deletions

View file

@ -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<ConversationAvatar>; // 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<ConversationAvatar>;
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 (
<div className="module-conversation-list-item__avatar-container">
@ -58,7 +54,7 @@ const AvatarItem = (props: {
name={userName}
size={AvatarSize.S}
memberAvatars={memberAvatars}
pubkey={phoneNumber}
pubkey={conversationId}
/>
</div>
);
@ -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
)}
>
<UserItem isMe={isMe} phoneNumber={phoneNumber} name={name} profileName={profileName} />
<UserItem
isMe={isMe}
conversationId={conversationId}
name={name}
profileName={profileName}
/>
</div>
{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 (
<div key={key}>
<div
role="button"
onClick={() => {
window.inboxStore?.dispatch(openConversationExternal(id));
dispatch(openConversationExternal(conversationId));
}}
onContextMenu={(e: any) => {
contextMenu.show({
@ -232,9 +235,9 @@ const ConversationListItem = (props: Props) => {
)}
>
<AvatarItem
phoneNumber={phoneNumber}
conversationId={conversationId}
avatarPath={avatarPath}
memberAvatars={memberAvatars}
memberAvatars={membersAvatar}
profileName={profileName}
name={name}
/>
@ -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) => {
<Portal>
<MemoConversationListItemContextMenu
triggerId={triggerId}
conversationId={id}
conversationId={conversationId}
hasNickname={hasNickname}
isBlocked={isBlocked}
isKickedFromGroup={isKickedFromGroup}
@ -268,6 +271,4 @@ const ConversationListItem = (props: Props) => {
);
};
export const MemoConversationListItemWithDetails = usingClosedConversationDetails(
React.memo(ConversationListItem, _.isEqual)
);
export const MemoConversationListItemWithDetails = React.memo(ConversationListItem, _.isEqual);

View file

@ -6,20 +6,21 @@ 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<Props> {
public renderFromName() {
const { from, to } = this.props;
const FromName = (props: { from: FindAndFormatContactType; to: FindAndFormatContactType }) => {
const { from, to } = props;
if (from.isMe && to.isMe) {
return (
@ -29,9 +30,7 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
);
}
if (from.isMe) {
return (
<span className="module-message-search-result__header__name">{window.i18n('you')}</span>
);
return <span className="module-message-search-result__header__name">{window.i18n('you')}</span>;
}
return (
@ -44,11 +43,11 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
shouldShowPubkey={false}
/>
);
}
};
public renderFrom() {
const { to } = this.props;
const fromName = this.renderFromName();
const From = (props: { from: FindAndFormatContactType; to: FindAndFormatContactType }) => {
const { to, from } = props;
const fromName = <FromName from={from} to={to} />;
if (!to.isMe) {
return (
@ -67,10 +66,10 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
}
return <div className="module-message-search-result__header__from">{fromName}</div>;
}
};
public renderAvatar() {
const { from } = this.props;
const AvatarItem = (props: { from: FindAndFormatContactType }) => {
const { from } = props;
const userName = from.profileName || from.phoneNumber;
return (
@ -81,10 +80,11 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
pubkey={from.phoneNumber}
/>
);
}
};
export const MessageSearchResult = (props: Props) => {
const { from, id, isSelected, conversationId, receivedAt, snippet, to } = props;
public render() {
const { from, id, isSelected, conversationId, onClick, receivedAt, snippet, to } = this.props;
const dispatch = useDispatch();
if (!from || !to) {
return null;
@ -94,19 +94,17 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
<div
role="button"
onClick={() => {
if (onClick) {
onClick(conversationId, id);
}
dispatch(openConversationExternal(conversationId, id));
}}
className={classNames(
'module-message-search-result',
isSelected ? 'module-message-search-result--is-selected' : null
)}
>
{this.renderAvatar()}
<AvatarItem from={from} />
<div className="module-message-search-result__text">
<div className="module-message-search-result__header">
{this.renderFrom()}
<From from={from} to={to} />
<div className="module-message-search-result__header__timestamp">
<Timestamp timestamp={receivedAt} />
</div>
@ -117,7 +115,4 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
</div>
</div>
);
}
}
export const MessageSearchResult = withTheme(MessageSearchResultInner);
};

View file

@ -49,10 +49,7 @@ export class SearchResults extends React.Component<Props> {
{window.i18n('conversationsHeader')}
</div>
{conversations.map(conversation => (
<MemoConversationListItemWithDetails
{...conversation}
onClick={openConversationExternal}
/>
<MemoConversationListItemWithDetails {...conversation} />
))}
</div>
) : null}
@ -66,11 +63,7 @@ export class SearchResults extends React.Component<Props> {
</div>
)}
{messages.map(message => (
<MessageSearchResult
key={message.id}
{...message}
onClick={openConversationExternal}
/>
<MessageSearchResult key={message.id} {...message} />
))}
</div>
) : null}
@ -78,13 +71,11 @@ export class SearchResults extends React.Component<Props> {
);
}
private renderContacts(header: string, items: Array<ConversationListItemProps>) {
const { openConversationExternal } = this.props;
return (
<div className="module-search-results__contacts">
<div className="module-search-results__contacts-header">{header}</div>
{items.map(contact => (
<MemoConversationListItemWithDetails {...contact} onClick={openConversationExternal} />
<MemoConversationListItemWithDetails {...contact} />
))}
</div>
);

View file

@ -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<NotificationForConvoOption>;
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<ConversationAvatar>; // this is added by usingClosedConversationDetails
}
// memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
};
const SelectionOverlay = (props: {
onDeleteSelectedMessages: () => void;
@ -191,14 +194,24 @@ const BackButton = (props: { onGoBack: () => void; showBackButton: boolean }) =>
);
};
class ConversationHeaderInner extends React.Component<Props> {
public constructor(props: Props) {
super(props);
export type ConversationHeaderTitleProps = {
phoneNumber: string;
profileName?: string;
isMe: boolean;
isGroup: boolean;
isPublic: boolean;
members: Array<any>;
subscriberCount?: number;
isKickedFromGroup: boolean;
name?: string;
};
autoBind(this);
const ConversationHeaderTitle = () => {
const headerTitleProps = useSelector(getConversationHeaderTitleProps);
if (!headerTitleProps) {
return null;
}
public renderTitle() {
const {
phoneNumber,
profileName,
@ -209,7 +222,8 @@ class ConversationHeaderInner extends React.Component<Props> {
isMe,
isKickedFromGroup,
name,
} = this.props;
} = headerTitleProps;
const { i18n } = window;
if (isMe) {
@ -247,67 +261,106 @@ class ConversationHeaderInner extends React.Component<Props> {
{textEl}
</div>
);
}
};
public render() {
const { isKickedFromGroup, selectionMode, expirationSettingName, showBackButton } = this.props;
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';
console.warn('conversation header render', this.props);
return (
<div className="module-conversation-header">
<div className="conversation-header--items-wrapper">
<BackButton onGoBack={this.props.onGoBack} showBackButton={this.props.showBackButton} />
<BackButton onGoBack={onGoBack} showBackButton={showBackButton} />
<div className="module-conversation-header__title-container">
<div className="module-conversation-header__title-flex">
<TripleDotsMenu triggerId={triggerId} showBackButton={showBackButton} />
{this.renderTitle()}
<ConversationHeaderTitle />
</div>
</div>
{!isKickedFromGroup && <ExpirationLength expirationSettingName={expirationSettingName} />}
{!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}
onAvatarClick={onAvatarClick}
phoneNumber={phoneNumber}
showBackButton={showBackButton}
avatarPath={avatarPath}
memberAvatars={memberDetails}
name={name}
profileName={profileName}
/>
)}
<MemoConversationHeaderMenu
conversationId={this.props.id}
conversationId={id}
triggerId={triggerId}
isMe={this.props.isMe}
isPublic={this.props.isPublic}
isGroup={this.props.isGroup}
isMe={isMe}
isPublic={isPublic}
isGroup={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}
weAreAdmin={weAreAdmin}
isBlocked={isBlocked}
isPrivate={isPrivate}
left={left}
hasNickname={hasNickname}
notificationForConvo={notificationForConvo}
currentNotificationSetting={currentNotificationSetting}
/>
</div>
{selectionMode && (
<SelectionOverlay
isPublic={this.props.isPublic}
onCloseOverlay={this.props.onCloseOverlay}
onDeleteSelectedMessages={this.props.onDeleteSelectedMessages}
isPublic={isPublic}
onCloseOverlay={onCloseOverlay}
onDeleteSelectedMessages={onDeleteSelectedMessages}
/>
)}
</div>
);
}
}
export const ConversationHeaderWithDetails = usingClosedConversationDetails(
ConversationHeaderInner
);
};

View file

@ -37,13 +37,7 @@ export class LeftPaneContactSection extends React.Component<Props> {
const { directContacts } = this.props;
const item = directContacts[index];
return (
<MemoConversationListItemWithDetails
style={style}
{...item}
onClick={this.props.openConversationExternal}
/>
);
return <MemoConversationListItemWithDetails style={style} {...item} />;
};
private renderContacts() {

View file

@ -89,13 +89,7 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
const conversation = conversations[index];
return (
<MemoConversationListItemWithDetails
style={style}
{...conversation}
onClick={openConversationExternal}
/>
);
return <MemoConversationListItemWithDetails style={style} {...conversation} />;
};
public renderList(): JSX.Element | Array<JSX.Element | null> {

View file

@ -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<File>) => void;
showLeftPaneSection: (section: SectionType) => void;
showSettingsSection: (category: SessionSettingCategory) => void;
theme: DefaultTheme;
}
@ -834,7 +831,6 @@ export class SessionCompositionBox extends React.Component<Props, State> {
const { stagedLinkPreview } = this.state;
// Send message
this.props.onMessageSending();
const extractedQuotedMessageProps = _.pick(
quotedMessageProps,
'id',
@ -861,8 +857,6 @@ export class SessionCompositionBox extends React.Component<Props, State> {
{}
);
// Message sending sucess
this.props.onMessageSuccess();
this.props.clearAttachments();
// Empty composition box and stagedAttachments
@ -875,7 +869,6 @@ export class SessionCompositionBox extends React.Component<Props, State> {
} catch (e) {
// Message sending failed
window?.log?.error(e);
this.props.onMessageFailure();
}
}
@ -939,8 +932,8 @@ export class SessionCompositionBox extends React.Component<Props, State> {
}
ToastUtils.pushAudioPermissionNeeded(() => {
this.props.showLeftPaneSection(SectionType.Settings);
this.props.showSettingsSection(SessionSettingCategory.Privacy);
window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
});
}

View file

@ -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<string>;
@ -99,10 +88,6 @@ export class SessionConversation extends React.Component<Props, State> {
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<Props, State> {
return (
<SessionTheme theme={this.props.theme}>
<div className="conversation-header">{this.renderHeader()}</div>
{/* <SessionProgress
visible={this.state.messageProgressVisible}
value={this.state.sendingProgress}
prevValue={this.state.prevSendingProgress}
sendStatus={this.state.sendingProgressStatus}
resetProgress={this.resetSendingProgress}
/> */}
<div
// if you change the classname, also update it on onKeyDown
className={classNames('conversation-content', selectionMode && 'selection-mode')}
@ -285,13 +263,8 @@ export class SessionConversation extends React.Component<Props, State> {
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<Props, State> {
});
}
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<Props, State> {
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<Props, State> {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~ 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<string>, askUserForConfirmation: boolean) {
// Get message objects
const { selectedConversationKey, selectedConversation, messagesProps } = this.props;

View file

@ -41,13 +41,6 @@ interface Props {
messageContainerRef: React.RefObject<any>;
selectMessage: (messageId: string) => void;
deleteMessage: (messageId: string) => void;
fetchMessagesForConversation: ({
conversationKey,
count,
}: {
conversationKey: string;
count: number;
}) => void;
replyToMessage: (messageId: number) => Promise<void>;
showMessageDetails: (messageProps: any) => void;
onClickAttachment: (attachment: any, message: any) => void;
@ -428,7 +421,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
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<Props, State> {
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<Props, State> {
return scrollHeight - scrollTop - clientHeight;
}
}
function fetchMessagesForConversation(arg0: { conversationKey: string; count: number }): any {
throw new Error('Function not implemented.');
}

View file

@ -28,7 +28,7 @@ export type PropsConversationHeaderMenu = {
isKickedFromGroup: boolean;
left: boolean;
isGroup: boolean;
isAdmin: boolean;
weAreAdmin: boolean;
notificationForConvo: Array<NotificationForConvoOption>;
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)}

View file

@ -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 <WrappedComponent memberAvatars={this.state.memberAvatars} {...this.props} />;
}
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,

View file

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

View file

@ -381,10 +381,21 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
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<ConversationAttributes> {
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<ConversationAttributes> {
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<ConversationAttributes> {
left: !!this.get('left'),
groupAdmins,
members,
expireTimer: this.get('expireTimer') || 0,
subscriberCount: this.get('subscriberCount') || 0,
};
}

View file

@ -3,7 +3,7 @@ import { PubKey } from '../types';
import { getConversationController } from '../conversations';
import { fromHexToArray } from './String';
export async function getGroupMembers(groupId: PubKey): Promise<Array<PubKey>> {
export function getGroupMembers(groupId: PubKey): Array<PubKey> {
const groupConversation = getConversationController().get(groupId.key);
const groupMembers = groupConversation ? groupConversation.get('members') : undefined;

View file

@ -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<string>; // admins for closed groups and moderators for open groups
members?: Array<string>; // members for closed groups only
members: Array<string>; // members for closed groups only
currentNotificationSetting: ConversationNotificationSettingType;
notificationForConvo: Array<NotificationForConvoOption>;
}
export type ConversationLookupType = {

View file

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

View file

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

View file

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

View file

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