use React Provider for convoListItem (#2088)
this is to avoid passing down the prop to all the componentspull/2092/head
parent
38325215e6
commit
abd146c4ca
@ -1,400 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import { contextMenu } from 'react-contexify';
|
|
||||||
|
|
||||||
import { Avatar, AvatarSize } from '../avatar/Avatar';
|
|
||||||
import { Timestamp } from '../conversation/Timestamp';
|
|
||||||
import { ContactName } from '../conversation/ContactName';
|
|
||||||
import { TypingAnimation } from '../conversation/TypingAnimation';
|
|
||||||
|
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { PubKey } from '../../session/types';
|
|
||||||
import {
|
|
||||||
LastMessageType,
|
|
||||||
openConversationWithMessages,
|
|
||||||
ReduxConversationType,
|
|
||||||
} from '../../state/ducks/conversations';
|
|
||||||
import _ from 'underscore';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { SectionType } from '../../state/ducks/section';
|
|
||||||
import { getFocusedSection } from '../../state/selectors/section';
|
|
||||||
import { ConversationNotificationSettingType } from '../../models/conversation';
|
|
||||||
import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils';
|
|
||||||
import { updateUserDetailsModal } from '../../state/ducks/modalDialog';
|
|
||||||
import { approveConversation, blockConvoById } from '../../interactions/conversationInteractions';
|
|
||||||
import { useAvatarPath, useConversationUsername, useIsMe } from '../../hooks/useParamSelector';
|
|
||||||
import { MessageBody } from '../conversation/message/message-content/MessageBody';
|
|
||||||
import { OutgoingMessageStatus } from '../conversation/message/message-content/OutgoingMessageStatus';
|
|
||||||
import { SessionIcon, SessionIconButton } from '../icon';
|
|
||||||
import { MemoConversationListItemContextMenu } from '../menu/ConversationListItemContextMenu';
|
|
||||||
|
|
||||||
// tslint:disable-next-line: no-empty-interface
|
|
||||||
export interface ConversationListItemProps extends ReduxConversationType {}
|
|
||||||
|
|
||||||
export const StyledConversationListItemIconWrapper = styled.div`
|
|
||||||
svg {
|
|
||||||
margin: 0px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type PropsHousekeeping = {
|
|
||||||
style?: Object;
|
|
||||||
isMessageRequest?: boolean;
|
|
||||||
};
|
|
||||||
// tslint:disable: use-simple-attributes
|
|
||||||
|
|
||||||
type Props = ConversationListItemProps & PropsHousekeeping;
|
|
||||||
|
|
||||||
const Portal = ({ children }: { children: any }) => {
|
|
||||||
return createPortal(children, document.querySelector('.inbox.index') as Element);
|
|
||||||
};
|
|
||||||
|
|
||||||
const HeaderItem = (props: {
|
|
||||||
unreadCount: number;
|
|
||||||
mentionedUs: boolean;
|
|
||||||
activeAt?: number;
|
|
||||||
conversationId: string;
|
|
||||||
isPinned: boolean;
|
|
||||||
currentNotificationSetting: ConversationNotificationSettingType;
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
unreadCount,
|
|
||||||
mentionedUs,
|
|
||||||
activeAt,
|
|
||||||
isPinned,
|
|
||||||
conversationId,
|
|
||||||
currentNotificationSetting,
|
|
||||||
} = 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 = useSelector(getFocusedSection) === SectionType.Message;
|
|
||||||
|
|
||||||
const pinIcon =
|
|
||||||
isMessagesSection && isPinned ? (
|
|
||||||
<SessionIcon iconType="pin" iconColor={'var(--color-text-subtle)'} iconSize="small" />
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const NotificationSettingIcon = () => {
|
|
||||||
if (!isMessagesSection) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (currentNotificationSetting) {
|
|
||||||
case 'all':
|
|
||||||
return null;
|
|
||||||
case 'disabled':
|
|
||||||
return (
|
|
||||||
<SessionIcon iconType="mute" iconColor={'var(--color-text-subtle)'} iconSize="small" />
|
|
||||||
);
|
|
||||||
case 'mentions_only':
|
|
||||||
return (
|
|
||||||
<SessionIcon iconType="bell" iconColor={'var(--color-text-subtle)'} iconSize="small" />
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="module-conversation-list-item__header">
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'module-conversation-list-item__header__name',
|
|
||||||
unreadCount > 0 ? 'module-conversation-list-item__header__name--with-unread' : null
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<UserItem conversationId={conversationId} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<StyledConversationListItemIconWrapper>
|
|
||||||
{pinIcon}
|
|
||||||
<NotificationSettingIcon />
|
|
||||||
</StyledConversationListItemIconWrapper>
|
|
||||||
{unreadCountDiv}
|
|
||||||
{atSymbol}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'module-conversation-list-item__header__date',
|
|
||||||
unreadCount > 0 ? 'module-conversation-list-item__header__date--has-unread' : null
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Timestamp timestamp={activeAt} extended={false} isConversationListItem={true} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserItem = (props: { conversationId: string }) => {
|
|
||||||
const { conversationId } = props;
|
|
||||||
|
|
||||||
const shortenedPubkey = PubKey.shorten(conversationId);
|
|
||||||
const isMe = useIsMe(conversationId);
|
|
||||||
const username = useConversationUsername(conversationId);
|
|
||||||
|
|
||||||
const displayedPubkey = username ? shortenedPubkey : conversationId;
|
|
||||||
const displayName = isMe ? window.i18n('noteToSelf') : username;
|
|
||||||
|
|
||||||
let shouldShowPubkey = false;
|
|
||||||
if ((!username || username.length === 0) && (!displayName || displayName.length === 0)) {
|
|
||||||
shouldShowPubkey = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="module-conversation__user">
|
|
||||||
<ContactName
|
|
||||||
pubkey={displayedPubkey}
|
|
||||||
name={username}
|
|
||||||
profileName={displayName}
|
|
||||||
module="module-conversation__user"
|
|
||||||
boldProfileName={true}
|
|
||||||
shouldShowPubkey={shouldShowPubkey}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MessageItem = (props: {
|
|
||||||
lastMessage?: LastMessageType;
|
|
||||||
isTyping: boolean;
|
|
||||||
unreadCount: number;
|
|
||||||
isMessageRequest: boolean;
|
|
||||||
conversationId: string;
|
|
||||||
}) => {
|
|
||||||
const { lastMessage, isTyping, unreadCount } = props;
|
|
||||||
|
|
||||||
if (!lastMessage && !isTyping) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const text = lastMessage?.text || '';
|
|
||||||
|
|
||||||
if (isEmpty(text)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="module-conversation-list-item__message">
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'module-conversation-list-item__message__text',
|
|
||||||
unreadCount > 0 ? 'module-conversation-list-item__message__text--has-unread' : null
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isTyping ? (
|
|
||||||
<TypingAnimation />
|
|
||||||
) : (
|
|
||||||
<MessageBody isGroup={true} text={text} disableJumbomoji={true} disableLinks={true} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<MessageRequestButtons
|
|
||||||
conversationId={props.conversationId}
|
|
||||||
isMessageRequest={props.isMessageRequest}
|
|
||||||
/>
|
|
||||||
{lastMessage && lastMessage.status && !props.isMessageRequest ? (
|
|
||||||
<OutgoingMessageStatus status={lastMessage.status} />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AvatarItem = (props: { conversationId: string; isPrivate: boolean }) => {
|
|
||||||
const { isPrivate, conversationId } = props;
|
|
||||||
const userName = useConversationUsername(conversationId);
|
|
||||||
const avatarPath = useAvatarPath(conversationId);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
function onPrivateAvatarClick() {
|
|
||||||
dispatch(
|
|
||||||
updateUserDetailsModal({
|
|
||||||
conversationId: conversationId,
|
|
||||||
userName: userName || '',
|
|
||||||
authorAvatarPath: avatarPath,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="module-conversation-list-item__avatar-container">
|
|
||||||
<Avatar
|
|
||||||
size={AvatarSize.S}
|
|
||||||
pubkey={conversationId}
|
|
||||||
onAvatarClick={isPrivate ? onPrivateAvatarClick : undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RejectMessageRequestButton = ({ conversationId }: { conversationId: string }) => {
|
|
||||||
/**
|
|
||||||
* Removes conversation from requests list,
|
|
||||||
* adds ID to block list, syncs the block with linked devices.
|
|
||||||
*/
|
|
||||||
const handleConversationBlock = async () => {
|
|
||||||
await blockConvoById(conversationId);
|
|
||||||
await forceSyncConfigurationNowIfNeeded();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<SessionIconButton
|
|
||||||
iconType="exit"
|
|
||||||
iconSize="large"
|
|
||||||
onClick={handleConversationBlock}
|
|
||||||
backgroundColor="var(--color-destructive)"
|
|
||||||
iconColor="var(--color-foreground-primary)"
|
|
||||||
iconPadding="var(--margins-xs)"
|
|
||||||
borderRadius="2px"
|
|
||||||
margin="0 5px 0 0"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ApproveMessageRequestButton = ({ conversationId }: { conversationId: string }) => {
|
|
||||||
return (
|
|
||||||
<SessionIconButton
|
|
||||||
iconType="check"
|
|
||||||
iconSize="large"
|
|
||||||
onClick={async () => {
|
|
||||||
await approveConversation(conversationId);
|
|
||||||
}}
|
|
||||||
backgroundColor="var(--color-accent)"
|
|
||||||
iconColor="var(--color-foreground-primary)"
|
|
||||||
iconPadding="var(--margins-xs)"
|
|
||||||
borderRadius="2px"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MessageRequestButtons = ({
|
|
||||||
conversationId,
|
|
||||||
isMessageRequest,
|
|
||||||
}: {
|
|
||||||
conversationId: string;
|
|
||||||
isMessageRequest: boolean;
|
|
||||||
}) => {
|
|
||||||
if (!isMessageRequest) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<RejectMessageRequestButton conversationId={conversationId} />
|
|
||||||
<ApproveMessageRequestButton conversationId={conversationId} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// tslint:disable: max-func-body-length
|
|
||||||
const ConversationListItem = (props: Props) => {
|
|
||||||
const {
|
|
||||||
activeAt,
|
|
||||||
unreadCount,
|
|
||||||
id: conversationId,
|
|
||||||
isSelected,
|
|
||||||
isBlocked,
|
|
||||||
style,
|
|
||||||
mentionedUs,
|
|
||||||
isMe,
|
|
||||||
isPinned,
|
|
||||||
isTyping,
|
|
||||||
lastMessage,
|
|
||||||
hasNickname,
|
|
||||||
isKickedFromGroup,
|
|
||||||
left,
|
|
||||||
type,
|
|
||||||
isPublic,
|
|
||||||
avatarPath,
|
|
||||||
isPrivate,
|
|
||||||
currentNotificationSetting,
|
|
||||||
weAreAdmin,
|
|
||||||
isMessageRequest,
|
|
||||||
} = props;
|
|
||||||
const triggerId = `conversation-item-${conversationId}-ctxmenu`;
|
|
||||||
const key = `conversation-item-${conversationId}`;
|
|
||||||
|
|
||||||
const openConvo = useCallback(
|
|
||||||
async (e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
// mousedown is invoked sooner than onClick, but for both right and left click
|
|
||||||
if (e.button === 0) {
|
|
||||||
await openConversationWithMessages({ conversationKey: conversationId });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[conversationId]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={key}>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
onMouseDown={openConvo}
|
|
||||||
onMouseUp={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
onContextMenu={(e: any) => {
|
|
||||||
contextMenu.show({
|
|
||||||
id: triggerId,
|
|
||||||
event: e,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
style={style}
|
|
||||||
className={classNames(
|
|
||||||
'module-conversation-list-item',
|
|
||||||
unreadCount && unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null,
|
|
||||||
unreadCount && unreadCount > 0 && mentionedUs
|
|
||||||
? 'module-conversation-list-item--mentioned-us'
|
|
||||||
: null,
|
|
||||||
isSelected ? 'module-conversation-list-item--is-selected' : null,
|
|
||||||
isBlocked ? 'module-conversation-list-item--is-blocked' : null
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<AvatarItem conversationId={conversationId} isPrivate={isPrivate || false} />
|
|
||||||
<div className="module-conversation-list-item__content">
|
|
||||||
<HeaderItem
|
|
||||||
mentionedUs={!!mentionedUs}
|
|
||||||
unreadCount={unreadCount || 0}
|
|
||||||
activeAt={activeAt}
|
|
||||||
isPinned={!!isPinned}
|
|
||||||
conversationId={conversationId}
|
|
||||||
currentNotificationSetting={currentNotificationSetting || 'all'}
|
|
||||||
/>
|
|
||||||
<MessageItem
|
|
||||||
isTyping={!!isTyping}
|
|
||||||
unreadCount={unreadCount || 0}
|
|
||||||
lastMessage={lastMessage}
|
|
||||||
isMessageRequest={Boolean(isMessageRequest)}
|
|
||||||
conversationId={conversationId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Portal>
|
|
||||||
<MemoConversationListItemContextMenu
|
|
||||||
triggerId={triggerId}
|
|
||||||
conversationId={conversationId}
|
|
||||||
hasNickname={!!hasNickname}
|
|
||||||
isBlocked={!!isBlocked}
|
|
||||||
isPrivate={!!isPrivate}
|
|
||||||
isKickedFromGroup={!!isKickedFromGroup}
|
|
||||||
isMe={!!isMe}
|
|
||||||
isPublic={!!isPublic}
|
|
||||||
left={!!left}
|
|
||||||
type={type}
|
|
||||||
currentNotificationSetting={currentNotificationSetting || 'all'}
|
|
||||||
avatarPath={avatarPath || null}
|
|
||||||
weAreAdmin={weAreAdmin}
|
|
||||||
/>
|
|
||||||
</Portal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MemoConversationListItemWithDetails = React.memo(ConversationListItem, _.isEqual);
|
|
@ -0,0 +1,142 @@
|
|||||||
|
import React, { useCallback, useContext } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { contextMenu } from 'react-contexify';
|
||||||
|
|
||||||
|
import { Avatar, AvatarSize } from '../../avatar/Avatar';
|
||||||
|
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import {
|
||||||
|
openConversationWithMessages,
|
||||||
|
ReduxConversationType,
|
||||||
|
} from '../../../state/ducks/conversations';
|
||||||
|
import _ from 'underscore';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { updateUserDetailsModal } from '../../../state/ducks/modalDialog';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useAvatarPath,
|
||||||
|
useConversationUsername,
|
||||||
|
useIsPrivate,
|
||||||
|
} from '../../../hooks/useParamSelector';
|
||||||
|
import { MemoConversationListItemContextMenu } from '../../menu/ConversationListItemContextMenu';
|
||||||
|
import { HeaderItem } from './HeaderItem';
|
||||||
|
import { MessageItem } from './MessageItem';
|
||||||
|
|
||||||
|
// tslint:disable-next-line: no-empty-interface
|
||||||
|
export type ConversationListItemProps = Pick<
|
||||||
|
ReduxConversationType,
|
||||||
|
'unreadCount' | 'id' | 'isSelected' | 'isBlocked' | 'mentionedUs' | 'unreadCount' | 'profileName'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This React context is used to share deeply in the tree of the ConversationListItem what is the ID we are currently rendering.
|
||||||
|
* This is to avoid passing the prop to all the subtree component
|
||||||
|
*/
|
||||||
|
export const ContextConversationId = React.createContext('');
|
||||||
|
|
||||||
|
type PropsHousekeeping = {
|
||||||
|
style?: Object;
|
||||||
|
isMessageRequest?: boolean;
|
||||||
|
};
|
||||||
|
// tslint:disable: use-simple-attributes
|
||||||
|
|
||||||
|
type Props = ConversationListItemProps & PropsHousekeeping;
|
||||||
|
|
||||||
|
const Portal = ({ children }: { children: any }) => {
|
||||||
|
return createPortal(children, document.querySelector('.inbox.index') as Element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AvatarItem = () => {
|
||||||
|
const conversationId = useContext(ContextConversationId);
|
||||||
|
const userName = useConversationUsername(conversationId);
|
||||||
|
const isPrivate = useIsPrivate(conversationId);
|
||||||
|
const avatarPath = useAvatarPath(conversationId);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
function onPrivateAvatarClick() {
|
||||||
|
dispatch(
|
||||||
|
updateUserDetailsModal({
|
||||||
|
conversationId: conversationId,
|
||||||
|
userName: userName || '',
|
||||||
|
authorAvatarPath: avatarPath,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-conversation-list-item__avatar-container">
|
||||||
|
<Avatar
|
||||||
|
size={AvatarSize.S}
|
||||||
|
pubkey={conversationId}
|
||||||
|
onAvatarClick={isPrivate ? onPrivateAvatarClick : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// tslint:disable: max-func-body-length
|
||||||
|
const ConversationListItem = (props: Props) => {
|
||||||
|
const {
|
||||||
|
unreadCount,
|
||||||
|
id: conversationId,
|
||||||
|
isSelected,
|
||||||
|
isBlocked,
|
||||||
|
style,
|
||||||
|
mentionedUs,
|
||||||
|
isMessageRequest,
|
||||||
|
} = props;
|
||||||
|
const triggerId = `conversation-item-${conversationId}-ctxmenu`;
|
||||||
|
const key = `conversation-item-${conversationId}`;
|
||||||
|
|
||||||
|
const openConvo = useCallback(
|
||||||
|
async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
// mousedown is invoked sooner than onClick, but for both right and left click
|
||||||
|
if (e.button === 0) {
|
||||||
|
await openConversationWithMessages({ conversationKey: conversationId });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[conversationId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextConversationId.Provider value={conversationId}>
|
||||||
|
<div key={key}>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
onMouseDown={openConvo}
|
||||||
|
onMouseUp={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
onContextMenu={(e: any) => {
|
||||||
|
contextMenu.show({
|
||||||
|
id: triggerId,
|
||||||
|
event: e,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
style={style}
|
||||||
|
className={classNames(
|
||||||
|
'module-conversation-list-item',
|
||||||
|
unreadCount && unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null,
|
||||||
|
unreadCount && unreadCount > 0 && mentionedUs
|
||||||
|
? 'module-conversation-list-item--mentioned-us'
|
||||||
|
: null,
|
||||||
|
isSelected ? 'module-conversation-list-item--is-selected' : null,
|
||||||
|
isBlocked ? 'module-conversation-list-item--is-blocked' : null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AvatarItem />
|
||||||
|
<div className="module-conversation-list-item__content">
|
||||||
|
<HeaderItem />
|
||||||
|
<MessageItem isMessageRequest={Boolean(isMessageRequest)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Portal>
|
||||||
|
<MemoConversationListItemContextMenu triggerId={triggerId} />
|
||||||
|
</Portal>
|
||||||
|
</div>
|
||||||
|
</ContextConversationId.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MemoConversationListItemWithDetails = React.memo(ConversationListItem, _.isEqual);
|
@ -0,0 +1,116 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useConversationPropsById, useIsPinned } from '../../../hooks/useParamSelector';
|
||||||
|
import { SectionType } from '../../../state/ducks/section';
|
||||||
|
import { getFocusedSection } from '../../../state/selectors/section';
|
||||||
|
import { Timestamp } from '../../conversation/Timestamp';
|
||||||
|
import { SessionIcon } from '../../icon';
|
||||||
|
import { ContextConversationId } from './ConversationListItem';
|
||||||
|
import { UserItem } from './UserItem';
|
||||||
|
|
||||||
|
const NotificationSettingIcon = (props: { isMessagesSection: boolean }) => {
|
||||||
|
const convoSetting = useSelector(useConversationPropsById)?.currentNotificationSetting;
|
||||||
|
|
||||||
|
if (!props.isMessagesSection) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (convoSetting) {
|
||||||
|
case 'all':
|
||||||
|
return null;
|
||||||
|
case 'disabled':
|
||||||
|
return (
|
||||||
|
<SessionIcon iconType="mute" iconColor={'var(--color-text-subtle)'} iconSize="small" />
|
||||||
|
);
|
||||||
|
case 'mentions_only':
|
||||||
|
return (
|
||||||
|
<SessionIcon iconType="bell" iconColor={'var(--color-text-subtle)'} iconSize="small" />
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledConversationListItemIconWrapper = styled.div`
|
||||||
|
svg {
|
||||||
|
margin: 0px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function useHeaderItemProps(conversationId: string) {
|
||||||
|
const convoProps = useConversationPropsById(conversationId);
|
||||||
|
if (!convoProps) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isPinned: !!convoProps.isPinned,
|
||||||
|
mentionedUs: convoProps.mentionedUs || false,
|
||||||
|
unreadCount: convoProps.unreadCount || 0,
|
||||||
|
activeAt: convoProps.activeAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItemIcons = () => {
|
||||||
|
const isMessagesSection = useSelector(getFocusedSection) === SectionType.Message;
|
||||||
|
const conversationId = useContext(ContextConversationId);
|
||||||
|
const isPinned = useIsPinned(conversationId);
|
||||||
|
|
||||||
|
const pinIcon =
|
||||||
|
isMessagesSection && isPinned ? (
|
||||||
|
<SessionIcon iconType="pin" iconColor={'var(--color-text-subtle)'} iconSize="small" />
|
||||||
|
) : null;
|
||||||
|
return (
|
||||||
|
<StyledConversationListItemIconWrapper>
|
||||||
|
{pinIcon}
|
||||||
|
<NotificationSettingIcon isMessagesSection={isMessagesSection} />
|
||||||
|
</StyledConversationListItemIconWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HeaderItem = () => {
|
||||||
|
const conversationId = useContext(ContextConversationId);
|
||||||
|
|
||||||
|
const convoProps = useHeaderItemProps(conversationId);
|
||||||
|
if (!convoProps) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { unreadCount, mentionedUs, activeAt } = convoProps;
|
||||||
|
|
||||||
|
let atSymbol = null;
|
||||||
|
let unreadCountDiv = null;
|
||||||
|
if (unreadCount > 0) {
|
||||||
|
atSymbol = mentionedUs ? <p className="at-symbol">@</p> : null;
|
||||||
|
unreadCountDiv = <p className="module-conversation-list-item__unread-count">{unreadCount}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-conversation-list-item__header">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-conversation-list-item__header__name',
|
||||||
|
unreadCount > 0 ? 'module-conversation-list-item__header__name--with-unread' : null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<UserItem />
|
||||||
|
</div>
|
||||||
|
<ListItemIcons />
|
||||||
|
|
||||||
|
{unreadCountDiv}
|
||||||
|
{atSymbol}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-conversation-list-item__header__date',
|
||||||
|
unreadCount > 0 ? 'module-conversation-list-item__header__date--has-unread' : null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Timestamp timestamp={activeAt} extended={false} isConversationListItem={true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,61 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { isEmpty } from 'underscore';
|
||||||
|
import { useConversationPropsById } from '../../../hooks/useParamSelector';
|
||||||
|
import { MessageBody } from '../../conversation/message/message-content/MessageBody';
|
||||||
|
import { OutgoingMessageStatus } from '../../conversation/message/message-content/OutgoingMessageStatus';
|
||||||
|
import { TypingAnimation } from '../../conversation/TypingAnimation';
|
||||||
|
import { ContextConversationId } from './ConversationListItem';
|
||||||
|
import { MessageRequestButtons } from './MessageRequest';
|
||||||
|
|
||||||
|
function useMessageItemProps(convoId: string) {
|
||||||
|
const convoProps = useConversationPropsById(convoId);
|
||||||
|
if (!convoProps) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isTyping: !!convoProps.isTyping,
|
||||||
|
lastMessage: convoProps.lastMessage,
|
||||||
|
|
||||||
|
unreadCount: convoProps.unreadCount || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MessageItem = (props: { isMessageRequest: boolean }) => {
|
||||||
|
const conversationId = useContext(ContextConversationId);
|
||||||
|
const convoProps = useMessageItemProps(conversationId);
|
||||||
|
if (!convoProps) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { lastMessage, isTyping, unreadCount } = convoProps;
|
||||||
|
|
||||||
|
if (!lastMessage && !isTyping) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const text = lastMessage?.text || '';
|
||||||
|
|
||||||
|
if (isEmpty(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-conversation-list-item__message">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-conversation-list-item__message__text',
|
||||||
|
unreadCount > 0 ? 'module-conversation-list-item__message__text--has-unread' : null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isTyping ? (
|
||||||
|
<TypingAnimation />
|
||||||
|
) : (
|
||||||
|
<MessageBody isGroup={true} text={text} disableJumbomoji={true} disableLinks={true} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<MessageRequestButtons isMessageRequest={props.isMessageRequest} />
|
||||||
|
{lastMessage && lastMessage.status && !props.isMessageRequest ? (
|
||||||
|
<OutgoingMessageStatus status={lastMessage.status} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,64 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import {
|
||||||
|
approveConversation,
|
||||||
|
blockConvoById,
|
||||||
|
} from '../../../interactions/conversationInteractions';
|
||||||
|
import { forceSyncConfigurationNowIfNeeded } from '../../../session/utils/syncUtils';
|
||||||
|
import { SessionIconButton } from '../../icon';
|
||||||
|
import { ContextConversationId } from './ConversationListItem';
|
||||||
|
|
||||||
|
const RejectMessageRequestButton = () => {
|
||||||
|
const conversationId = useContext(ContextConversationId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes conversation from requests list,
|
||||||
|
* adds ID to block list, syncs the block with linked devices.
|
||||||
|
*/
|
||||||
|
const handleConversationBlock = async () => {
|
||||||
|
await blockConvoById(conversationId);
|
||||||
|
await forceSyncConfigurationNowIfNeeded();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<SessionIconButton
|
||||||
|
iconType="exit"
|
||||||
|
iconSize="large"
|
||||||
|
onClick={handleConversationBlock}
|
||||||
|
backgroundColor="var(--color-destructive)"
|
||||||
|
iconColor="var(--color-foreground-primary)"
|
||||||
|
iconPadding="var(--margins-xs)"
|
||||||
|
borderRadius="2px"
|
||||||
|
margin="0 5px 0 0"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ApproveMessageRequestButton = () => {
|
||||||
|
const conversationId = useContext(ContextConversationId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SessionIconButton
|
||||||
|
iconType="check"
|
||||||
|
iconSize="large"
|
||||||
|
onClick={async () => {
|
||||||
|
await approveConversation(conversationId);
|
||||||
|
}}
|
||||||
|
backgroundColor="var(--color-accent)"
|
||||||
|
iconColor="var(--color-foreground-primary)"
|
||||||
|
iconPadding="var(--margins-xs)"
|
||||||
|
borderRadius="2px"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MessageRequestButtons = ({ isMessageRequest }: { isMessageRequest: boolean }) => {
|
||||||
|
if (!isMessageRequest) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RejectMessageRequestButton />
|
||||||
|
<ApproveMessageRequestButton />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,34 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { useConversationUsername, useIsMe } from '../../../hooks/useParamSelector';
|
||||||
|
import { PubKey } from '../../../session/types';
|
||||||
|
import { ContactName } from '../../conversation/ContactName';
|
||||||
|
import { ContextConversationId } from './ConversationListItem';
|
||||||
|
|
||||||
|
export const UserItem = () => {
|
||||||
|
const conversationId = useContext(ContextConversationId);
|
||||||
|
|
||||||
|
const shortenedPubkey = PubKey.shorten(conversationId);
|
||||||
|
const isMe = useIsMe(conversationId);
|
||||||
|
const username = useConversationUsername(conversationId);
|
||||||
|
|
||||||
|
const displayedPubkey = username ? shortenedPubkey : conversationId;
|
||||||
|
const displayName = isMe ? window.i18n('noteToSelf') : username;
|
||||||
|
|
||||||
|
let shouldShowPubkey = false;
|
||||||
|
if ((!username || username.length === 0) && (!displayName || displayName.length === 0)) {
|
||||||
|
shouldShowPubkey = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-conversation__user">
|
||||||
|
<ContactName
|
||||||
|
pubkey={displayedPubkey}
|
||||||
|
name={username}
|
||||||
|
profileName={displayName}
|
||||||
|
module="module-conversation__user"
|
||||||
|
boldProfileName={true}
|
||||||
|
shouldShowPubkey={shouldShowPubkey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue