Render group stories
This commit is contained in:
parent
14ab7b9e0d
commit
e3d537cbd3
24 changed files with 527 additions and 163 deletions
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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={[]}
|
||||||
|
|
|
@ -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 || (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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',
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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: (
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
29
ts/types/Stories.ts
Normal 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>;
|
||||||
|
};
|
Loading…
Reference in a new issue