signal-desktop/ts/components/StoryListItem.tsx

234 lines
6.5 KiB
TypeScript
Raw Normal View History

2022-03-04 21:14:52 +00:00
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState } from 'react';
import classNames from 'classnames';
2022-07-01 00:52:03 +00:00
import type { ConversationStoryType, StoryViewType } from '../types/Stories';
2022-11-09 02:38:19 +00:00
import type { ConversationType } from '../state/ducks/conversations';
2023-08-09 00:53:06 +00:00
import type { LocalizerType, ThemeType } from '../types/Util';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
2022-08-22 17:44:23 +00:00
import type { ViewUserStoriesActionCreatorType } from '../state/ducks/stories';
2022-07-22 00:44:35 +00:00
import { Avatar, AvatarSize } from './Avatar';
2022-03-04 21:14:52 +00:00
import { ConfirmationDialog } from './ConfirmationDialog';
2022-07-25 18:55:44 +00:00
import { ContextMenu } from './ContextMenu';
2022-11-09 21:11:45 +00:00
import { SIGNAL_ACI } from '../types/SignalConversation';
2022-11-09 02:38:19 +00:00
import { StoryViewTargetType, HasStories } from '../types/Stories';
2022-10-11 17:59:02 +00:00
2022-03-04 21:14:52 +00:00
import { MessageTimestamp } from './conversation/MessageTimestamp';
2022-03-29 01:10:08 +00:00
import { StoryImage } from './StoryImage';
import { getAvatarColor } from '../types/Colors';
2022-03-04 21:14:52 +00:00
2022-07-01 00:52:03 +00:00
export type PropsType = Pick<ConversationStoryType, 'group' | 'isHidden'> & {
conversationId: string;
getPreferredBadge: PreferredBadgeSelectorType;
hasReplies?: boolean;
hasRepliesFromSelf?: boolean;
2022-03-04 21:14:52 +00:00
i18n: LocalizerType;
onGoToConversation: (conversationId: string) => unknown;
onHideStory: (conversationId: string) => unknown;
2022-03-29 01:10:08 +00:00
queueStoryDownload: (storyId: string) => unknown;
2023-02-24 23:18:57 +00:00
onMediaPlaybackStart: () => void;
2022-03-04 21:14:52 +00:00
story: StoryViewType;
2023-08-09 00:53:06 +00:00
theme: ThemeType;
2022-08-22 17:44:23 +00:00
viewUserStories: ViewUserStoriesActionCreatorType;
2022-03-04 21:14:52 +00:00
};
function StoryListItemAvatar({
acceptedMessageRequest,
2024-07-11 19:44:09 +00:00
avatarUrl,
avatarStoryRing,
badges,
color,
getPreferredBadge,
i18n,
isMe,
profileName,
sharedGroupNames,
title,
2023-08-09 00:53:06 +00:00
theme,
}: Pick<
ConversationType,
| 'acceptedMessageRequest'
2024-07-11 19:44:09 +00:00
| 'avatarUrl'
| 'color'
| 'profileName'
| 'sharedGroupNames'
| 'title'
> & {
avatarStoryRing?: HasStories;
badges?: ConversationType['badges'];
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
isMe?: boolean;
2023-08-09 00:53:06 +00:00
theme: ThemeType;
}): JSX.Element {
return (
<Avatar
acceptedMessageRequest={acceptedMessageRequest}
2024-07-11 19:44:09 +00:00
avatarUrl={avatarUrl}
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}
2023-08-09 00:53:06 +00:00
theme={theme}
title={title}
/>
);
}
2022-11-18 00:45:19 +00:00
export function StoryListItem({
conversationId,
getPreferredBadge,
2022-03-04 21:14:52 +00:00
group,
hasReplies,
hasRepliesFromSelf,
2022-03-04 21:14:52 +00:00
i18n,
isHidden,
onGoToConversation,
onHideStory,
2023-02-24 23:18:57 +00:00
onMediaPlaybackStart,
2022-03-29 01:10:08 +00:00
queueStoryDownload,
2022-03-04 21:14:52 +00:00
story,
2023-08-09 00:53:06 +00:00
theme,
2022-07-25 18:55:44 +00:00
viewUserStories,
2022-11-18 00:45:19 +00:00
}: PropsType): JSX.Element {
2022-03-04 21:14:52 +00:00
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
const { attachment, isUnread, sender, timestamp } = story;
2022-03-04 21:14:52 +00:00
const { firstName, title } = sender;
2022-03-04 21:14:52 +00:00
2023-08-16 20:54:39 +00:00
const isSignalOfficial = sender.serviceId === SIGNAL_ACI;
2022-11-09 02:38:19 +00:00
2022-07-22 00:44:35 +00:00
let avatarStoryRing: HasStories | undefined;
2022-03-04 21:14:52 +00:00
if (attachment) {
2022-07-22 00:44:35 +00:00
avatarStoryRing = isUnread ? HasStories.Unread : HasStories.Read;
2022-03-04 21:14:52 +00:00
}
let repliesElement: JSX.Element | undefined;
2022-11-23 04:05:33 +00:00
if (group === undefined && hasRepliesFromSelf) {
2022-03-04 21:14:52 +00:00
repliesElement = <div className="StoryListItem__info--replies--self" />;
2022-11-23 04:05:33 +00:00
} else if (group && (hasReplies || hasRepliesFromSelf)) {
2022-03-04 21:14:52 +00:00
repliesElement = <div className="StoryListItem__info--replies--others" />;
}
const menuOptions = [];
if (isHidden) {
menuOptions.push({
icon: 'StoryListItem__icon--unhide',
label: i18n('icu:StoryListItem__unhide'),
onClick: () => {
onHideStory(conversationId);
},
});
} else {
menuOptions.push({
2022-11-09 02:38:19 +00:00
icon: 'StoryListItem__icon--hide',
label: i18n('icu:StoryListItem__hide'),
2022-11-09 02:38:19 +00:00
onClick: () => {
setHasConfirmHideStory(true);
2022-11-09 02:38:19 +00:00
},
});
}
2022-11-09 02:38:19 +00:00
if (!isSignalOfficial) {
menuOptions.push({
icon: 'StoryListItem__icon--info',
2023-03-30 00:03:25 +00:00
label: i18n('icu:StoryListItem__info'),
2022-11-09 02:38:19 +00:00
onClick: () =>
viewUserStories({
conversationId,
viewTarget: StoryViewTargetType.Details,
}),
});
menuOptions.push({
icon: 'StoryListItem__icon--chat',
2023-03-30 00:03:25 +00:00
label: i18n('icu:StoryListItem__go-to-chat'),
2022-11-09 02:38:19 +00:00
onClick: () => onGoToConversation(conversationId),
});
}
2022-03-04 21:14:52 +00:00
return (
<>
2022-07-25 18:55:44 +00:00
<ContextMenu
2023-03-30 00:03:25 +00:00
aria-label={i18n('icu:StoryListItem__label')}
2022-07-25 18:55:44 +00:00
i18n={i18n}
2022-11-09 02:38:19 +00:00
menuOptions={menuOptions}
2022-07-25 18:55:44 +00:00
moduleClassName={classNames('StoryListItem', {
2022-03-04 21:14:52 +00:00
'StoryListItem--hidden': isHidden,
})}
2022-08-22 17:44:23 +00:00
onClick={() => viewUserStories({ conversationId })}
2022-07-25 18:55:44 +00:00
popperOptions={{
placement: 'bottom',
strategy: 'absolute',
2022-03-04 21:14:52 +00:00
}}
>
<StoryListItemAvatar
avatarStoryRing={avatarStoryRing}
getPreferredBadge={getPreferredBadge}
2022-03-04 21:14:52 +00:00
i18n={i18n}
2023-08-09 00:53:06 +00:00
theme={theme}
{...(group || sender)}
2022-03-04 21:14:52 +00:00
/>
<div className="StoryListItem__info">
2022-11-18 00:45:19 +00:00
<div className="StoryListItem__info--title">
{group ? group.title : title}
{isSignalOfficial && (
2023-06-16 19:04:31 +00:00
<span className="ContactModal__official-badge" />
2022-11-09 02:38:19 +00:00
)}
2022-11-18 00:45:19 +00:00
</div>
{!isSignalOfficial && (
<MessageTimestamp
i18n={i18n}
isRelativeTime
module="StoryListItem__info--timestamp"
timestamp={timestamp}
/>
)}
2022-03-04 21:14:52 +00:00
{repliesElement}
</div>
2022-07-01 00:52:03 +00:00
<div className="StoryListItem__previews">
2022-03-29 01:10:08 +00:00
<StoryImage
attachment={attachment}
2022-08-04 00:38:41 +00:00
firstName={firstName || title}
2022-03-29 01:10:08 +00:00
i18n={i18n}
isThumbnail
label=""
moduleClassName="StoryListItem__previews--image"
queueStoryDownload={queueStoryDownload}
storyId={story.messageId}
2023-02-24 23:18:57 +00:00
onMediaPlaybackStart={onMediaPlaybackStart}
2022-03-29 01:10:08 +00:00
/>
2022-03-04 21:14:52 +00:00
</div>
2022-07-25 18:55:44 +00:00
</ContextMenu>
2022-03-04 21:14:52 +00:00
{hasConfirmHideStory && (
<ConfirmationDialog
2022-09-27 20:24:21 +00:00
dialogName="StoryListItem.hideStory"
2022-03-04 21:14:52 +00:00
actions={[
{
action: () => onHideStory(conversationId),
2022-03-04 21:14:52 +00:00
style: 'affirmative',
2023-03-30 00:03:25 +00:00
text: i18n('icu:StoryListItem__hide-modal--confirm'),
2022-03-04 21:14:52 +00:00
},
]}
i18n={i18n}
onClose={() => {
setHasConfirmHideStory(false);
}}
>
2023-03-30 00:03:25 +00:00
{i18n('icu:StoryListItem__hide-modal--body', {
2023-03-27 23:37:39 +00:00
name: String(firstName),
})}
2022-03-04 21:14:52 +00:00
</ConfirmationDialog>
)}
</>
);
2022-11-18 00:45:19 +00:00
}