Render group stories

This commit is contained in:
Josh Perez 2022-04-14 20:08:46 -04:00 committed by GitHub
parent 14ab7b9e0d
commit e3d537cbd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 527 additions and 163 deletions

View File

@ -107,11 +107,16 @@
} }
&__actions { &__actions {
margin: 16px 0 32px 0; align-items: center;
display: flex;
justify-content: center;
margin-bottom: 32px;
min-height: 52px;
} }
&__reply { &__reply {
@include button-reset; @include button-reset;
color: $color-gray-05;
@include keyboard-mode { @include keyboard-mode {
&:focus { &:focus {
color: $color-ultramarine; color: $color-ultramarine;

View File

@ -125,6 +125,11 @@
border-radius: 18px; border-radius: 18px;
margin-left: 8px; margin-left: 8px;
padding: 7px 12px; padding: 7px 12px;
&--doe {
background: none;
border: 1px solid $color-gray-75;
}
} }
&__quote { &__quote {

View File

@ -73,6 +73,7 @@
&__title { &__title {
align-items: flex-start; align-items: flex-start;
color: $color-gray-05;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;

View File

@ -86,11 +86,15 @@ export const ModalHost = React.memo(
<animated.div <animated.div
role="presentation" role="presentation"
className={getClassName('__overlay')} className={getClassName('__overlay')}
onMouseDown={noMouseClose ? undefined : handleMouseDown}
onMouseUp={noMouseClose ? undefined : handleMouseUp}
style={overlayStyles} style={overlayStyles}
/> />
<div className={getClassName('__overlay-container')}>{children}</div> <div
className={getClassName('__overlay-container')}
onMouseDown={noMouseClose ? undefined : handleMouseDown}
onMouseUp={noMouseClose ? undefined : handleMouseUp}
>
{children}
</div>
</div> </div>
); );

View File

@ -7,6 +7,7 @@ import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
import type { ConversationType } from '../state/ducks/conversations';
import type { PropsType } from './Stories'; import type { PropsType } from './Stories';
import { Stories } from './Stories'; import { Stories } from './Stories';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
@ -28,7 +29,17 @@ function createStory({
timestamp, timestamp,
}: { }: {
attachment?: AttachmentType; attachment?: AttachmentType;
group?: { title: string }; group?: Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'id'
| 'name'
| 'profileName'
| 'sharedGroupNames'
| 'title'
>;
timestamp: number; timestamp: number;
}) { }) {
const replies = Math.random() > 0.5; const replies = Math.random() > 0.5;
@ -87,7 +98,7 @@ const getDefaultProps = (): PropsType => ({
timestamp: Date.now() - 5 * durations.MINUTE, timestamp: Date.now() - 5 * durations.MINUTE,
}), }),
createStory({ createStory({
group: { title: 'BBQ in the park' }, group: getDefaultConversation({ title: 'BBQ in the park' }),
attachment: getAttachmentWithThumbnail( attachment: getAttachmentWithThumbnail(
'/fixtures/nathan-anderson-316188-unsplash.jpg' '/fixtures/nathan-anderson-316188-unsplash.jpg'
), ),
@ -102,7 +113,7 @@ const getDefaultProps = (): PropsType => ({
timestamp: Date.now() - 164 * durations.MINUTE, timestamp: Date.now() - 164 * durations.MINUTE,
}), }),
createStory({ createStory({
group: { title: 'Breaking Signal for Science' }, group: getDefaultConversation({ title: 'Breaking Signal for Science' }),
attachment: getAttachmentWithThumbnail('/fixtures/kitten-2-64-64.jpg'), attachment: getAttachmentWithThumbnail('/fixtures/kitten-2-64-64.jpg'),
timestamp: Date.now() - 380 * durations.MINUTE, timestamp: Date.now() - 380 * durations.MINUTE,
}), }),

View File

@ -4,7 +4,7 @@
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ConversationStoryType, StoryViewType } from './StoryListItem'; import type { ConversationStoryType } from './StoryListItem';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { PropsType as SmartStoryViewerPropsType } from '../state/smart/StoryViewer'; import type { PropsType as SmartStoryViewerPropsType } from '../state/smart/StoryViewer';
import { StoriesPane } from './StoriesPane'; import { StoriesPane } from './StoriesPane';
@ -23,11 +23,6 @@ export type PropsType = {
toggleStoriesView: () => unknown; toggleStoriesView: () => unknown;
}; };
type ViewingStoryType = {
conversationId: string;
stories: Array<StoryViewType>;
};
export const Stories = ({ export const Stories = ({
hiddenStories, hiddenStories,
i18n, i18n,
@ -39,8 +34,8 @@ export const Stories = ({
toggleHideStories, toggleHideStories,
toggleStoriesView, toggleStoriesView,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [storiesToView, setStoriesToView] = useState< const [conversationIdToView, setConversationIdToView] = useState<
undefined | ViewingStoryType undefined | string
>(); >();
const width = getWidthFromPreferredWidth(preferredWidthFromStorage, { const width = getWidthFromPreferredWidth(preferredWidthFromStorage, {
@ -49,42 +44,35 @@ export const Stories = ({
return ( return (
<div className={classNames('Stories', themeClassName(Theme.Dark))}> <div className={classNames('Stories', themeClassName(Theme.Dark))}>
{storiesToView && {conversationIdToView &&
renderStoryViewer({ renderStoryViewer({
conversationId: storiesToView.conversationId, conversationId: conversationIdToView,
onClose: () => setStoriesToView(undefined), onClose: () => setConversationIdToView(undefined),
onNextUserStories: () => { onNextUserStories: () => {
const storyIndex = stories.findIndex( const storyIndex = stories.findIndex(
x => x.conversationId === storiesToView.conversationId x => x.conversationId === conversationIdToView
); );
if (storyIndex >= stories.length - 1) { if (storyIndex >= stories.length - 1) {
setStoriesToView(undefined); setConversationIdToView(undefined);
return; return;
} }
const nextStory = stories[storyIndex + 1]; const nextStory = stories[storyIndex + 1];
setStoriesToView({ setConversationIdToView(nextStory.conversationId);
conversationId: nextStory.conversationId,
stories: nextStory.stories,
});
}, },
onPrevUserStories: () => { onPrevUserStories: () => {
const storyIndex = stories.findIndex( const storyIndex = stories.findIndex(
x => x.conversationId === storiesToView.conversationId x => x.conversationId === conversationIdToView
); );
if (storyIndex === 0) { if (storyIndex === 0) {
setStoriesToView(undefined); setConversationIdToView(undefined);
return; return;
} }
const prevStory = stories[storyIndex - 1]; const prevStory = stories[storyIndex - 1];
setStoriesToView({ setConversationIdToView(prevStory.conversationId);
conversationId: prevStory.conversationId,
stories: prevStory.stories,
});
}, },
stories: storiesToView.stories,
})} })}
<div className="Stories__pane" style={{ width }}> <FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}> <div className="Stories__pane" style={{ width }}>
<StoriesPane <StoriesPane
hiddenStories={hiddenStories} hiddenStories={hiddenStories}
i18n={i18n} i18n={i18n}
@ -96,10 +84,7 @@ export const Stories = ({
const foundStory = stories[storyIndex]; const foundStory = stories[storyIndex];
if (foundStory) { if (foundStory) {
setStoriesToView({ setConversationIdToView(conversationId);
conversationId,
stories: foundStory.stories,
});
} }
}} }}
openConversationInternal={openConversationInternal} openConversationInternal={openConversationInternal}
@ -107,8 +92,8 @@ export const Stories = ({
stories={stories} stories={stories}
toggleHideStories={toggleHideStories} toggleHideStories={toggleHideStories}
/> />
</FocusTrap> </div>
</div> </FocusTrap>
<div className="Stories__placeholder"> <div className="Stories__placeholder">
<div className="Stories__placeholder__stories" /> <div className="Stories__placeholder__stories" />
{i18n('Stories__placeholder--text')} {i18n('Stories__placeholder--text')}

View File

@ -111,8 +111,9 @@ export const StoriesPane = ({
> >
{renderedStories.map(story => ( {renderedStories.map(story => (
<StoryListItem <StoryListItem
key={getNewestStory(story).timestamp} group={story.group}
i18n={i18n} i18n={i18n}
key={getNewestStory(story).timestamp}
onClick={() => { onClick={() => {
onStoryClicked(story.conversationId); onStoryClicked(story.conversationId);
}} }}

View File

@ -63,7 +63,7 @@ story.add('My Story (many)', () => (
story.add("Someone's story", () => ( story.add("Someone's story", () => (
<StoryListItem <StoryListItem
{...getDefaultProps()} {...getDefaultProps()}
group={{ title: 'Sports Group' }} group={getDefaultConversation({ title: 'Sports Group' })}
story={{ story={{
attachment: fakeAttachment({ attachment: fakeAttachment({
thumbnail: fakeThumbnail('/fixtures/tina-rolf-269345-unsplash.jpg'), thumbnail: fakeThumbnail('/fixtures/tina-rolf-269345-unsplash.jpg'),

View File

@ -15,7 +15,17 @@ import { getAvatarColor } from '../types/Colors';
export type ConversationStoryType = { export type ConversationStoryType = {
conversationId: string; conversationId: string;
group?: Pick<ConversationType, 'title'>; group?: Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'id'
| 'name'
| 'profileName'
| 'sharedGroupNames'
| 'title'
>;
hasMultiple?: boolean; hasMultiple?: boolean;
isHidden?: boolean; isHidden?: boolean;
searchNames?: string; // This is just here to satisfy Fuse's types searchNames?: string; // This is just here to satisfy Fuse's types
@ -24,6 +34,7 @@ export type ConversationStoryType = {
export type StoryViewType = { export type StoryViewType = {
attachment?: AttachmentType; attachment?: AttachmentType;
canReply?: boolean;
hasReplies?: boolean; hasReplies?: boolean;
hasRepliesFromSelf?: boolean; hasRepliesFromSelf?: boolean;
isHidden?: boolean; isHidden?: boolean;

View File

@ -17,10 +17,14 @@ const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/StoryViewer', module); const story = storiesOf('Components/StoryViewer', module);
function getDefaultProps(): PropsType { function getDefaultProps(): PropsType {
const sender = getDefaultConversation();
return { return {
conversationId: sender.id,
getPreferredBadge: () => undefined, getPreferredBadge: () => undefined,
group: undefined, group: undefined,
i18n, i18n,
loadStoryReplies: action('loadStoryReplies'),
markStoryRead: action('markStoryRead'), markStoryRead: action('markStoryRead'),
onClose: action('onClose'), onClose: action('onClose'),
onNextUserStories: action('onNextUserStories'), onNextUserStories: action('onNextUserStories'),
@ -33,18 +37,16 @@ function getDefaultProps(): PropsType {
preferredReactionEmoji: ['❤️', '👍', '👎', '😂', '😮', '😢'], preferredReactionEmoji: ['❤️', '👍', '👎', '😂', '😮', '😢'],
queueStoryDownload: action('queueStoryDownload'), queueStoryDownload: action('queueStoryDownload'),
renderEmojiPicker: () => <div />, renderEmojiPicker: () => <div />,
replies: Math.floor(Math.random() * 20),
stories: [ stories: [
{ {
attachment: fakeAttachment({ attachment: fakeAttachment({
url: '/fixtures/snow.jpg', url: '/fixtures/snow.jpg',
}), }),
messageId: '123', messageId: '123',
sender: getDefaultConversation(), sender,
timestamp: Date.now(), timestamp: Date.now(),
}, },
], ],
views: Math.floor(Math.random() * 20),
}; };
} }

View File

@ -9,6 +9,7 @@ import type { ConversationType } from '../state/ducks/conversations';
import type { EmojiPickDataType } from './emoji/EmojiPicker'; import type { EmojiPickDataType } from './emoji/EmojiPicker';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker'; import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
import type { ReplyStateType } from '../types/Stories';
import type { StoryViewType } from './StoryListItem'; import type { StoryViewType } from './StoryListItem';
import { Avatar, AvatarSize } from './Avatar'; import { Avatar, AvatarSize } from './Avatar';
import { Intl } from './Intl'; import { Intl } from './Intl';
@ -22,9 +23,21 @@ import { isDownloaded, isDownloading } from '../types/Attachment';
import { useEscapeHandling } from '../hooks/useEscapeHandling'; import { useEscapeHandling } from '../hooks/useEscapeHandling';
export type PropsType = { export type PropsType = {
conversationId: string;
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
group?: ConversationType; group?: Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'id'
| 'name'
| 'profileName'
| 'sharedGroupNames'
| 'title'
>;
i18n: LocalizerType; i18n: LocalizerType;
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
markStoryRead: (mId: string) => unknown; markStoryRead: (mId: string) => unknown;
onClose: () => unknown; onClose: () => unknown;
onNextUserStories: () => unknown; onNextUserStories: () => unknown;
@ -42,11 +55,10 @@ export type PropsType = {
preferredReactionEmoji: Array<string>; preferredReactionEmoji: Array<string>;
queueStoryDownload: (storyId: string) => unknown; queueStoryDownload: (storyId: string) => unknown;
recentEmojis?: Array<string>; recentEmojis?: Array<string>;
replies?: number;
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element; renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
replyState?: ReplyStateType;
skinTone?: number; skinTone?: number;
stories: Array<StoryViewType>; stories: Array<StoryViewType>;
views?: number;
}; };
const CAPTION_BUFFER = 20; const CAPTION_BUFFER = 20;
@ -54,9 +66,11 @@ const CAPTION_INITIAL_LENGTH = 200;
const CAPTION_MAX_LENGTH = 700; const CAPTION_MAX_LENGTH = 700;
export const StoryViewer = ({ export const StoryViewer = ({
conversationId,
getPreferredBadge, getPreferredBadge,
group, group,
i18n, i18n,
loadStoryReplies,
markStoryRead, markStoryRead,
onClose, onClose,
onNextUserStories, onNextUserStories,
@ -70,17 +84,16 @@ export const StoryViewer = ({
queueStoryDownload, queueStoryDownload,
recentEmojis, recentEmojis,
renderEmojiPicker, renderEmojiPicker,
replies, replyState,
skinTone, skinTone,
stories, stories,
views,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [currentStoryIndex, setCurrentStoryIndex] = useState(0); const [currentStoryIndex, setCurrentStoryIndex] = useState(0);
const [storyDuration, setStoryDuration] = useState<number | undefined>(); const [storyDuration, setStoryDuration] = useState<number | undefined>();
const visibleStory = stories[currentStoryIndex]; const visibleStory = stories[currentStoryIndex];
const { attachment, messageId, timestamp } = visibleStory; const { attachment, canReply, messageId, timestamp } = visibleStory;
const { const {
acceptedMessageRequest, acceptedMessageRequest,
avatarPath, avatarPath,
@ -240,6 +253,20 @@ export const StoryViewer = ({
}; };
}, [navigateStories]); }, [navigateStories]);
const isGroupStory = Boolean(group?.id);
useEffect(() => {
if (!isGroupStory) {
return;
}
loadStoryReplies(conversationId, messageId);
}, [conversationId, isGroupStory, loadStoryReplies, messageId]);
const replies =
replyState && replyState.messageId === messageId ? replyState.replies : [];
const viewCount = 0;
const replyCount = replies.length;
return ( return (
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}> <FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
<div className="StoryViewer"> <div className="StoryViewer">
@ -366,49 +393,51 @@ export const StoryViewer = ({
<div className="StoryViewer__actions"> <div className="StoryViewer__actions">
{isMe ? ( {isMe ? (
<> <>
{views && {viewCount &&
(views === 1 ? ( (viewCount === 1 ? (
<Intl <Intl
i18n={i18n} i18n={i18n}
id="MyStories__views--singular" id="MyStories__views--singular"
components={[<strong>{views}</strong>]} components={[<strong>{viewCount}</strong>]}
/> />
) : ( ) : (
<Intl <Intl
i18n={i18n} i18n={i18n}
id="MyStories__views--plural" id="MyStories__views--plural"
components={[<strong>{views}</strong>]} components={[<strong>{viewCount}</strong>]}
/> />
))} ))}
{views && replies && ' '} {viewCount && replyCount && ' '}
{replies && {replyCount &&
(replies === 1 ? ( (replyCount === 1 ? (
<Intl <Intl
i18n={i18n} i18n={i18n}
id="MyStories__replies--singular" id="MyStories__replies--singular"
components={[<strong>{replies}</strong>]} components={[<strong>{replyCount}</strong>]}
/> />
) : ( ) : (
<Intl <Intl
i18n={i18n} i18n={i18n}
id="MyStories__replies--plural" id="MyStories__replies--plural"
components={[<strong>{replies}</strong>]} components={[<strong>{replyCount}</strong>]}
/> />
))} ))}
</> </>
) : ( ) : (
<button canReply && (
className="StoryViewer__reply" <button
onClick={() => setHasReplyModal(true)} className="StoryViewer__reply"
tabIndex={0} onClick={() => setHasReplyModal(true)}
type="button" tabIndex={0}
> type="button"
{i18n('StoryViewer__reply')} >
</button> {i18n('StoryViewer__reply')}
</button>
)
)} )}
</div> </div>
</div> </div>
{hasReplyModal && ( {hasReplyModal && canReply && (
<StoryViewsNRepliesModal <StoryViewsNRepliesModal
authorTitle={title} authorTitle={title}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
@ -428,7 +457,7 @@ export const StoryViewer = ({
preferredReactionEmoji={preferredReactionEmoji} preferredReactionEmoji={preferredReactionEmoji}
recentEmojis={recentEmojis} recentEmojis={recentEmojis}
renderEmojiPicker={renderEmojiPicker} renderEmojiPicker={renderEmojiPicker}
replies={[]} replies={replies}
skinTone={skinTone} skinTone={skinTone}
storyPreviewAttachment={attachment} storyPreviewAttachment={attachment}
views={[]} views={[]}

View File

@ -12,6 +12,7 @@ import type { EmojiPickDataType } from './emoji/EmojiPicker';
import type { InputApi } from './CompositionInput'; import type { InputApi } from './CompositionInput';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker'; import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
import type { ReplyType } from '../types/Stories';
import { Avatar, AvatarSize } from './Avatar'; import { Avatar, AvatarSize } from './Avatar';
import { CompositionInput } from './CompositionInput'; import { CompositionInput } from './CompositionInput';
import { ContactName } from './conversation/ContactName'; import { ContactName } from './conversation/ContactName';
@ -23,26 +24,10 @@ import { Modal } from './Modal';
import { Quote } from './conversation/Quote'; import { Quote } from './conversation/Quote';
import { ReactionPicker } from './conversation/ReactionPicker'; import { ReactionPicker } from './conversation/ReactionPicker';
import { Tabs } from './Tabs'; import { Tabs } from './Tabs';
import { Theme } from '../util/theme';
import { ThemeType } from '../types/Util'; import { ThemeType } from '../types/Util';
import { getAvatarColor } from '../types/Colors'; import { getAvatarColor } from '../types/Colors';
type ReplyType = Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'isMe'
| 'name'
| 'profileName'
| 'sharedGroupNames'
| 'title'
> & {
body?: string;
contactNameColor?: ContactNameColorType;
reactionEmoji?: string;
timestamp: number;
};
type ViewType = Pick< type ViewType = Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
@ -223,7 +208,7 @@ export const StoryViewsNRepliesModal = ({
<div className="StoryViewsNRepliesModal__replies"> <div className="StoryViewsNRepliesModal__replies">
{replies.map(reply => {replies.map(reply =>
reply.reactionEmoji ? ( reply.reactionEmoji ? (
<div className="StoryViewsNRepliesModal__reaction"> <div className="StoryViewsNRepliesModal__reaction" key={reply.id}>
<div className="StoryViewsNRepliesModal__reaction--container"> <div className="StoryViewsNRepliesModal__reaction--container">
<Avatar <Avatar
acceptedMessageRequest={reply.acceptedMessageRequest} acceptedMessageRequest={reply.acceptedMessageRequest}
@ -257,7 +242,7 @@ export const StoryViewsNRepliesModal = ({
<Emojify text={reply.reactionEmoji} /> <Emojify text={reply.reactionEmoji} />
</div> </div>
) : ( ) : (
<div className="StoryViewsNRepliesModal__reply"> <div className="StoryViewsNRepliesModal__reply" key={reply.id}>
<Avatar <Avatar
acceptedMessageRequest={reply.acceptedMessageRequest} acceptedMessageRequest={reply.acceptedMessageRequest}
avatarPath={reply.avatarPath} avatarPath={reply.avatarPath}
@ -272,7 +257,13 @@ export const StoryViewsNRepliesModal = ({
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}
title={reply.title} title={reply.title}
/> />
<div className="StoryViewsNRepliesModal__message-bubble"> <div
className={classNames('StoryViewsNRepliesModal__message-bubble', {
'StoryViewsNRepliesModal__message-bubble--doe': Boolean(
reply.deletedForEveryone
),
})}
>
<div className="StoryViewsNRepliesModal__reply--title"> <div className="StoryViewsNRepliesModal__reply--title">
<ContactName <ContactName
contactNameColor={reply.contactNameColor} contactNameColor={reply.contactNameColor}
@ -280,7 +271,14 @@ export const StoryViewsNRepliesModal = ({
/> />
</div> </div>
<MessageBody i18n={i18n} text={String(reply.body)} /> <MessageBody
i18n={i18n}
text={
reply.deletedForEveryone
? i18n('message--deletedForEveryone')
: String(reply.body)
}
/>
<MessageTimestamp <MessageTimestamp
i18n={i18n} i18n={i18n}
@ -373,6 +371,7 @@ export const StoryViewsNRepliesModal = ({
})} })}
onClose={onClose} onClose={onClose}
useFocusTrap={!hasOnlyViewsElement} useFocusTrap={!hasOnlyViewsElement}
theme={Theme.Dark}
> >
{tabsElement || ( {tabsElement || (
<> <>

View File

@ -47,7 +47,7 @@ type State = {
export type QuotedAttachmentType = Pick< export type QuotedAttachmentType = Pick<
AttachmentType, AttachmentType,
'contentType' | 'fileName' | 'isVoiceMessage' | 'thumbnail' 'contentType' | 'fileName' | 'isVoiceMessage' | 'thumbnail' | 'textAttachment'
>; >;
function validateQuote(quote: Props): boolean { function validateQuote(quote: Props): boolean {
@ -221,10 +221,11 @@ export class Quote extends React.Component<Props, State> {
return null; return null;
} }
const { fileName, contentType } = attachment; const { fileName, contentType, textAttachment } = attachment;
const isGenericFile = const isGenericFile =
!GoogleChrome.isVideoTypeSupported(contentType) && !GoogleChrome.isVideoTypeSupported(contentType) &&
!GoogleChrome.isImageTypeSupported(contentType) && !GoogleChrome.isImageTypeSupported(contentType) &&
!textAttachment &&
!MIME.isAudio(contentType); !MIME.isAudio(contentType);
if (!isGenericFile) { if (!isGenericFile) {
@ -257,13 +258,18 @@ export class Quote extends React.Component<Props, State> {
return null; return null;
} }
const { contentType, thumbnail } = attachment; const { contentType, textAttachment, thumbnail } = attachment;
const url = getUrl(thumbnail); const url = getUrl(thumbnail);
if (isViewOnce) { if (isViewOnce) {
return this.renderIcon('view-once'); return this.renderIcon('view-once');
} }
// TODO DESKTOP-3433
if (textAttachment) {
return this.renderIcon('image');
}
if (GoogleChrome.isVideoTypeSupported(contentType)) { if (GoogleChrome.isVideoTypeSupported(contentType)) {
return url && !imageBroken return url && !imageBroken
? this.renderImage(url, 'play') ? this.renderImage(url, 'play')

View File

@ -3958,7 +3958,7 @@ export class ConversationModel extends window.Backbone
storyId?: string; storyId?: string;
timestamp?: number; timestamp?: number;
} = {} } = {}
): Promise<void> { ): Promise<MessageAttributesType | undefined> {
if (this.isGroupV1AndDisabled()) { if (this.isGroupV1AndDisabled()) {
return; return;
} }
@ -4143,6 +4143,8 @@ export class ConversationModel extends window.Backbone
} }
window.Signal.Data.updateConversation(this.attributes); window.Signal.Data.updateConversation(this.attributes);
return attributes;
} }
// Is this someone who is a contact, or are we sharing our profile with them? // Is this someone who is a contact, or are we sharing our profile with them?

View File

@ -43,10 +43,13 @@ export function getStoryDataFromMessageAttributes(
selectedReaction, selectedReaction,
...pick(message, [ ...pick(message, [
'conversationId', 'conversationId',
'deletedForEveryone',
'readStatus', 'readStatus',
'sendStateByConversationId',
'source', 'source',
'sourceUuid', 'sourceUuid',
'timestamp', 'timestamp',
'type',
]), ]),
}; };
} }

View File

@ -1238,7 +1238,7 @@ async function getOlderMessagesByConversation(
messageId?: string; messageId?: string;
receivedAt?: number; receivedAt?: number;
sentAt?: number; sentAt?: number;
storyId?: UUIDStringType; storyId?: string;
} }
) { ) {
const messages = await channels.getOlderMessagesByConversation( const messages = await channels.getOlderMessagesByConversation(

View File

@ -617,7 +617,7 @@ export type ServerInterface = DataInterface & {
messageId?: string; messageId?: string;
receivedAt?: number; receivedAt?: number;
sentAt?: number; sentAt?: number;
storyId?: UUIDStringType; storyId?: string;
} }
) => Promise<Array<MessageTypeUnhydrated>>; ) => Promise<Array<MessageTypeUnhydrated>>;
getNewerMessagesByConversation: ( getNewerMessagesByConversation: (
@ -687,7 +687,7 @@ export type ClientInterface = DataInterface & {
messageId?: string; messageId?: string;
receivedAt?: number; receivedAt?: number;
sentAt?: number; sentAt?: number;
storyId?: UUIDStringType; storyId?: string;
} }
) => Promise<Array<MessageAttributesType>>; ) => Promise<Array<MessageAttributesType>>;
getNewerMessagesByConversation: ( getNewerMessagesByConversation: (

View File

@ -2314,7 +2314,7 @@ async function getOlderMessagesByConversation(
messageId?: string; messageId?: string;
receivedAt?: number; receivedAt?: number;
sentAt?: number; sentAt?: number;
storyId?: UUIDStringType; storyId?: string;
} }
): Promise<Array<MessageTypeUnhydrated>> { ): Promise<Array<MessageTypeUnhydrated>> {
return getOlderMessagesByConversationSync(conversationId, options); return getOlderMessagesByConversationSync(conversationId, options);
@ -2332,7 +2332,7 @@ function getOlderMessagesByConversationSync(
messageId?: string; messageId?: string;
receivedAt?: number; receivedAt?: number;
sentAt?: number; sentAt?: number;
storyId?: UUIDStringType; storyId?: string;
} = {} } = {}
): Array<MessageTypeUnhydrated> { ): Array<MessageTypeUnhydrated> {
const db = getInstance(); const db = getInstance();

View File

@ -6,7 +6,10 @@ import { pick } from 'lodash';
import type { AttachmentType } from '../../types/Attachment'; import type { AttachmentType } from '../../types/Attachment';
import type { BodyRangeType } from '../../types/Util'; import type { BodyRangeType } from '../../types/Util';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import type { MessageDeletedActionType } from './conversations'; import type {
MessageChangedActionType,
MessageDeletedActionType,
} from './conversations';
import type { NoopActionType } from './noop'; import type { NoopActionType } from './noop';
import type { StateType as RootStateType } from '../reducer'; import type { StateType as RootStateType } from '../reducer';
import type { StoryViewType } from '../../components/StoryListItem'; import type { StoryViewType } from '../../components/StoryListItem';
@ -33,23 +36,44 @@ export type StoryDataType = {
selectedReaction?: string; selectedReaction?: string;
} & Pick< } & Pick<
MessageAttributesType, MessageAttributesType,
'conversationId' | 'readStatus' | 'source' | 'sourceUuid' | 'timestamp' | 'conversationId'
| 'deletedForEveryone'
| 'readStatus'
| 'sendStateByConversationId'
| 'source'
| 'sourceUuid'
| 'timestamp'
| 'type'
>; >;
// State // State
export type StoriesStateType = { export type StoriesStateType = {
readonly isShowingStoriesView: boolean; readonly isShowingStoriesView: boolean;
readonly replyState?: {
messageId: string;
replies: Array<MessageAttributesType>;
};
readonly stories: Array<StoryDataType>; readonly stories: Array<StoryDataType>;
}; };
// Actions // Actions
const LOAD_STORY_REPLIES = 'stories/LOAD_STORY_REPLIES';
const MARK_STORY_READ = 'stories/MARK_STORY_READ'; const MARK_STORY_READ = 'stories/MARK_STORY_READ';
const REACT_TO_STORY = 'stories/REACT_TO_STORY'; const REACT_TO_STORY = 'stories/REACT_TO_STORY';
const REPLY_TO_STORY = 'stories/REPLY_TO_STORY';
const STORY_CHANGED = 'stories/STORY_CHANGED'; const STORY_CHANGED = 'stories/STORY_CHANGED';
const TOGGLE_VIEW = 'stories/TOGGLE_VIEW'; const TOGGLE_VIEW = 'stories/TOGGLE_VIEW';
type LoadStoryRepliesActionType = {
type: typeof LOAD_STORY_REPLIES;
payload: {
messageId: string;
replies: Array<MessageAttributesType>;
};
};
type MarkStoryReadActionType = { type MarkStoryReadActionType = {
type: typeof MARK_STORY_READ; type: typeof MARK_STORY_READ;
payload: string; payload: string;
@ -63,6 +87,11 @@ type ReactToStoryActionType = {
}; };
}; };
type ReplyToStoryActionType = {
type: typeof REPLY_TO_STORY;
payload: MessageAttributesType;
};
type StoryChangedActionType = { type StoryChangedActionType = {
type: typeof STORY_CHANGED; type: typeof STORY_CHANGED;
payload: StoryDataType; payload: StoryDataType;
@ -73,15 +102,19 @@ type ToggleViewActionType = {
}; };
export type StoriesActionType = export type StoriesActionType =
| LoadStoryRepliesActionType
| MarkStoryReadActionType | MarkStoryReadActionType
| MessageChangedActionType
| MessageDeletedActionType | MessageDeletedActionType
| ReactToStoryActionType | ReactToStoryActionType
| ReplyToStoryActionType
| StoryChangedActionType | StoryChangedActionType
| ToggleViewActionType; | ToggleViewActionType;
// Action Creators // Action Creators
export const actions = { export const actions = {
loadStoryReplies,
markStoryRead, markStoryRead,
queueStoryDownload, queueStoryDownload,
reactToStory, reactToStory,
@ -92,6 +125,26 @@ export const actions = {
export const useStoriesActions = (): typeof actions => useBoundActions(actions); export const useStoriesActions = (): typeof actions => useBoundActions(actions);
function loadStoryReplies(
conversationId: string,
messageId: string
): ThunkAction<void, RootStateType, unknown, LoadStoryRepliesActionType> {
return async dispatch => {
const replies = await dataInterface.getOlderMessagesByConversation(
conversationId,
{ limit: 9000, storyId: messageId }
);
dispatch({
type: LOAD_STORY_REPLIES,
payload: {
messageId,
replies,
},
});
};
}
function markStoryRead( function markStoryRead(
messageId: string messageId: string
): ThunkAction<void, RootStateType, unknown, MarkStoryReadActionType> { ): ThunkAction<void, RootStateType, unknown, MarkStoryReadActionType> {
@ -225,11 +278,16 @@ function replyToStory(
mentions: Array<BodyRangeType>, mentions: Array<BodyRangeType>,
timestamp: number, timestamp: number,
story: StoryViewType story: StoryViewType
): NoopActionType { ): ThunkAction<void, RootStateType, unknown, ReplyToStoryActionType> {
const conversation = window.ConversationController.get(conversationId); return async dispatch => {
const conversation = window.ConversationController.get(conversationId);
if (conversation) { if (!conversation) {
conversation.enqueueMessageForSend( log.error('replyToStory: conversation does not exist', conversationId);
return;
}
const messageAttributes = await conversation.enqueueMessageForSend(
{ {
body: messageBody, body: messageBody,
attachments: [], attachments: [],
@ -240,11 +298,13 @@ function replyToStory(
timestamp, timestamp,
} }
); );
}
return { if (messageAttributes) {
type: 'NOOP', dispatch({
payload: null, type: REPLY_TO_STORY,
payload: messageAttributes,
});
}
}; };
} }
@ -285,11 +345,17 @@ export function reducer(
} }
if (action.type === 'MESSAGE_DELETED') { if (action.type === 'MESSAGE_DELETED') {
const nextStories = state.stories.filter(
story => story.messageId !== action.payload.id
);
if (nextStories.length === state.stories.length) {
return state;
}
return { return {
...state, ...state,
stories: state.stories.filter( stories: nextStories,
story => story.messageId !== action.payload.id
),
}; };
} }
@ -297,12 +363,15 @@ export function reducer(
const newStory = pick(action.payload, [ const newStory = pick(action.payload, [
'attachment', 'attachment',
'conversationId', 'conversationId',
'deletedForEveryone',
'messageId', 'messageId',
'readStatus', 'readStatus',
'selectedReaction', 'selectedReaction',
'sendStateByConversationId',
'source', 'source',
'sourceUuid', 'sourceUuid',
'timestamp', 'timestamp',
'type',
]); ]);
// Stories don't really need to change except for when we don't have the // Stories don't really need to change except for when we don't have the
@ -326,6 +395,10 @@ export function reducer(
existingStory => existingStory.messageId === newStory.messageId existingStory => existingStory.messageId === newStory.messageId
); );
if (storyIndex < 0) {
return state;
}
return { return {
...state, ...state,
stories: replaceIndex(state.stories, storyIndex, newStory), stories: replaceIndex(state.stories, storyIndex, newStory),
@ -374,5 +447,63 @@ export function reducer(
}; };
} }
if (action.type === LOAD_STORY_REPLIES) {
return {
...state,
replyState: action.payload,
};
}
// For live updating of the story replies
if (
action.type === 'MESSAGE_CHANGED' &&
state.replyState &&
state.replyState.messageId === action.payload.data.storyId
) {
const { replyState } = state;
const messageIndex = replyState.replies.findIndex(
reply => reply.id === action.payload.id
);
// New message
if (messageIndex < 0) {
return {
...state,
replyState: {
messageId: replyState.messageId,
replies: [...replyState.replies, action.payload.data],
},
};
}
// Changed message, also handles DOE
return {
...state,
replyState: {
messageId: replyState.messageId,
replies: replaceIndex(
replyState.replies,
messageIndex,
action.payload.data
),
},
};
}
if (action.type === REPLY_TO_STORY) {
const { replyState } = state;
if (!replyState) {
return state;
}
return {
...state,
replyState: {
messageId: replyState.messageId,
replies: [...replyState.replies, action.payload],
},
};
}
return state; return state;
} }

View File

@ -4,14 +4,22 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { pick } from 'lodash'; import { pick } from 'lodash';
import type { GetConversationByIdType } from './conversations';
import type { import type {
ConversationStoryType, ConversationStoryType,
StoryViewType, StoryViewType,
} from '../../components/StoryListItem'; } from '../../components/StoryListItem';
import type { ReplyStateType } from '../../types/Stories';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { StoriesStateType } from '../ducks/stories'; import type { StoryDataType, StoriesStateType } from '../ducks/stories';
import { ReadStatus } from '../../messages/MessageReadStatus'; import { ReadStatus } from '../../messages/MessageReadStatus';
import { getConversationSelector } from './conversations'; import { canReply } from './message';
import {
getContactNameColorSelector,
getConversationSelector,
getMe,
} from './conversations';
import { getUserConversationId } from './user';
export const getStoriesState = (state: StateType): StoriesStateType => export const getStoriesState = (state: StateType): StoriesStateType =>
state.stories; state.stories;
@ -47,12 +55,148 @@ function sortByRecencyAndUnread(
return storyA.timestamp > storyB.timestamp ? -1 : 1; return storyA.timestamp > storyB.timestamp ? -1 : 1;
} }
function getConversationStory(
conversationSelector: GetConversationByIdType,
story: StoryDataType,
ourConversationId?: string
): ConversationStoryType {
const sender = pick(conversationSelector(story.sourceUuid || story.source), [
'acceptedMessageRequest',
'avatarPath',
'color',
'firstName',
'hideStory',
'id',
'isMe',
'name',
'profileName',
'sharedGroupNames',
'title',
]);
const conversation = pick(conversationSelector(story.conversationId), [
'acceptedMessageRequest',
'avatarPath',
'color',
'id',
'name',
'profileName',
'sharedGroupNames',
'title',
]);
const { attachment, timestamp } = pick(story, ['attachment', 'timestamp']);
const storyView: StoryViewType = {
attachment,
canReply: canReply(story, ourConversationId, conversationSelector),
isUnread: story.readStatus === ReadStatus.Unread,
messageId: story.messageId,
selectedReaction: story.selectedReaction,
sender,
timestamp,
};
return {
conversationId: conversation.id,
group: conversation.id !== sender.id ? conversation : undefined,
isHidden: Boolean(sender.hideStory),
stories: [storyView],
};
}
export type GetStoriesByConversationIdType = (
conversationId: string
) => ConversationStoryType;
export const getStoriesSelector = createSelector(
getConversationSelector,
getUserConversationId,
getStoriesState,
(
conversationSelector,
ourConversationId,
{ stories }: Readonly<StoriesStateType>
): GetStoriesByConversationIdType => {
return conversationId => {
const conversationStoryAcc: ConversationStoryType = {
conversationId,
stories: [],
};
return stories.reduce((acc, story) => {
if (story.conversationId !== conversationId) {
return acc;
}
const conversationStory = getConversationStory(
conversationSelector,
story,
ourConversationId
);
return {
...acc,
...conversationStory,
stories: [...acc.stories, ...conversationStory.stories],
};
}, conversationStoryAcc);
};
}
);
export const getStoryReplies = createSelector(
getConversationSelector,
getContactNameColorSelector,
getMe,
getStoriesState,
(
conversationSelector,
contactNameColorSelector,
me,
{ replyState }: Readonly<StoriesStateType>
): ReplyStateType | undefined => {
if (!replyState) {
return;
}
return {
messageId: replyState.messageId,
replies: replyState.replies.map(reply => {
const conversation =
reply.type === 'outgoing'
? me
: conversationSelector(reply.sourceUuid || reply.source);
return {
...pick(conversation, [
'acceptedMessageRequest',
'avatarPath',
'color',
'isMe',
'name',
'profileName',
'sharedGroupNames',
'title',
]),
...pick(reply, ['body', 'deletedForEveryone', 'id', 'timestamp']),
contactNameColor: contactNameColorSelector(
reply.conversationId,
conversation.id
),
};
}),
};
}
);
export const getStories = createSelector( export const getStories = createSelector(
getConversationSelector, getConversationSelector,
getUserConversationId,
getStoriesState, getStoriesState,
shouldShowStoriesView, shouldShowStoriesView,
( (
conversationSelector, conversationSelector,
ourConversationId,
{ stories }: Readonly<StoriesStateType>, { stories }: Readonly<StoriesStateType>,
isShowingStoriesView isShowingStoriesView
): { ): {
@ -70,58 +214,30 @@ export const getStories = createSelector(
const hiddenStoriesById = new Map<string, ConversationStoryType>(); const hiddenStoriesById = new Map<string, ConversationStoryType>();
stories.forEach(story => { stories.forEach(story => {
const sender = pick( const conversationStory = getConversationStory(
conversationSelector(story.sourceUuid || story.source), conversationSelector,
[ story,
'acceptedMessageRequest', ourConversationId
'avatarPath',
'color',
'firstName',
'hideStory',
'id',
'isMe',
'name',
'profileName',
'sharedGroupNames',
'title',
]
); );
const conversation = pick(conversationSelector(story.conversationId), [
'id',
'title',
]);
const { attachment, timestamp } = pick(story, [
'attachment',
'timestamp',
]);
let storiesMap: Map<string, ConversationStoryType>; let storiesMap: Map<string, ConversationStoryType>;
if (sender.hideStory) { if (conversationStory.isHidden) {
storiesMap = hiddenStoriesById; storiesMap = hiddenStoriesById;
} else { } else {
storiesMap = storiesById; storiesMap = storiesById;
} }
const storyView: StoryViewType = { const existingConversationStory = storiesMap.get(
attachment, conversationStory.conversationId
isUnread: story.readStatus === ReadStatus.Unread, ) || { stories: [] };
messageId: story.messageId,
selectedReaction: story.selectedReaction,
sender,
timestamp,
};
const conversationStory = storiesMap.get(conversation.id) || { storiesMap.set(conversationStory.conversationId, {
conversationId: conversation.id, ...existingConversationStory,
group: conversation.id !== sender.id ? conversation : undefined,
isHidden: Boolean(sender.hideStory),
stories: [],
};
storiesMap.set(conversation.id, {
...conversationStory, ...conversationStory,
stories: [...conversationStory.stories, storyView], stories: [
...existingConversationStory.stories,
...conversationStory.stories,
],
}); });
}); });

View File

@ -20,7 +20,6 @@ function renderStoryViewer({
onClose, onClose,
onNextUserStories, onNextUserStories,
onPrevUserStories, onPrevUserStories,
stories,
}: SmartStoryViewerPropsType): JSX.Element { }: SmartStoryViewerPropsType): JSX.Element {
return ( return (
<SmartStoryViewer <SmartStoryViewer
@ -28,7 +27,6 @@ function renderStoryViewer({
onClose={onClose} onClose={onClose}
onNextUserStories={onNextUserStories} onNextUserStories={onNextUserStories}
onPrevUserStories={onPrevUserStories} onPrevUserStories={onPrevUserStories}
stories={stories}
/> />
); );
} }

View File

@ -4,15 +4,16 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { GetStoriesByConversationIdType } from '../selectors/stories';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { StoryViewType } from '../../components/StoryListItem';
import { StoryViewer } from '../../components/StoryViewer'; import { StoryViewer } from '../../components/StoryViewer';
import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong'; import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong';
import { import {
getEmojiSkinTone, getEmojiSkinTone,
getPreferredReactionEmoji, getPreferredReactionEmoji,
} from '../selectors/items'; } from '../selectors/items';
import { getStoriesSelector, getStoryReplies } from '../selectors/stories';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { getPreferredBadgeSelector } from '../selectors/badges'; import { getPreferredBadgeSelector } from '../selectors/badges';
import { renderEmojiPicker } from './renderEmojiPicker'; import { renderEmojiPicker } from './renderEmojiPicker';
@ -27,7 +28,6 @@ export type PropsType = {
onClose: () => unknown; onClose: () => unknown;
onNextUserStories: () => unknown; onNextUserStories: () => unknown;
onPrevUserStories: () => unknown; onPrevUserStories: () => unknown;
stories: Array<StoryViewType>;
}; };
export function SmartStoryViewer({ export function SmartStoryViewer({
@ -35,7 +35,6 @@ export function SmartStoryViewer({
onClose, onClose,
onNextUserStories, onNextUserStories,
onPrevUserStories, onPrevUserStories,
stories,
}: PropsType): JSX.Element | null { }: PropsType): JSX.Element | null {
const storiesActions = useStoriesActions(); const storiesActions = useStoriesActions();
const { onSetSkinTone } = useItemsActions(); const { onSetSkinTone } = useItemsActions();
@ -47,12 +46,22 @@ export function SmartStoryViewer({
getPreferredReactionEmoji getPreferredReactionEmoji
); );
const getStoriesByConversationId = useSelector<
StateType,
GetStoriesByConversationIdType
>(getStoriesSelector);
const { group, stories } = getStoriesByConversationId(conversationId);
const recentEmojis = useRecentEmojis(); const recentEmojis = useRecentEmojis();
const skinTone = useSelector<StateType, number>(getEmojiSkinTone); const skinTone = useSelector<StateType, number>(getEmojiSkinTone);
const replyState = useSelector(getStoryReplies);
return ( return (
<StoryViewer <StoryViewer
conversationId={conversationId}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
group={group}
i18n={i18n} i18n={i18n}
onClose={onClose} onClose={onClose}
onNextUserStories={onNextUserStories} onNextUserStories={onNextUserStories}
@ -76,6 +85,7 @@ export function SmartStoryViewer({
preferredReactionEmoji={preferredReactionEmoji} preferredReactionEmoji={preferredReactionEmoji}
recentEmojis={recentEmojis} recentEmojis={recentEmojis}
renderEmojiPicker={renderEmojiPicker} renderEmojiPicker={renderEmojiPicker}
replyState={replyState}
stories={stories} stories={stories}
skinTone={skinTone} skinTone={skinTone}
{...storiesActions} {...storiesActions}

View File

@ -60,7 +60,11 @@ import type { UnprocessedType } from '../textsecure.d';
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups'; import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
import createTaskWithTimeout from './TaskWithTimeout'; import createTaskWithTimeout from './TaskWithTimeout';
import { processAttachment, processDataMessage } from './processDataMessage'; import {
processAttachment,
processDataMessage,
processGroupV2Context,
} from './processDataMessage';
import { processSyncMessage } from './processSyncMessage'; import { processSyncMessage } from './processSyncMessage';
import type { EventHandler } from './EventTarget'; import type { EventHandler } from './EventTarget';
import EventTarget from './EventTarget'; import EventTarget from './EventTarget';
@ -1813,6 +1817,17 @@ export default class MessageReceiver
}); });
} }
const groupV2 = msg.group ? processGroupV2Context(msg.group) : undefined;
if (groupV2 && this.isGroupBlocked(groupV2.id)) {
log.warn(
`MessageReceiver.handleStoryMessage: envelope ${this.getEnvelopeId(
envelope
)} ignored; destined for blocked group`
);
this.removeFromCache(envelope);
return;
}
const expireTimer = Math.min( const expireTimer = Math.min(
Math.floor( Math.floor(
(envelope.serverTimestamp + durations.DAY - Date.now()) / 1000 (envelope.serverTimestamp + durations.DAY - Date.now()) / 1000
@ -1844,6 +1859,7 @@ export default class MessageReceiver
attachments, attachments,
expireTimer, expireTimer,
flags: 0, flags: 0,
groupV2,
isStory: true, isStory: true,
isViewOnce: false, isViewOnce: false,
timestamp: envelope.timestamp, timestamp: envelope.timestamp,

29
ts/types/Stories.ts Normal file
View File

@ -0,0 +1,29 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ContactNameColorType } from './Colors';
import type { ConversationType } from '../state/ducks/conversations';
export type ReplyType = Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'isMe'
| 'name'
| 'profileName'
| 'sharedGroupNames'
| 'title'
> & {
body?: string;
contactNameColor?: ContactNameColorType;
deletedForEveryone?: boolean;
id: string;
reactionEmoji?: string;
timestamp: number;
};
export type ReplyStateType = {
messageId: string;
replies: Array<ReplyType>;
};