|
|
|
@ -19,12 +19,18 @@ import {
|
|
|
|
|
} from './session/menu/ConversationListItemContextMenu';
|
|
|
|
|
import { createPortal } from 'react-dom';
|
|
|
|
|
import { OutgoingMessageStatus } from './conversation/message/OutgoingMessageStatus';
|
|
|
|
|
import { DefaultTheme, withTheme } from 'styled-components';
|
|
|
|
|
import { DefaultTheme, useTheme, withTheme } from 'styled-components';
|
|
|
|
|
import { PubKey } from '../session/types';
|
|
|
|
|
import { ConversationType, openConversationExternal } from '../state/ducks/conversations';
|
|
|
|
|
import {
|
|
|
|
|
ConversationType,
|
|
|
|
|
LastMessageType,
|
|
|
|
|
openConversationExternal,
|
|
|
|
|
} from '../state/ducks/conversations';
|
|
|
|
|
import { SessionIcon, SessionIconSize, SessionIconType } from './session/icon';
|
|
|
|
|
import { useSelector } from 'react-redux';
|
|
|
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
|
|
|
import { SectionType } from './session/ActionsPanel';
|
|
|
|
|
import { getTheme } from '../state/selectors/theme';
|
|
|
|
|
import { getFocusedSection } from '../state/selectors/section';
|
|
|
|
|
|
|
|
|
|
export interface ConversationListItemProps extends ConversationType {
|
|
|
|
|
index?: number; // used to force a refresh when one conversation is removed on top of the list
|
|
|
|
@ -42,197 +48,274 @@ const Portal = ({ children }: { children: any }) => {
|
|
|
|
|
return createPortal(children, document.querySelector('.inbox.index') as Element);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class ConversationListItem extends React.PureComponent<Props> {
|
|
|
|
|
public constructor(props: Props) {
|
|
|
|
|
super(props);
|
|
|
|
|
}
|
|
|
|
|
const ConversationListItem = (props: Props) => {
|
|
|
|
|
const {
|
|
|
|
|
phoneNumber,
|
|
|
|
|
unreadCount,
|
|
|
|
|
id,
|
|
|
|
|
isSelected,
|
|
|
|
|
isBlocked,
|
|
|
|
|
style,
|
|
|
|
|
mentionedUs,
|
|
|
|
|
avatarPath,
|
|
|
|
|
name,
|
|
|
|
|
profileName,
|
|
|
|
|
activeAt,
|
|
|
|
|
isMe,
|
|
|
|
|
isPinned,
|
|
|
|
|
isTyping,
|
|
|
|
|
lastMessage,
|
|
|
|
|
memberAvatars,
|
|
|
|
|
} = props;
|
|
|
|
|
const triggerId: string = `conversation-item-${phoneNumber}-ctxmenu`;
|
|
|
|
|
const key: string = `conversation-item-${phoneNumber}`;
|
|
|
|
|
|
|
|
|
|
public renderAvatar() {
|
|
|
|
|
const { avatarPath, name, phoneNumber, profileName, memberAvatars } = this.props;
|
|
|
|
|
const dispatch = useDispatch();
|
|
|
|
|
|
|
|
|
|
const userName = name || profileName || phoneNumber;
|
|
|
|
|
const menuProps: PropsContextConversationItem = {
|
|
|
|
|
...props,
|
|
|
|
|
triggerId,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="module-conversation-list-item__avatar-container">
|
|
|
|
|
<Avatar
|
|
|
|
|
return (
|
|
|
|
|
<div key={key}>
|
|
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
dispatch(openConversationExternal(id));
|
|
|
|
|
}}
|
|
|
|
|
onContextMenu={(e: any) => {
|
|
|
|
|
contextMenu.show({
|
|
|
|
|
id: triggerId,
|
|
|
|
|
event: e,
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
style={style}
|
|
|
|
|
className={classNames(
|
|
|
|
|
'module-conversation-list-item',
|
|
|
|
|
unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null,
|
|
|
|
|
unreadCount > 0 && mentionedUs ? 'module-conversation-list-item--mentioned-us' : null,
|
|
|
|
|
isSelected ? 'module-conversation-list-item--is-selected' : null,
|
|
|
|
|
isBlocked ? 'module-conversation-list-item--is-blocked' : null
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<ConversationListItemAvatar
|
|
|
|
|
avatarPath={avatarPath}
|
|
|
|
|
name={userName}
|
|
|
|
|
size={AvatarSize.S}
|
|
|
|
|
name={name}
|
|
|
|
|
profileName={profileName}
|
|
|
|
|
memberAvatars={memberAvatars}
|
|
|
|
|
pubkey={phoneNumber}
|
|
|
|
|
/>
|
|
|
|
|
<div className="module-conversation-list-item__content">
|
|
|
|
|
<ConversationListItemHeader
|
|
|
|
|
unreadCount={unreadCount}
|
|
|
|
|
mentionedUs={mentionedUs}
|
|
|
|
|
activeAt={activeAt}
|
|
|
|
|
isPinned={isPinned}
|
|
|
|
|
name={name}
|
|
|
|
|
phoneNumber={phoneNumber}
|
|
|
|
|
profileName={profileName}
|
|
|
|
|
isMe={isMe}
|
|
|
|
|
/>
|
|
|
|
|
<ConversationListItemMessage
|
|
|
|
|
lastMessage={lastMessage}
|
|
|
|
|
isTyping={isTyping}
|
|
|
|
|
unreadCount={unreadCount}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
<Portal>
|
|
|
|
|
<ConversationListItemContextMenu {...menuProps} />
|
|
|
|
|
</Portal>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public renderHeader() {
|
|
|
|
|
const { unreadCount, mentionedUs, activeAt, isPinned } = this.props;
|
|
|
|
|
|
|
|
|
|
let atSymbol = null;
|
|
|
|
|
let unreadCountDiv = null;
|
|
|
|
|
if (unreadCount > 0) {
|
|
|
|
|
atSymbol = mentionedUs ? <p className="at-symbol">@</p> : null;
|
|
|
|
|
unreadCountDiv = <p className="module-conversation-list-item__unread-count">{unreadCount}</p>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isMessagesSection =
|
|
|
|
|
window.inboxStore?.getState().section.focusedSection === SectionType.Message;
|
|
|
|
|
|
|
|
|
|
const pinIcon =
|
|
|
|
|
isMessagesSection && isPinned ? (
|
|
|
|
|
<SessionIcon
|
|
|
|
|
iconType={SessionIconType.Pin}
|
|
|
|
|
iconColor={this.props.theme.colors.textColorSubtle}
|
|
|
|
|
iconSize={SessionIconSize.Tiny}
|
|
|
|
|
/>
|
|
|
|
|
) : null;
|
|
|
|
|
export interface ConversationListItemAvatarProps {
|
|
|
|
|
avatarPath?: string;
|
|
|
|
|
name?: string;
|
|
|
|
|
profileName?: string;
|
|
|
|
|
phoneNumber?: string;
|
|
|
|
|
memberAvatars?: Array<ConversationAvatar>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="module-conversation-list-item__header">
|
|
|
|
|
<div
|
|
|
|
|
className={classNames(
|
|
|
|
|
'module-conversation-list-item__header__name',
|
|
|
|
|
unreadCount > 0 ? 'module-conversation-list-item__header__name--with-unread' : null
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{this.renderUser()}
|
|
|
|
|
</div>
|
|
|
|
|
export const ConversationListItemAvatar = (props: ConversationListItemAvatarProps) => {
|
|
|
|
|
const { avatarPath, name, phoneNumber, profileName, memberAvatars } = props;
|
|
|
|
|
const userName = name || profileName || phoneNumber;
|
|
|
|
|
|
|
|
|
|
{pinIcon}
|
|
|
|
|
{unreadCountDiv}
|
|
|
|
|
{atSymbol}
|
|
|
|
|
{
|
|
|
|
|
<div
|
|
|
|
|
className={classNames(
|
|
|
|
|
'module-conversation-list-item__header__date',
|
|
|
|
|
unreadCount > 0 ? 'module-conversation-list-item__header__date--has-unread' : null
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{
|
|
|
|
|
<Timestamp
|
|
|
|
|
timestamp={activeAt}
|
|
|
|
|
extended={false}
|
|
|
|
|
isConversationListItem={true}
|
|
|
|
|
theme={this.props.theme}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<div className="module-conversation-list-item__avatar-container">
|
|
|
|
|
<Avatar
|
|
|
|
|
avatarPath={avatarPath}
|
|
|
|
|
name={userName}
|
|
|
|
|
size={AvatarSize.S}
|
|
|
|
|
memberAvatars={memberAvatars}
|
|
|
|
|
pubkey={phoneNumber}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public renderMessage() {
|
|
|
|
|
const { lastMessage, isTyping, unreadCount } = this.props;
|
|
|
|
|
export interface ConversationListItemHeaderProps {
|
|
|
|
|
unreadCount: number;
|
|
|
|
|
mentionedUs: boolean;
|
|
|
|
|
activeAt?: number;
|
|
|
|
|
isPinned?: boolean;
|
|
|
|
|
|
|
|
|
|
if (!lastMessage && !isTyping) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const text = lastMessage && lastMessage.text ? lastMessage.text : '';
|
|
|
|
|
name?: string;
|
|
|
|
|
phoneNumber: string;
|
|
|
|
|
profileName?: string;
|
|
|
|
|
isMe: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isEmpty(text)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
export const ConversationListItemHeader = (props: ConversationListItemHeaderProps) => {
|
|
|
|
|
const {
|
|
|
|
|
unreadCount,
|
|
|
|
|
mentionedUs,
|
|
|
|
|
activeAt,
|
|
|
|
|
isPinned,
|
|
|
|
|
name,
|
|
|
|
|
phoneNumber,
|
|
|
|
|
profileName,
|
|
|
|
|
isMe,
|
|
|
|
|
} = props;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="module-conversation-list-item__message">
|
|
|
|
|
<div
|
|
|
|
|
className={classNames(
|
|
|
|
|
'module-conversation-list-item__message__text',
|
|
|
|
|
unreadCount > 0 ? 'module-conversation-list-item__message__text--has-unread' : null
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{isTyping ? (
|
|
|
|
|
<TypingAnimation />
|
|
|
|
|
) : (
|
|
|
|
|
<MessageBody isGroup={true} text={text} disableJumbomoji={true} disableLinks={true} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{lastMessage && lastMessage.status ? (
|
|
|
|
|
<OutgoingMessageStatus
|
|
|
|
|
status={lastMessage.status}
|
|
|
|
|
iconColor={this.props.theme.colors.textColorSubtle}
|
|
|
|
|
theme={this.props.theme}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
const theme = useTheme();
|
|
|
|
|
|
|
|
|
|
let atSymbol = null;
|
|
|
|
|
let unreadCountDiv = null;
|
|
|
|
|
if (unreadCount > 0) {
|
|
|
|
|
atSymbol = mentionedUs ? <p className="at-symbol">@</p> : null;
|
|
|
|
|
unreadCountDiv = <p className="module-conversation-list-item__unread-count">{unreadCount}</p>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public render() {
|
|
|
|
|
const { phoneNumber, unreadCount, id, isSelected, isBlocked, style, mentionedUs } = this.props;
|
|
|
|
|
const triggerId = `conversation-item-${phoneNumber}-ctxmenu`;
|
|
|
|
|
const key = `conversation-item-${phoneNumber}`;
|
|
|
|
|
const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message;
|
|
|
|
|
|
|
|
|
|
const pinIcon =
|
|
|
|
|
isMessagesSection && isPinned ? (
|
|
|
|
|
<SessionIcon
|
|
|
|
|
iconType={SessionIconType.Pin}
|
|
|
|
|
iconColor={theme.colors.textColorSubtle}
|
|
|
|
|
iconSize={SessionIconSize.Tiny}
|
|
|
|
|
/>
|
|
|
|
|
) : null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={key}>
|
|
|
|
|
return (
|
|
|
|
|
<div className="module-conversation-list-item__header">
|
|
|
|
|
<div
|
|
|
|
|
className={classNames(
|
|
|
|
|
'module-conversation-list-item__header__name',
|
|
|
|
|
unreadCount > 0 ? 'module-conversation-list-item__header__name--with-unread' : null
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<ConversationListItemUser
|
|
|
|
|
name={name}
|
|
|
|
|
phoneNumber={phoneNumber}
|
|
|
|
|
profileName={profileName}
|
|
|
|
|
isMe={isMe}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{pinIcon}
|
|
|
|
|
{unreadCountDiv}
|
|
|
|
|
{atSymbol}
|
|
|
|
|
{
|
|
|
|
|
<div
|
|
|
|
|
role="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
window.inboxStore?.dispatch(openConversationExternal(id));
|
|
|
|
|
}}
|
|
|
|
|
onContextMenu={(e: any) => {
|
|
|
|
|
contextMenu.show({
|
|
|
|
|
id: triggerId,
|
|
|
|
|
event: e,
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
style={style}
|
|
|
|
|
className={classNames(
|
|
|
|
|
'module-conversation-list-item',
|
|
|
|
|
unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null,
|
|
|
|
|
unreadCount > 0 && mentionedUs ? 'module-conversation-list-item--mentioned-us' : null,
|
|
|
|
|
isSelected ? 'module-conversation-list-item--is-selected' : null,
|
|
|
|
|
isBlocked ? 'module-conversation-list-item--is-blocked' : null
|
|
|
|
|
'module-conversation-list-item__header__date',
|
|
|
|
|
unreadCount > 0 ? 'module-conversation-list-item__header__date--has-unread' : null
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{this.renderAvatar()}
|
|
|
|
|
<div className="module-conversation-list-item__content">
|
|
|
|
|
{this.renderHeader()}
|
|
|
|
|
{this.renderMessage()}
|
|
|
|
|
</div>
|
|
|
|
|
{
|
|
|
|
|
<Timestamp
|
|
|
|
|
timestamp={activeAt}
|
|
|
|
|
extended={false}
|
|
|
|
|
isConversationListItem={true}
|
|
|
|
|
theme={theme}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
<Portal>
|
|
|
|
|
<ConversationListItemContextMenu {...this.getMenuProps(triggerId)} />
|
|
|
|
|
</Portal>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export interface ConversationListMessageProps {
|
|
|
|
|
lastMessage: LastMessageType;
|
|
|
|
|
isTyping: boolean;
|
|
|
|
|
unreadCount: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const ConversationListItemMessage = (props: any) => {
|
|
|
|
|
const { lastMessage, isTyping, unreadCount } = props;
|
|
|
|
|
const theme = useTheme();
|
|
|
|
|
|
|
|
|
|
if (!lastMessage && !isTyping) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const text = lastMessage && lastMessage.text ? lastMessage.text : '';
|
|
|
|
|
|
|
|
|
|
private getMenuProps(triggerId: string): PropsContextConversationItem {
|
|
|
|
|
return {
|
|
|
|
|
triggerId,
|
|
|
|
|
...this.props,
|
|
|
|
|
};
|
|
|
|
|
if (isEmpty(text)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderUser() {
|
|
|
|
|
const { name, phoneNumber, profileName, isMe } = this.props;
|
|
|
|
|
return (
|
|
|
|
|
<div className="module-conversation-list-item__message">
|
|
|
|
|
<div
|
|
|
|
|
className={classNames(
|
|
|
|
|
'module-conversation-list-item__message__text',
|
|
|
|
|
unreadCount > 0 ? 'module-conversation-list-item__message__text--has-unread' : null
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{isTyping ? (
|
|
|
|
|
<TypingAnimation />
|
|
|
|
|
) : (
|
|
|
|
|
<MessageBody isGroup={true} text={text} disableJumbomoji={true} disableLinks={true} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{lastMessage && lastMessage.status ? (
|
|
|
|
|
<OutgoingMessageStatus
|
|
|
|
|
status={lastMessage.status}
|
|
|
|
|
iconColor={theme.colors.textColorSubtle}
|
|
|
|
|
theme={theme}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const shortenedPubkey = PubKey.shorten(phoneNumber);
|
|
|
|
|
export interface ConversationListItemUserProps {
|
|
|
|
|
name?: string;
|
|
|
|
|
phoneNumber: string;
|
|
|
|
|
profileName?: string;
|
|
|
|
|
isMe: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const displayedPubkey = profileName ? shortenedPubkey : phoneNumber;
|
|
|
|
|
const displayName = isMe ? window.i18n('noteToSelf') : profileName;
|
|
|
|
|
export const ConversationListItemUser = (props: ConversationListItemUserProps) => {
|
|
|
|
|
const { name, phoneNumber, profileName, isMe } = props;
|
|
|
|
|
|
|
|
|
|
let shouldShowPubkey = false;
|
|
|
|
|
if ((!name || name.length === 0) && (!displayName || displayName.length === 0)) {
|
|
|
|
|
shouldShowPubkey = true;
|
|
|
|
|
}
|
|
|
|
|
const shortenedPubkey = PubKey.shorten(phoneNumber);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="module-conversation__user">
|
|
|
|
|
<ContactName
|
|
|
|
|
phoneNumber={displayedPubkey}
|
|
|
|
|
name={name}
|
|
|
|
|
profileName={displayName}
|
|
|
|
|
module="module-conversation__user"
|
|
|
|
|
boldProfileName={true}
|
|
|
|
|
shouldShowPubkey={shouldShowPubkey}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
const displayedPubkey = profileName ? shortenedPubkey : phoneNumber;
|
|
|
|
|
const displayName = isMe ? window.i18n('noteToSelf') : profileName;
|
|
|
|
|
|
|
|
|
|
let shouldShowPubkey = false;
|
|
|
|
|
if ((!name || name.length === 0) && (!displayName || displayName.length === 0)) {
|
|
|
|
|
shouldShowPubkey = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="module-conversation__user">
|
|
|
|
|
<ContactName
|
|
|
|
|
phoneNumber={displayedPubkey}
|
|
|
|
|
name={name}
|
|
|
|
|
profileName={displayName}
|
|
|
|
|
module="module-conversation__user"
|
|
|
|
|
boldProfileName={true}
|
|
|
|
|
shouldShowPubkey={shouldShowPubkey}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const ConversationListItemWithDetails = usingClosedConversationDetails(
|
|
|
|
|
withTheme(ConversationListItem)
|
|
|
|
|