Process incoming story messages
This commit is contained in:
parent
df7cdfacc7
commit
eb91eb6fec
84 changed files with 4382 additions and 652 deletions
240
ts/components/StoryListItem.tsx
Normal file
240
ts/components/StoryListItem.tsx
Normal file
|
@ -0,0 +1,240 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { Avatar, AvatarSize, AvatarStoryRing } from './Avatar';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { ContextMenuPopper } from './ContextMenu';
|
||||
import { getAvatarColor } from '../types/Colors';
|
||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||
|
||||
export type ConversationStoryType = {
|
||||
conversationId: string;
|
||||
group?: Pick<ConversationType, 'title'>;
|
||||
hasMultiple?: boolean;
|
||||
isHidden?: boolean;
|
||||
searchNames?: string; // This is just here to satisfy Fuse's types
|
||||
stories: Array<StoryViewType>;
|
||||
};
|
||||
|
||||
export type StoryViewType = {
|
||||
attachment?: AttachmentType;
|
||||
hasReplies?: boolean;
|
||||
hasRepliesFromSelf?: boolean;
|
||||
isHidden?: boolean;
|
||||
isUnread?: boolean;
|
||||
messageId: string;
|
||||
selectedReaction?: string;
|
||||
sender: Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'firstName'
|
||||
| 'id'
|
||||
| 'isMe'
|
||||
| 'name'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
>;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type PropsType = Pick<
|
||||
ConversationStoryType,
|
||||
'group' | 'hasMultiple' | 'isHidden'
|
||||
> & {
|
||||
i18n: LocalizerType;
|
||||
onClick: () => unknown;
|
||||
onGoToConversation?: (conversationId: string) => unknown;
|
||||
onHideStory?: (conversationId: string) => unknown;
|
||||
story: StoryViewType;
|
||||
};
|
||||
|
||||
export const StoryListItem = ({
|
||||
group,
|
||||
hasMultiple,
|
||||
i18n,
|
||||
isHidden,
|
||||
onClick,
|
||||
onGoToConversation,
|
||||
onHideStory,
|
||||
story,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
|
||||
const [isShowingContextMenu, setIsShowingContextMenu] = useState(false);
|
||||
const [referenceElement, setReferenceElement] =
|
||||
useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const {
|
||||
attachment,
|
||||
hasReplies,
|
||||
hasRepliesFromSelf,
|
||||
isUnread,
|
||||
sender,
|
||||
timestamp,
|
||||
} = story;
|
||||
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
color,
|
||||
firstName,
|
||||
isMe,
|
||||
name,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
title,
|
||||
} = sender;
|
||||
|
||||
let avatarStoryRing: AvatarStoryRing | undefined;
|
||||
if (attachment) {
|
||||
avatarStoryRing = isUnread ? AvatarStoryRing.Unread : AvatarStoryRing.Read;
|
||||
}
|
||||
|
||||
let repliesElement: JSX.Element | undefined;
|
||||
if (hasRepliesFromSelf) {
|
||||
repliesElement = <div className="StoryListItem__info--replies--self" />;
|
||||
} else if (hasReplies) {
|
||||
repliesElement = <div className="StoryListItem__info--replies--others" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
aria-label={i18n('StoryListItem__label')}
|
||||
className={classNames('StoryListItem', {
|
||||
'StoryListItem--hidden': isHidden,
|
||||
})}
|
||||
onClick={onClick}
|
||||
onContextMenu={ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (!isMe) {
|
||||
setIsShowingContextMenu(true);
|
||||
}
|
||||
}}
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
>
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
avatarPath={avatarPath}
|
||||
badge={undefined}
|
||||
color={getAvatarColor(color)}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={Boolean(isMe)}
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
storyRing={avatarStoryRing}
|
||||
title={title}
|
||||
/>
|
||||
<div className="StoryListItem__info">
|
||||
{isMe ? (
|
||||
<>
|
||||
<div className="StoryListItem__info--title">
|
||||
{i18n('Stories__mine')}
|
||||
</div>
|
||||
{!attachment && (
|
||||
<div className="StoryListItem__info--timestamp">
|
||||
{i18n('Stories__add')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="StoryListItem__info--title">
|
||||
{group
|
||||
? i18n('Stories__from-to-group', {
|
||||
name: title,
|
||||
group: group.title,
|
||||
})
|
||||
: title}
|
||||
</div>
|
||||
<MessageTimestamp
|
||||
i18n={i18n}
|
||||
module="StoryListItem__info--timestamp"
|
||||
now={Date.now()}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{repliesElement}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames('StoryListItem__previews', {
|
||||
'StoryListItem__previews--multiple': hasMultiple,
|
||||
})}
|
||||
>
|
||||
{!attachment && isMe && (
|
||||
<div
|
||||
aria-label={i18n('Stories__add')}
|
||||
className="StoryListItem__previews--add StoryListItem__previews--image"
|
||||
/>
|
||||
)}
|
||||
{hasMultiple && <div className="StoryListItem__previews--more" />}
|
||||
{attachment && (
|
||||
<div
|
||||
className="StoryListItem__previews--image"
|
||||
style={{
|
||||
backgroundImage: `url("${attachment.thumbnail?.url}")`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
<ContextMenuPopper
|
||||
isMenuShowing={isShowingContextMenu}
|
||||
menuOptions={[
|
||||
{
|
||||
icon: 'StoryListItem__icon--hide',
|
||||
label: i18n('StoryListItem__hide'),
|
||||
onClick: () => {
|
||||
setHasConfirmHideStory(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'StoryListItem__icon--chat',
|
||||
label: i18n('StoryListItem__go-to-chat'),
|
||||
onClick: () => {
|
||||
onGoToConversation?.(sender.id);
|
||||
},
|
||||
},
|
||||
]}
|
||||
onClose={() => setIsShowingContextMenu(false)}
|
||||
popperOptions={{
|
||||
placement: 'bottom',
|
||||
strategy: 'absolute',
|
||||
}}
|
||||
referenceElement={referenceElement}
|
||||
/>
|
||||
{hasConfirmHideStory && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
action: () => onHideStory?.(sender.id),
|
||||
style: 'affirmative',
|
||||
text: i18n('StoryListItem__hide-modal--confirm'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setHasConfirmHideStory(false);
|
||||
}}
|
||||
>
|
||||
{i18n('StoryListItem__hide-modal--body', [String(firstName)])}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue