221 lines
6.2 KiB
TypeScript
221 lines
6.2 KiB
TypeScript
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import React, { useState } from 'react';
|
|
import classNames from 'classnames';
|
|
import type { ConversationStoryType, StoryViewType } from '../types/Stories';
|
|
import type { ConversationType } from '../state/ducks/conversations';
|
|
import type { LocalizerType } from '../types/Util';
|
|
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
|
import type { ViewUserStoriesActionCreatorType } from '../state/ducks/stories';
|
|
import { Avatar, AvatarSize } from './Avatar';
|
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
|
import { ContextMenu } from './ContextMenu';
|
|
import { SIGNAL_ACI } from '../types/SignalConversation';
|
|
import { StoryViewTargetType, HasStories } from '../types/Stories';
|
|
|
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
|
import { StoryImage } from './StoryImage';
|
|
import { ThemeType } from '../types/Util';
|
|
import { getAvatarColor } from '../types/Colors';
|
|
|
|
export type PropsType = Pick<ConversationStoryType, 'group' | 'isHidden'> & {
|
|
conversationId: string;
|
|
getPreferredBadge: PreferredBadgeSelectorType;
|
|
hasReplies?: boolean;
|
|
hasRepliesFromSelf?: boolean;
|
|
i18n: LocalizerType;
|
|
onGoToConversation: (conversationId: string) => unknown;
|
|
onHideStory: (conversationId: string) => unknown;
|
|
queueStoryDownload: (storyId: string) => unknown;
|
|
story: StoryViewType;
|
|
viewUserStories: ViewUserStoriesActionCreatorType;
|
|
};
|
|
|
|
function StoryListItemAvatar({
|
|
acceptedMessageRequest,
|
|
avatarPath,
|
|
avatarStoryRing,
|
|
badges,
|
|
color,
|
|
getPreferredBadge,
|
|
i18n,
|
|
isMe,
|
|
profileName,
|
|
sharedGroupNames,
|
|
title,
|
|
}: Pick<
|
|
ConversationType,
|
|
| 'acceptedMessageRequest'
|
|
| 'avatarPath'
|
|
| 'color'
|
|
| 'profileName'
|
|
| 'sharedGroupNames'
|
|
| 'title'
|
|
> & {
|
|
avatarStoryRing?: HasStories;
|
|
badges?: ConversationType['badges'];
|
|
getPreferredBadge: PreferredBadgeSelectorType;
|
|
i18n: LocalizerType;
|
|
isMe?: boolean;
|
|
}): JSX.Element {
|
|
return (
|
|
<Avatar
|
|
acceptedMessageRequest={acceptedMessageRequest}
|
|
avatarPath={avatarPath}
|
|
badge={badges ? getPreferredBadge(badges) : undefined}
|
|
color={getAvatarColor(color)}
|
|
conversationType="direct"
|
|
i18n={i18n}
|
|
isMe={Boolean(isMe)}
|
|
profileName={profileName}
|
|
sharedGroupNames={sharedGroupNames}
|
|
size={AvatarSize.FORTY_EIGHT}
|
|
storyRing={avatarStoryRing}
|
|
theme={ThemeType.dark}
|
|
title={title}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function StoryListItem({
|
|
conversationId,
|
|
getPreferredBadge,
|
|
group,
|
|
hasReplies,
|
|
hasRepliesFromSelf,
|
|
i18n,
|
|
isHidden,
|
|
onGoToConversation,
|
|
onHideStory,
|
|
queueStoryDownload,
|
|
story,
|
|
viewUserStories,
|
|
}: PropsType): JSX.Element {
|
|
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
|
|
|
|
const { attachment, isUnread, sender, timestamp } = story;
|
|
|
|
const { firstName, title } = sender;
|
|
|
|
const isSignalOfficial = sender.uuid === SIGNAL_ACI;
|
|
|
|
let avatarStoryRing: HasStories | undefined;
|
|
if (attachment) {
|
|
avatarStoryRing = isUnread ? HasStories.Unread : HasStories.Read;
|
|
}
|
|
|
|
let repliesElement: JSX.Element | undefined;
|
|
if (group === undefined && hasRepliesFromSelf) {
|
|
repliesElement = <div className="StoryListItem__info--replies--self" />;
|
|
} else if (group && (hasReplies || hasRepliesFromSelf)) {
|
|
repliesElement = <div className="StoryListItem__info--replies--others" />;
|
|
}
|
|
|
|
const menuOptions = [
|
|
{
|
|
icon: 'StoryListItem__icon--hide',
|
|
label: isHidden
|
|
? i18n('StoryListItem__unhide')
|
|
: i18n('StoryListItem__hide'),
|
|
onClick: () => {
|
|
if (isHidden) {
|
|
onHideStory(conversationId);
|
|
} else {
|
|
setHasConfirmHideStory(true);
|
|
}
|
|
},
|
|
},
|
|
];
|
|
|
|
if (!isSignalOfficial) {
|
|
menuOptions.push({
|
|
icon: 'StoryListItem__icon--info',
|
|
label: i18n('StoryListItem__info'),
|
|
onClick: () =>
|
|
viewUserStories({
|
|
conversationId,
|
|
viewTarget: StoryViewTargetType.Details,
|
|
}),
|
|
});
|
|
|
|
menuOptions.push({
|
|
icon: 'StoryListItem__icon--chat',
|
|
label: i18n('StoryListItem__go-to-chat'),
|
|
onClick: () => onGoToConversation(conversationId),
|
|
});
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<ContextMenu
|
|
aria-label={i18n('StoryListItem__label')}
|
|
i18n={i18n}
|
|
menuOptions={menuOptions}
|
|
moduleClassName={classNames('StoryListItem', {
|
|
'StoryListItem--hidden': isHidden,
|
|
})}
|
|
onClick={() => viewUserStories({ conversationId })}
|
|
popperOptions={{
|
|
placement: 'bottom',
|
|
strategy: 'absolute',
|
|
}}
|
|
>
|
|
<StoryListItemAvatar
|
|
avatarStoryRing={avatarStoryRing}
|
|
getPreferredBadge={getPreferredBadge}
|
|
i18n={i18n}
|
|
{...(group || sender)}
|
|
/>
|
|
<div className="StoryListItem__info">
|
|
<div className="StoryListItem__info--title">
|
|
{group ? group.title : title}
|
|
{isSignalOfficial && (
|
|
<span className="StoryListItem__signal-official" />
|
|
)}
|
|
</div>
|
|
{!isSignalOfficial && (
|
|
<MessageTimestamp
|
|
i18n={i18n}
|
|
isRelativeTime
|
|
module="StoryListItem__info--timestamp"
|
|
timestamp={timestamp}
|
|
/>
|
|
)}
|
|
{repliesElement}
|
|
</div>
|
|
|
|
<div className="StoryListItem__previews">
|
|
<StoryImage
|
|
attachment={attachment}
|
|
firstName={firstName || title}
|
|
i18n={i18n}
|
|
isThumbnail
|
|
label=""
|
|
moduleClassName="StoryListItem__previews--image"
|
|
queueStoryDownload={queueStoryDownload}
|
|
storyId={story.messageId}
|
|
/>
|
|
</div>
|
|
</ContextMenu>
|
|
{hasConfirmHideStory && (
|
|
<ConfirmationDialog
|
|
dialogName="StoryListItem.hideStory"
|
|
actions={[
|
|
{
|
|
action: () => onHideStory(conversationId),
|
|
style: 'affirmative',
|
|
text: i18n('StoryListItem__hide-modal--confirm'),
|
|
},
|
|
]}
|
|
i18n={i18n}
|
|
onClose={() => {
|
|
setHasConfirmHideStory(false);
|
|
}}
|
|
>
|
|
{i18n('StoryListItem__hide-modal--body', [String(firstName)])}
|
|
</ConfirmationDialog>
|
|
)}
|
|
</>
|
|
);
|
|
}
|