mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
added a hook to fetch avatar of closed group members
This commit is contained in:
parent
016461f506
commit
7f76ab274c
19 changed files with 548 additions and 504 deletions
|
@ -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);
|
||||
|
|
|
@ -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<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 (
|
||||
<span className="module-message-search-result__header__name">
|
||||
{window.i18n('noteToSelf')}
|
||||
if (from.isMe && to.isMe) {
|
||||
return (
|
||||
<span className="module-message-search-result__header__name">
|
||||
{window.i18n('noteToSelf')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (from.isMe) {
|
||||
return <span className="module-message-search-result__header__name">{window.i18n('you')}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
// tslint:disable: use-simple-attributes
|
||||
<ContactName
|
||||
phoneNumber={from.phoneNumber}
|
||||
name={from.name || ''}
|
||||
profileName={from.profileName || ''}
|
||||
module="module-message-search-result__header__name"
|
||||
shouldShowPubkey={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const From = (props: { from: FindAndFormatContactType; to: FindAndFormatContactType }) => {
|
||||
const { to, from } = props;
|
||||
const fromName = <FromName from={from} to={to} />;
|
||||
|
||||
if (!to.isMe) {
|
||||
return (
|
||||
<div className="module-message-search-result__header__from">
|
||||
{fromName} {window.i18n('to')}{' '}
|
||||
<span className="module-mesages-search-result__header__group">
|
||||
<ContactName
|
||||
phoneNumber={to.phoneNumber}
|
||||
name={to.name || ''}
|
||||
profileName={to.profileName || ''}
|
||||
shouldShowPubkey={false}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (from.isMe) {
|
||||
return (
|
||||
<span className="module-message-search-result__header__name">{window.i18n('you')}</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// tslint:disable: use-simple-attributes
|
||||
<ContactName
|
||||
phoneNumber={from.phoneNumber}
|
||||
name={from.name || ''}
|
||||
profileName={from.profileName || ''}
|
||||
module="module-message-search-result__header__name"
|
||||
shouldShowPubkey={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderFrom() {
|
||||
const { to } = this.props;
|
||||
const fromName = this.renderFromName();
|
||||
|
||||
if (!to.isMe) {
|
||||
return (
|
||||
<div className="module-message-search-result__header__from">
|
||||
{fromName} {window.i18n('to')}{' '}
|
||||
<span className="module-mesages-search-result__header__group">
|
||||
<ContactName
|
||||
phoneNumber={to.phoneNumber}
|
||||
name={to.name || ''}
|
||||
profileName={to.profileName || ''}
|
||||
shouldShowPubkey={false}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="module-message-search-result__header__from">{fromName}</div>;
|
||||
}
|
||||
|
||||
public renderAvatar() {
|
||||
const { from } = this.props;
|
||||
const userName = from.profileName || from.phoneNumber;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
avatarPath={from.avatarPath || ''}
|
||||
name={userName}
|
||||
size={AvatarSize.S}
|
||||
pubkey={from.phoneNumber}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { from, id, isSelected, conversationId, onClick, receivedAt, snippet, to } = this.props;
|
||||
|
||||
if (!from || !to) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => {
|
||||
if (onClick) {
|
||||
onClick(conversationId, id);
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
'module-message-search-result',
|
||||
isSelected ? 'module-message-search-result--is-selected' : null
|
||||
)}
|
||||
>
|
||||
{this.renderAvatar()}
|
||||
<div className="module-message-search-result__text">
|
||||
<div className="module-message-search-result__header">
|
||||
{this.renderFrom()}
|
||||
<div className="module-message-search-result__header__timestamp">
|
||||
<Timestamp timestamp={receivedAt} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-message-search-result__body">
|
||||
<MessageBodyHighlight text={snippet || ''} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const MessageSearchResult = withTheme(MessageSearchResultInner);
|
||||
return <div className="module-message-search-result__header__from">{fromName}</div>;
|
||||
};
|
||||
|
||||
const AvatarItem = (props: { from: FindAndFormatContactType }) => {
|
||||
const { from } = props;
|
||||
const userName = from.profileName || from.phoneNumber;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
avatarPath={from.avatarPath || ''}
|
||||
name={userName}
|
||||
size={AvatarSize.S}
|
||||
pubkey={from.phoneNumber}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const MessageSearchResult = (props: Props) => {
|
||||
const { from, id, isSelected, conversationId, receivedAt, snippet, to } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
if (!from || !to) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => {
|
||||
dispatch(openConversationExternal(conversationId, id));
|
||||
}}
|
||||
className={classNames(
|
||||
'module-message-search-result',
|
||||
isSelected ? 'module-message-search-result--is-selected' : null
|
||||
)}
|
||||
>
|
||||
<AvatarItem from={from} />
|
||||
<div className="module-message-search-result__text">
|
||||
<div className="module-message-search-result__header">
|
||||
<From from={from} to={to} />
|
||||
<div className="module-message-search-result__header__timestamp">
|
||||
<Timestamp timestamp={receivedAt} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-message-search-result__body">
|
||||
<MessageBodyHighlight text={snippet || ''} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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,123 +194,173 @@ 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,
|
||||
isGroup,
|
||||
isPublic,
|
||||
members,
|
||||
subscriberCount,
|
||||
isMe,
|
||||
isKickedFromGroup,
|
||||
name,
|
||||
} = this.props;
|
||||
const { i18n } = window;
|
||||
const {
|
||||
phoneNumber,
|
||||
profileName,
|
||||
isGroup,
|
||||
isPublic,
|
||||
members,
|
||||
subscriberCount,
|
||||
isMe,
|
||||
isKickedFromGroup,
|
||||
name,
|
||||
} = headerTitleProps;
|
||||
|
||||
if (isMe) {
|
||||
return <div className="module-conversation-header__title">{i18n('noteToSelf')}</div>;
|
||||
const { i18n } = window;
|
||||
|
||||
if (isMe) {
|
||||
return <div className="module-conversation-header__title">{i18n('noteToSelf')}</div>;
|
||||
}
|
||||
|
||||
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 : (
|
||||
<span className="module-conversation-header__title-text">{text}</span>
|
||||
);
|
||||
let text = '';
|
||||
if (isGroup && memberCount > 0) {
|
||||
const count = String(memberCount);
|
||||
text = i18n('members', [count]);
|
||||
}
|
||||
|
||||
const title = profileName || name || phoneNumber;
|
||||
|
||||
return (
|
||||
<div className="module-conversation-header__title">
|
||||
<span className="module-contact-name__profile-name">{title}</span>
|
||||
{textEl}
|
||||
</div>
|
||||
const textEl =
|
||||
text === '' || isKickedFromGroup ? null : (
|
||||
<span className="module-conversation-header__title-text">{text}</span>
|
||||
);
|
||||
|
||||
const title = profileName || name || phoneNumber;
|
||||
|
||||
return (
|
||||
<div className="module-conversation-header__title">
|
||||
<span className="module-contact-name__profile-name">{title}</span>
|
||||
{textEl}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
public render() {
|
||||
const { isKickedFromGroup, selectionMode, expirationSettingName, showBackButton } = this.props;
|
||||
const triggerId = 'conversation-header';
|
||||
console.warn('conversation header render', this.props);
|
||||
const {
|
||||
onGoBack,
|
||||
onAvatarClick,
|
||||
onCloseOverlay,
|
||||
onDeleteSelectedMessages,
|
||||
showBackButton,
|
||||
selectionMode,
|
||||
} = headerPropsNonRedux;
|
||||
const triggerId = 'conversation-header';
|
||||
|
||||
return (
|
||||
<div className="module-conversation-header">
|
||||
<div className="conversation-header--items-wrapper">
|
||||
<BackButton onGoBack={this.props.onGoBack} showBackButton={this.props.showBackButton} />
|
||||
return (
|
||||
<div className="module-conversation-header">
|
||||
<div className="conversation-header--items-wrapper">
|
||||
<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()}
|
||||
</div>
|
||||
<div className="module-conversation-header__title-container">
|
||||
<div className="module-conversation-header__title-flex">
|
||||
<TripleDotsMenu triggerId={triggerId} showBackButton={showBackButton} />
|
||||
<ConversationHeaderTitle />
|
||||
</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}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MemoConversationHeaderMenu
|
||||
conversationId={this.props.id}
|
||||
triggerId={triggerId}
|
||||
isMe={this.props.isMe}
|
||||
isPublic={this.props.isPublic}
|
||||
isGroup={this.props.isGroup}
|
||||
isKickedFromGroup={isKickedFromGroup}
|
||||
isAdmin={this.props.isAdmin}
|
||||
isBlocked={this.props.isBlocked}
|
||||
isPrivate={this.props.isPrivate}
|
||||
left={this.props.left}
|
||||
hasNickname={this.props.hasNickname}
|
||||
notificationForConvo={this.props.notificationForConvo}
|
||||
currentNotificationSetting={this.props.currentNotificationSetting}
|
||||
/>
|
||||
</div>
|
||||
{!isKickedFromGroup && <ExpirationLength expirationSettingName={expirationSettingName} />}
|
||||
|
||||
{selectionMode && (
|
||||
<SelectionOverlay
|
||||
isPublic={this.props.isPublic}
|
||||
onCloseOverlay={this.props.onCloseOverlay}
|
||||
onDeleteSelectedMessages={this.props.onDeleteSelectedMessages}
|
||||
{!selectionMode && (
|
||||
<AvatarHeader
|
||||
onAvatarClick={onAvatarClick}
|
||||
phoneNumber={phoneNumber}
|
||||
showBackButton={showBackButton}
|
||||
avatarPath={avatarPath}
|
||||
memberAvatars={memberDetails}
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ConversationHeaderWithDetails = usingClosedConversationDetails(
|
||||
ConversationHeaderInner
|
||||
);
|
||||
<MemoConversationHeaderMenu
|
||||
conversationId={id}
|
||||
triggerId={triggerId}
|
||||
isMe={isMe}
|
||||
isPublic={isPublic}
|
||||
isGroup={isGroup}
|
||||
isKickedFromGroup={isKickedFromGroup}
|
||||
weAreAdmin={weAreAdmin}
|
||||
isBlocked={isBlocked}
|
||||
isPrivate={isPrivate}
|
||||
left={left}
|
||||
hasNickname={hasNickname}
|
||||
notificationForConvo={notificationForConvo}
|
||||
currentNotificationSetting={currentNotificationSetting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectionMode && (
|
||||
<SelectionOverlay
|
||||
isPublic={isPublic}
|
||||
onCloseOverlay={onCloseOverlay}
|
||||
onDeleteSelectedMessages={onDeleteSelectedMessages}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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,
|
||||
|
|
55
ts/hooks/useMembersAvatar.tsx
Normal file
55
ts/hooks/useMembersAvatar.tsx
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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);
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue