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