Fixes story viewing behavior
This commit is contained in:
parent
c4b6eebcd6
commit
3e644f45cf
26 changed files with 960 additions and 939 deletions
|
@ -17,6 +17,7 @@ import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||||
import type { MenuOptionsType, MenuActionType } from '../types/menu';
|
import type { MenuOptionsType, MenuActionType } from '../types/menu';
|
||||||
import { TitleBarContainer } from './TitleBarContainer';
|
import { TitleBarContainer } from './TitleBarContainer';
|
||||||
import type { ExecuteMenuRoleType } from './TitleBarContainer';
|
import type { ExecuteMenuRoleType } from './TitleBarContainer';
|
||||||
|
import type { SelectedStoryDataType } from '../state/ducks/stories';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
appView: AppViewType;
|
appView: AppViewType;
|
||||||
|
@ -27,6 +28,8 @@ type PropsType = {
|
||||||
renderGlobalModalContainer: () => JSX.Element;
|
renderGlobalModalContainer: () => JSX.Element;
|
||||||
isShowingStoriesView: boolean;
|
isShowingStoriesView: boolean;
|
||||||
renderStories: () => JSX.Element;
|
renderStories: () => JSX.Element;
|
||||||
|
selectedStoryData?: SelectedStoryDataType;
|
||||||
|
renderStoryViewer: () => JSX.Element;
|
||||||
requestVerification: (
|
requestVerification: (
|
||||||
type: 'sms' | 'voice',
|
type: 'sms' | 'voice',
|
||||||
number: string,
|
number: string,
|
||||||
|
@ -69,9 +72,11 @@ export const App = ({
|
||||||
renderLeftPane,
|
renderLeftPane,
|
||||||
renderSafetyNumber,
|
renderSafetyNumber,
|
||||||
renderStories,
|
renderStories,
|
||||||
|
renderStoryViewer,
|
||||||
requestVerification,
|
requestVerification,
|
||||||
selectedConversationId,
|
selectedConversationId,
|
||||||
selectedMessage,
|
selectedMessage,
|
||||||
|
selectedStoryData,
|
||||||
showConversation,
|
showConversation,
|
||||||
showWhatsNewModal,
|
showWhatsNewModal,
|
||||||
theme,
|
theme,
|
||||||
|
@ -169,6 +174,7 @@ export const App = ({
|
||||||
{renderGlobalModalContainer()}
|
{renderGlobalModalContainer()}
|
||||||
{renderCallManager()}
|
{renderCallManager()}
|
||||||
{isShowingStoriesView && renderStories()}
|
{isShowingStoriesView && renderStories()}
|
||||||
|
{selectedStoryData && renderStoryViewer()}
|
||||||
{contents}
|
{contents}
|
||||||
</div>
|
</div>
|
||||||
</TitleBarContainer>
|
</TitleBarContainer>
|
||||||
|
|
|
@ -47,6 +47,7 @@ export default {
|
||||||
renderStoryViewer: {
|
renderStoryViewer: {
|
||||||
action: true,
|
action: true,
|
||||||
},
|
},
|
||||||
|
viewStory: { action: true },
|
||||||
},
|
},
|
||||||
} as Meta;
|
} as Meta;
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import type { MyStoryType, StoryViewType } from '../types/Stories';
|
import type { MyStoryType, StoryViewType } from '../types/Stories';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import type { PropsType as SmartStoryViewerPropsType } from '../state/smart/StoryViewer';
|
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import { ContextMenu } from './ContextMenu';
|
import { ContextMenu } from './ContextMenu';
|
||||||
import { MY_STORIES_ID } from '../types/Stories';
|
import { MY_STORIES_ID, StoryViewModeType } from '../types/Stories';
|
||||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
|
@ -19,9 +19,8 @@ export type PropsType = {
|
||||||
onDelete: (story: StoryViewType) => unknown;
|
onDelete: (story: StoryViewType) => unknown;
|
||||||
onForward: (storyId: string) => unknown;
|
onForward: (storyId: string) => unknown;
|
||||||
onSave: (story: StoryViewType) => unknown;
|
onSave: (story: StoryViewType) => unknown;
|
||||||
ourConversationId: string;
|
|
||||||
queueStoryDownload: (storyId: string) => unknown;
|
queueStoryDownload: (storyId: string) => unknown;
|
||||||
renderStoryViewer: (props: SmartStoryViewerPropsType) => JSX.Element;
|
viewStory: ViewStoryActionCreatorType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MyStories = ({
|
export const MyStories = ({
|
||||||
|
@ -31,16 +30,13 @@ export const MyStories = ({
|
||||||
onDelete,
|
onDelete,
|
||||||
onForward,
|
onForward,
|
||||||
onSave,
|
onSave,
|
||||||
ourConversationId,
|
|
||||||
queueStoryDownload,
|
queueStoryDownload,
|
||||||
renderStoryViewer,
|
viewStory,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const [confirmDeleteStory, setConfirmDeleteStory] = useState<
|
const [confirmDeleteStory, setConfirmDeleteStory] = useState<
|
||||||
StoryViewType | undefined
|
StoryViewType | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const [storyToView, setStoryToView] = useState<StoryViewType | undefined>();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{confirmDeleteStory && (
|
{confirmDeleteStory && (
|
||||||
|
@ -58,12 +54,6 @@ export const MyStories = ({
|
||||||
{i18n('MyStories__delete')}
|
{i18n('MyStories__delete')}
|
||||||
</ConfirmationDialog>
|
</ConfirmationDialog>
|
||||||
)}
|
)}
|
||||||
{storyToView &&
|
|
||||||
renderStoryViewer({
|
|
||||||
conversationId: ourConversationId,
|
|
||||||
onClose: () => setStoryToView(undefined),
|
|
||||||
storyToView,
|
|
||||||
})}
|
|
||||||
<div className="Stories__pane__header Stories__pane__header--centered">
|
<div className="Stories__pane__header Stories__pane__header--centered">
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('back')}
|
aria-label={i18n('back')}
|
||||||
|
@ -89,7 +79,9 @@ export const MyStories = ({
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('MyStories__story')}
|
aria-label={i18n('MyStories__story')}
|
||||||
className="MyStories__story__preview"
|
className="MyStories__story__preview"
|
||||||
onClick={() => setStoryToView(story)}
|
onClick={() =>
|
||||||
|
viewStory(story.messageId, StoryViewModeType.Single)
|
||||||
|
}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<StoryImage
|
<StoryImage
|
||||||
|
|
|
@ -28,6 +28,9 @@ export default {
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultValue: i18n,
|
defaultValue: i18n,
|
||||||
},
|
},
|
||||||
|
me: {
|
||||||
|
defaultValue: getDefaultConversation(),
|
||||||
|
},
|
||||||
myStories: {
|
myStories: {
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
},
|
},
|
||||||
|
@ -48,6 +51,8 @@ export default {
|
||||||
},
|
},
|
||||||
toggleHideStories: { action: true },
|
toggleHideStories: { action: true },
|
||||||
toggleStoriesView: { action: true },
|
toggleStoriesView: { action: true },
|
||||||
|
viewUserStories: { action: true },
|
||||||
|
viewStory: { action: true },
|
||||||
},
|
},
|
||||||
} as Meta;
|
} as Meta;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type {
|
import type {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -15,8 +15,7 @@ import type {
|
||||||
} from '../types/Stories';
|
} from '../types/Stories';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import type { PropsType as SmartStoryCreatorPropsType } from '../state/smart/StoryCreator';
|
import type { PropsType as SmartStoryCreatorPropsType } from '../state/smart/StoryCreator';
|
||||||
import type { PropsType as SmartStoryViewerPropsType } from '../state/smart/StoryViewer';
|
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||||
import * as log from '../logging/log';
|
|
||||||
import { MyStories } from './MyStories';
|
import { MyStories } from './MyStories';
|
||||||
import { StoriesPane } from './StoriesPane';
|
import { StoriesPane } from './StoriesPane';
|
||||||
import { Theme, themeClassName } from '../util/theme';
|
import { Theme, themeClassName } from '../util/theme';
|
||||||
|
@ -30,15 +29,15 @@ export type PropsType = {
|
||||||
myStories: Array<MyStoryType>;
|
myStories: Array<MyStoryType>;
|
||||||
onForwardStory: (storyId: string) => unknown;
|
onForwardStory: (storyId: string) => unknown;
|
||||||
onSaveStory: (story: StoryViewType) => unknown;
|
onSaveStory: (story: StoryViewType) => unknown;
|
||||||
ourConversationId: string;
|
|
||||||
preferredWidthFromStorage: number;
|
preferredWidthFromStorage: number;
|
||||||
queueStoryDownload: (storyId: string) => unknown;
|
queueStoryDownload: (storyId: string) => unknown;
|
||||||
renderStoryCreator: (props: SmartStoryCreatorPropsType) => JSX.Element;
|
renderStoryCreator: (props: SmartStoryCreatorPropsType) => JSX.Element;
|
||||||
renderStoryViewer: (props: SmartStoryViewerPropsType) => JSX.Element;
|
|
||||||
showConversation: ShowConversationType;
|
showConversation: ShowConversationType;
|
||||||
stories: Array<ConversationStoryType>;
|
stories: Array<ConversationStoryType>;
|
||||||
toggleHideStories: (conversationId: string) => unknown;
|
toggleHideStories: (conversationId: string) => unknown;
|
||||||
toggleStoriesView: () => unknown;
|
toggleStoriesView: () => unknown;
|
||||||
|
viewUserStories: (conversationId: string) => unknown;
|
||||||
|
viewStory: ViewStoryActionCreatorType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Stories = ({
|
export const Stories = ({
|
||||||
|
@ -49,76 +48,20 @@ export const Stories = ({
|
||||||
myStories,
|
myStories,
|
||||||
onForwardStory,
|
onForwardStory,
|
||||||
onSaveStory,
|
onSaveStory,
|
||||||
ourConversationId,
|
|
||||||
preferredWidthFromStorage,
|
preferredWidthFromStorage,
|
||||||
queueStoryDownload,
|
queueStoryDownload,
|
||||||
renderStoryCreator,
|
renderStoryCreator,
|
||||||
renderStoryViewer,
|
|
||||||
showConversation,
|
showConversation,
|
||||||
stories,
|
stories,
|
||||||
toggleHideStories,
|
toggleHideStories,
|
||||||
toggleStoriesView,
|
toggleStoriesView,
|
||||||
|
viewUserStories,
|
||||||
|
viewStory,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const [conversationIdToView, setConversationIdToView] = useState<
|
|
||||||
undefined | string
|
|
||||||
>();
|
|
||||||
|
|
||||||
const width = getWidthFromPreferredWidth(preferredWidthFromStorage, {
|
const width = getWidthFromPreferredWidth(preferredWidthFromStorage, {
|
||||||
requiresFullWidth: true,
|
requiresFullWidth: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onNextUserStories = useCallback(() => {
|
|
||||||
// First find the next unread story if there are any
|
|
||||||
const nextUnreadIndex = stories.findIndex(conversationStory =>
|
|
||||||
conversationStory.stories.some(story => story.isUnread)
|
|
||||||
);
|
|
||||||
|
|
||||||
log.info('stories.onNextUserStories', { nextUnreadIndex });
|
|
||||||
|
|
||||||
if (nextUnreadIndex >= 0) {
|
|
||||||
const nextStory = stories[nextUnreadIndex];
|
|
||||||
setConversationIdToView(nextStory.conversationId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not then play the next available story
|
|
||||||
const storyIndex = stories.findIndex(
|
|
||||||
x => x.conversationId === conversationIdToView
|
|
||||||
);
|
|
||||||
|
|
||||||
log.info('stories.onNextUserStories', {
|
|
||||||
storyIndex,
|
|
||||||
length: stories.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If we've reached the end, close the viewer
|
|
||||||
if (storyIndex >= stories.length - 1 || storyIndex === -1) {
|
|
||||||
setConversationIdToView(undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextStory = stories[storyIndex + 1];
|
|
||||||
setConversationIdToView(nextStory.conversationId);
|
|
||||||
}, [conversationIdToView, stories]);
|
|
||||||
|
|
||||||
const onPrevUserStories = useCallback(() => {
|
|
||||||
const storyIndex = stories.findIndex(
|
|
||||||
x => x.conversationId === conversationIdToView
|
|
||||||
);
|
|
||||||
|
|
||||||
log.info('stories.onPrevUserStories', {
|
|
||||||
storyIndex,
|
|
||||||
length: stories.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (storyIndex <= 0) {
|
|
||||||
// Restart playback on the story if it's the oldest
|
|
||||||
setConversationIdToView(conversationIdToView);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const prevStory = stories[storyIndex - 1];
|
|
||||||
setConversationIdToView(prevStory.conversationId);
|
|
||||||
}, [conversationIdToView, stories]);
|
|
||||||
|
|
||||||
const [isShowingStoryCreator, setIsShowingStoryCreator] = useState(false);
|
const [isShowingStoryCreator, setIsShowingStoryCreator] = useState(false);
|
||||||
const [isMyStories, setIsMyStories] = useState(false);
|
const [isMyStories, setIsMyStories] = useState(false);
|
||||||
|
|
||||||
|
@ -128,13 +71,6 @@ export const Stories = ({
|
||||||
renderStoryCreator({
|
renderStoryCreator({
|
||||||
onClose: () => setIsShowingStoryCreator(false),
|
onClose: () => setIsShowingStoryCreator(false),
|
||||||
})}
|
})}
|
||||||
{conversationIdToView &&
|
|
||||||
renderStoryViewer({
|
|
||||||
conversationId: conversationIdToView,
|
|
||||||
onClose: () => setConversationIdToView(undefined),
|
|
||||||
onNextUserStories,
|
|
||||||
onPrevUserStories,
|
|
||||||
})}
|
|
||||||
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
|
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
|
||||||
<div className="Stories__pane" style={{ width }}>
|
<div className="Stories__pane" style={{ width }}>
|
||||||
{isMyStories && myStories.length ? (
|
{isMyStories && myStories.length ? (
|
||||||
|
@ -145,9 +81,8 @@ export const Stories = ({
|
||||||
onDelete={deleteStoryForEveryone}
|
onDelete={deleteStoryForEveryone}
|
||||||
onForward={onForwardStory}
|
onForward={onForwardStory}
|
||||||
onSave={onSaveStory}
|
onSave={onSaveStory}
|
||||||
ourConversationId={ourConversationId}
|
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
renderStoryViewer={renderStoryViewer}
|
viewStory={viewStory}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<StoriesPane
|
<StoriesPane
|
||||||
|
@ -163,16 +98,7 @@ export const Stories = ({
|
||||||
setIsShowingStoryCreator(true);
|
setIsShowingStoryCreator(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onStoryClicked={clickedIdToView => {
|
onStoryClicked={viewUserStories}
|
||||||
const storyIndex = stories.findIndex(
|
|
||||||
x => x.conversationId === clickedIdToView
|
|
||||||
);
|
|
||||||
log.info('stories.onStoryClicked[StoriesPane]', {
|
|
||||||
storyIndex,
|
|
||||||
length: stories.length,
|
|
||||||
});
|
|
||||||
setConversationIdToView(clickedIdToView);
|
|
||||||
}}
|
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
showConversation={showConversation}
|
showConversation={showConversation}
|
||||||
stories={stories}
|
stories={stories}
|
||||||
|
|
|
@ -21,17 +21,14 @@ import { StoryListItem } from './StoryListItem';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
|
|
||||||
const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationStoryType> = {
|
const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationStoryType> = {
|
||||||
getFn: (obj, path) => {
|
getFn: (story, path) => {
|
||||||
if (path === 'searchNames') {
|
if (path === 'searchNames') {
|
||||||
return obj.stories
|
return [story.storyView.sender.title, story.storyView.sender.name].filter(
|
||||||
.flatMap((story: StoryViewType) => [
|
isNotNil
|
||||||
story.sender.title,
|
);
|
||||||
story.sender.name,
|
|
||||||
])
|
|
||||||
.filter(isNotNil);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj.group?.title ?? '';
|
return story.group?.title ?? '';
|
||||||
},
|
},
|
||||||
keys: [
|
keys: [
|
||||||
{
|
{
|
||||||
|
@ -55,9 +52,7 @@ function search(
|
||||||
.map(result => result.item);
|
.map(result => result.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNewestStory(
|
function getNewestMyStory(story: MyStoryType): StoryViewType {
|
||||||
story: ConversationStoryType | MyStoryType
|
|
||||||
): StoryViewType {
|
|
||||||
return story.stories[story.stories.length - 1];
|
return story.stories[story.stories.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +132,7 @@ export const StoriesPane = ({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
me={me}
|
me={me}
|
||||||
newestStory={
|
newestStory={
|
||||||
myStories.length ? getNewestStory(myStories[0]) : undefined
|
myStories.length ? getNewestMyStory(myStories[0]) : undefined
|
||||||
}
|
}
|
||||||
onClick={onMyStoriesClicked}
|
onClick={onMyStoriesClicked}
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
|
@ -151,7 +146,7 @@ export const StoriesPane = ({
|
||||||
<StoryListItem
|
<StoryListItem
|
||||||
group={story.group}
|
group={story.group}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
key={getNewestStory(story).timestamp}
|
key={story.storyView.timestamp}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onStoryClicked(story.conversationId);
|
onStoryClicked(story.conversationId);
|
||||||
}}
|
}}
|
||||||
|
@ -161,7 +156,7 @@ export const StoriesPane = ({
|
||||||
toggleStoriesView();
|
toggleStoriesView();
|
||||||
}}
|
}}
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
story={getNewestStory(story)}
|
story={story.storyView}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{Boolean(hiddenStories.length) && (
|
{Boolean(hiddenStories.length) && (
|
||||||
|
@ -178,7 +173,7 @@ export const StoriesPane = ({
|
||||||
{isShowingHiddenStories &&
|
{isShowingHiddenStories &&
|
||||||
hiddenStories.map(story => (
|
hiddenStories.map(story => (
|
||||||
<StoryListItem
|
<StoryListItem
|
||||||
key={getNewestStory(story).timestamp}
|
key={story.storyView.timestamp}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isHidden
|
isHidden
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -190,7 +185,7 @@ export const StoriesPane = ({
|
||||||
toggleStoriesView();
|
toggleStoriesView();
|
||||||
}}
|
}}
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
story={getNewestStory(story)}
|
story={story.storyView}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { Meta, Story } from '@storybook/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
|
|
||||||
import type { PropsType } from './StoryViewer';
|
import type { PropsType } from './StoryViewer';
|
||||||
import { StoryViewer } from './StoryViewer';
|
import { StoryViewer } from './StoryViewer';
|
||||||
|
@ -10,184 +10,114 @@ import enMessages from '../../_locales/en/messages.json';
|
||||||
import { setupI18n } from '../util/setupI18n';
|
import { setupI18n } from '../util/setupI18n';
|
||||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||||
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
|
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
|
||||||
|
import { getFakeStoryView } from '../test-both/helpers/getFakeStory';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Components/StoryViewer',
|
title: 'Components/StoryViewer',
|
||||||
};
|
component: StoryViewer,
|
||||||
|
argTypes: {
|
||||||
|
currentIndex: {
|
||||||
|
defaultvalue: 0,
|
||||||
|
},
|
||||||
|
getPreferredBadge: { action: true },
|
||||||
|
group: {
|
||||||
|
defaultValue: undefined,
|
||||||
|
},
|
||||||
|
hasAllStoriesMuted: {
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
i18n: {
|
||||||
|
defaultValue: i18n,
|
||||||
|
},
|
||||||
|
loadStoryReplies: { action: true },
|
||||||
|
markStoryRead: { action: true },
|
||||||
|
numStories: {
|
||||||
|
defaultValue: 1,
|
||||||
|
},
|
||||||
|
onGoToConversation: { action: true },
|
||||||
|
onHideStory: { action: true },
|
||||||
|
onReactToStory: { action: true },
|
||||||
|
onReplyToStory: { action: true },
|
||||||
|
onSetSkinTone: { action: true },
|
||||||
|
onTextTooLong: { action: true },
|
||||||
|
onUseEmoji: { action: true },
|
||||||
|
preferredReactionEmoji: {
|
||||||
|
defaultValue: ['❤️', '👍', '👎', '😂', '😮', '😢'],
|
||||||
|
},
|
||||||
|
queueStoryDownload: { action: true },
|
||||||
|
renderEmojiPicker: { action: true },
|
||||||
|
skinTone: {
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
story: {
|
||||||
|
defaultValue: getFakeStoryView(),
|
||||||
|
},
|
||||||
|
toggleHasAllStoriesMuted: { action: true },
|
||||||
|
viewStory: { action: true },
|
||||||
|
},
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
function getDefaultProps(): PropsType {
|
const Template: Story<PropsType> = args => <StoryViewer {...args} />;
|
||||||
const sender = getDefaultConversation();
|
|
||||||
|
|
||||||
return {
|
|
||||||
conversationId: sender.id,
|
|
||||||
getPreferredBadge: () => undefined,
|
|
||||||
group: undefined,
|
|
||||||
hasAllStoriesMuted: false,
|
|
||||||
i18n,
|
|
||||||
loadStoryReplies: action('loadStoryReplies'),
|
|
||||||
markStoryRead: action('markStoryRead'),
|
|
||||||
onClose: action('onClose'),
|
|
||||||
onGoToConversation: action('onGoToConversation'),
|
|
||||||
onHideStory: action('onHideStory'),
|
|
||||||
onNextUserStories: action('onNextUserStories'),
|
|
||||||
onPrevUserStories: action('onPrevUserStories'),
|
|
||||||
onReactToStory: action('onReactToStory'),
|
|
||||||
onReplyToStory: action('onReplyToStory'),
|
|
||||||
onSetSkinTone: action('onSetSkinTone'),
|
|
||||||
onTextTooLong: action('onTextTooLong'),
|
|
||||||
onUseEmoji: action('onUseEmoji'),
|
|
||||||
preferredReactionEmoji: ['❤️', '👍', '👎', '😂', '😮', '😢'],
|
|
||||||
queueStoryDownload: action('queueStoryDownload'),
|
|
||||||
renderEmojiPicker: () => <div />,
|
|
||||||
stories: [
|
|
||||||
{
|
|
||||||
attachment: fakeAttachment({
|
|
||||||
path: 'snow.jpg',
|
|
||||||
url: '/fixtures/snow.jpg',
|
|
||||||
}),
|
|
||||||
canReply: true,
|
|
||||||
messageId: '123',
|
|
||||||
sender,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
toggleHasAllStoriesMuted: action('toggleHasAllStoriesMuted'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SomeonesStory = (): JSX.Element => (
|
|
||||||
<StoryViewer {...getDefaultProps()} />
|
|
||||||
);
|
|
||||||
|
|
||||||
|
export const SomeonesStory = Template.bind({});
|
||||||
|
SomeonesStory.args = {};
|
||||||
SomeonesStory.story = {
|
SomeonesStory.story = {
|
||||||
name: "Someone's story",
|
name: "Someone's story",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WideStory = (): JSX.Element => (
|
export const WideStory = Template.bind({});
|
||||||
<StoryViewer
|
WideStory.args = {
|
||||||
{...getDefaultProps()}
|
story: getFakeStoryView('/fixtures/nathan-anderson-316188-unsplash.jpg'),
|
||||||
stories={[
|
};
|
||||||
{
|
|
||||||
attachment: fakeAttachment({
|
|
||||||
path: 'file.jpg',
|
|
||||||
url: '/fixtures/nathan-anderson-316188-unsplash.jpg',
|
|
||||||
}),
|
|
||||||
canReply: true,
|
|
||||||
messageId: '123',
|
|
||||||
sender: getDefaultConversation(),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
WideStory.story = {
|
WideStory.story = {
|
||||||
name: 'Wide story',
|
name: 'Wide story',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InAGroup = (): JSX.Element => (
|
export const InAGroup = Template.bind({});
|
||||||
<StoryViewer
|
InAGroup.args = {
|
||||||
{...getDefaultProps()}
|
group: getDefaultConversation({
|
||||||
group={getDefaultConversation({
|
avatarPath: '/fixtures/kitten-4-112-112.jpg',
|
||||||
avatarPath: '/fixtures/kitten-4-112-112.jpg',
|
title: 'Family Group',
|
||||||
title: 'Family Group',
|
type: 'group',
|
||||||
type: 'group',
|
}),
|
||||||
})}
|
};
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
InAGroup.story = {
|
InAGroup.story = {
|
||||||
name: 'In a group',
|
name: 'In a group',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MultiStory = (): JSX.Element => {
|
export const MultiStory = Template.bind({});
|
||||||
const sender = getDefaultConversation();
|
MultiStory.args = {
|
||||||
return (
|
currentIndex: 2,
|
||||||
<StoryViewer
|
numStories: 7,
|
||||||
{...getDefaultProps()}
|
story: getFakeStoryView('/fixtures/snow.jpg'),
|
||||||
stories={[
|
|
||||||
{
|
|
||||||
attachment: fakeAttachment({
|
|
||||||
path: 'snow.jpg',
|
|
||||||
url: '/fixtures/snow.jpg',
|
|
||||||
}),
|
|
||||||
messageId: '123',
|
|
||||||
sender,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
attachment: fakeAttachment({
|
|
||||||
path: 'file.jpg',
|
|
||||||
url: '/fixtures/nathan-anderson-316188-unsplash.jpg',
|
|
||||||
}),
|
|
||||||
canReply: true,
|
|
||||||
messageId: '456',
|
|
||||||
sender,
|
|
||||||
timestamp: Date.now() - 3600,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MultiStory.story = {
|
MultiStory.story = {
|
||||||
name: 'Multi story',
|
name: 'Multi story',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Caption = (): JSX.Element => (
|
export const Caption = Template.bind({});
|
||||||
<StoryViewer
|
Caption.args = {
|
||||||
{...getDefaultProps()}
|
story: {
|
||||||
group={getDefaultConversation({
|
...getFakeStoryView(),
|
||||||
avatarPath: '/fixtures/kitten-4-112-112.jpg',
|
attachment: fakeAttachment({
|
||||||
title: 'Broskis',
|
caption: 'This place looks lovely',
|
||||||
type: 'group',
|
path: 'file.jpg',
|
||||||
})}
|
url: '/fixtures/nathan-anderson-316188-unsplash.jpg',
|
||||||
replyState={{
|
}),
|
||||||
messageId: '123',
|
},
|
||||||
replies: [
|
};
|
||||||
{
|
|
||||||
...getDefaultConversation(),
|
|
||||||
body: 'Cool',
|
|
||||||
id: 'abc',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
stories={[
|
|
||||||
{
|
|
||||||
attachment: fakeAttachment({
|
|
||||||
caption: 'This place looks lovely',
|
|
||||||
path: 'file.jpg',
|
|
||||||
url: '/fixtures/nathan-anderson-316188-unsplash.jpg',
|
|
||||||
}),
|
|
||||||
canReply: true,
|
|
||||||
messageId: '123',
|
|
||||||
sender: getDefaultConversation(),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const LongCaption = (): JSX.Element => (
|
export const LongCaption = Template.bind({});
|
||||||
<StoryViewer
|
LongCaption.args = {
|
||||||
{...getDefaultProps()}
|
story: {
|
||||||
hasAllStoriesMuted
|
...getFakeStoryView(),
|
||||||
stories={[
|
attachment: fakeAttachment({
|
||||||
{
|
caption:
|
||||||
attachment: fakeAttachment({
|
'Snowycle, snowycle, snowycle\nI want to ride my snowycle, snowycle, snowycle\nI want to ride my snowycle\nI want to ride my snow\nI want to ride my snowycle\nI want to ride it where I like\nSnowycle, snowycle, snowycle\nI want to ride my snowycle, snowycle, snowycle\nI want to ride my snowycle\nI want to ride my snow\nI want to ride my snowycle\nI want to ride it where I like\nSnowycle, snowycle, snowycle\nI want to ride my snowycle, snowycle, snowycle\nI want to ride my snowycle\nI want to ride my snow\nI want to ride my snowycle\nI want to ride it where I like',
|
||||||
caption:
|
path: 'file.jpg',
|
||||||
'Snowycle, snowycle, snowycle\nI want to ride my snowycle, snowycle, snowycle\nI want to ride my snowycle\nI want to ride my snow\nI want to ride my snowycle\nI want to ride it where I like\nSnowycle, snowycle, snowycle\nI want to ride my snowycle, snowycle, snowycle\nI want to ride my snowycle\nI want to ride my snow\nI want to ride my snowycle\nI want to ride it where I like\nSnowycle, snowycle, snowycle\nI want to ride my snowycle, snowycle, snowycle\nI want to ride my snowycle\nI want to ride my snow\nI want to ride my snowycle\nI want to ride it where I like',
|
url: '/fixtures/snow.jpg',
|
||||||
path: 'file.jpg',
|
}),
|
||||||
url: '/fixtures/snow.jpg',
|
},
|
||||||
}),
|
};
|
||||||
canReply: true,
|
|
||||||
messageId: '123',
|
|
||||||
sender: getDefaultConversation(),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
|
@ -2,13 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import React, {
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useSpring, animated, to } from '@react-spring/web';
|
import { useSpring, animated, to } from '@react-spring/web';
|
||||||
import type { BodyRangeType, LocalizerType } from '../types/Util';
|
import type { BodyRangeType, LocalizerType } from '../types/Util';
|
||||||
|
@ -17,6 +11,8 @@ 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, StoryViewType } from '../types/Stories';
|
import type { ReplyStateType, StoryViewType } from '../types/Stories';
|
||||||
|
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||||
|
import * as log from '../logging/log';
|
||||||
import { AnimatedEmojiGalore } from './AnimatedEmojiGalore';
|
import { AnimatedEmojiGalore } from './AnimatedEmojiGalore';
|
||||||
import { Avatar, AvatarSize } from './Avatar';
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
|
@ -25,18 +21,17 @@ import { Intl } from './Intl';
|
||||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||||
import { SendStatus } from '../messages/MessageSendState';
|
import { SendStatus } from '../messages/MessageSendState';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
|
import { StoryViewDirectionType, StoryViewModeType } from '../types/Stories';
|
||||||
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
import { getAvatarColor } from '../types/Colors';
|
import { getAvatarColor } from '../types/Colors';
|
||||||
import { getStoryBackground } from '../util/getStoryBackground';
|
import { getStoryBackground } from '../util/getStoryBackground';
|
||||||
import { getStoryDuration } from '../util/getStoryDuration';
|
import { getStoryDuration } from '../util/getStoryDuration';
|
||||||
import { graphemeAwareSlice } from '../util/graphemeAwareSlice';
|
import { graphemeAwareSlice } from '../util/graphemeAwareSlice';
|
||||||
import { isDownloaded, isDownloading } from '../types/Attachment';
|
|
||||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||||
import * as log from '../logging/log';
|
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
conversationId: string;
|
currentIndex: number;
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
group?: Pick<
|
group?: Pick<
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -53,11 +48,9 @@ export type PropsType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
|
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
|
||||||
markStoryRead: (mId: string) => unknown;
|
markStoryRead: (mId: string) => unknown;
|
||||||
onClose: () => unknown;
|
numStories: number;
|
||||||
onGoToConversation: (conversationId: string) => unknown;
|
onGoToConversation: (conversationId: string) => unknown;
|
||||||
onHideStory: (conversationId: string) => unknown;
|
onHideStory: (conversationId: string) => unknown;
|
||||||
onNextUserStories?: () => unknown;
|
|
||||||
onPrevUserStories?: () => unknown;
|
|
||||||
onSetSkinTone: (tone: number) => unknown;
|
onSetSkinTone: (tone: number) => unknown;
|
||||||
onTextTooLong: () => unknown;
|
onTextTooLong: () => unknown;
|
||||||
onReactToStory: (emoji: string, story: StoryViewType) => unknown;
|
onReactToStory: (emoji: string, story: StoryViewType) => unknown;
|
||||||
|
@ -74,8 +67,10 @@ export type PropsType = {
|
||||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
||||||
replyState?: ReplyStateType;
|
replyState?: ReplyStateType;
|
||||||
skinTone?: number;
|
skinTone?: number;
|
||||||
stories: Array<StoryViewType>;
|
story: StoryViewType;
|
||||||
|
storyViewMode?: StoryViewModeType;
|
||||||
toggleHasAllStoriesMuted: () => unknown;
|
toggleHasAllStoriesMuted: () => unknown;
|
||||||
|
viewStory: ViewStoryActionCreatorType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CAPTION_BUFFER = 20;
|
const CAPTION_BUFFER = 20;
|
||||||
|
@ -90,18 +85,16 @@ enum Arrow {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StoryViewer = ({
|
export const StoryViewer = ({
|
||||||
conversationId,
|
currentIndex,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
group,
|
group,
|
||||||
hasAllStoriesMuted,
|
hasAllStoriesMuted,
|
||||||
i18n,
|
i18n,
|
||||||
loadStoryReplies,
|
loadStoryReplies,
|
||||||
markStoryRead,
|
markStoryRead,
|
||||||
onClose,
|
numStories,
|
||||||
onGoToConversation,
|
onGoToConversation,
|
||||||
onHideStory,
|
onHideStory,
|
||||||
onNextUserStories,
|
|
||||||
onPrevUserStories,
|
|
||||||
onReactToStory,
|
onReactToStory,
|
||||||
onReplyToStory,
|
onReplyToStory,
|
||||||
onSetSkinTone,
|
onSetSkinTone,
|
||||||
|
@ -113,10 +106,11 @@ export const StoryViewer = ({
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
replyState,
|
replyState,
|
||||||
skinTone,
|
skinTone,
|
||||||
stories,
|
story,
|
||||||
|
storyViewMode,
|
||||||
toggleHasAllStoriesMuted,
|
toggleHasAllStoriesMuted,
|
||||||
|
viewStory,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const [currentStoryIndex, setCurrentStoryIndex] = useState(0);
|
|
||||||
const [storyDuration, setStoryDuration] = useState<number | undefined>();
|
const [storyDuration, setStoryDuration] = useState<number | undefined>();
|
||||||
const [isShowingContextMenu, setIsShowingContextMenu] = useState(false);
|
const [isShowingContextMenu, setIsShowingContextMenu] = useState(false);
|
||||||
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
|
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
|
||||||
|
@ -124,10 +118,8 @@ export const StoryViewer = ({
|
||||||
useState<HTMLButtonElement | null>(null);
|
useState<HTMLButtonElement | null>(null);
|
||||||
const [reactionEmoji, setReactionEmoji] = useState<string | undefined>();
|
const [reactionEmoji, setReactionEmoji] = useState<string | undefined>();
|
||||||
|
|
||||||
const visibleStory = stories[currentStoryIndex];
|
|
||||||
|
|
||||||
const { attachment, canReply, isHidden, messageId, sendState, timestamp } =
|
const { attachment, canReply, isHidden, messageId, sendState, timestamp } =
|
||||||
visibleStory;
|
story;
|
||||||
const {
|
const {
|
||||||
acceptedMessageRequest,
|
acceptedMessageRequest,
|
||||||
avatarPath,
|
avatarPath,
|
||||||
|
@ -139,10 +131,14 @@ export const StoryViewer = ({
|
||||||
profileName,
|
profileName,
|
||||||
sharedGroupNames,
|
sharedGroupNames,
|
||||||
title,
|
title,
|
||||||
} = visibleStory.sender;
|
} = story.sender;
|
||||||
|
|
||||||
const [hasReplyModal, setHasReplyModal] = useState(false);
|
const [hasReplyModal, setHasReplyModal] = useState(false);
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
viewStory();
|
||||||
|
}, [viewStory]);
|
||||||
|
|
||||||
const onEscape = useCallback(() => {
|
const onEscape = useCallback(() => {
|
||||||
if (hasReplyModal) {
|
if (hasReplyModal) {
|
||||||
setHasReplyModal(false);
|
setHasReplyModal(false);
|
||||||
|
@ -173,48 +169,6 @@ export const StoryViewer = ({
|
||||||
setHasExpandedCaption(false);
|
setHasExpandedCaption(false);
|
||||||
}, [messageId]);
|
}, [messageId]);
|
||||||
|
|
||||||
// These exist to change currentStoryIndex to the oldest unread story a user
|
|
||||||
// has, or set to 0 whenever conversationId changes.
|
|
||||||
// We use a ref so that we only depend on conversationId changing, since
|
|
||||||
// the stories Array will change once we mark as story as viewed.
|
|
||||||
const storiesRef = useRef(stories);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unreadStoryIndex = storiesRef.current.findIndex(
|
|
||||||
story => story.isUnread
|
|
||||||
);
|
|
||||||
log.info('stories.findUnreadStory', {
|
|
||||||
unreadStoryIndex,
|
|
||||||
stories: storiesRef.current.length,
|
|
||||||
});
|
|
||||||
setCurrentStoryIndex(unreadStoryIndex < 0 ? 0 : unreadStoryIndex);
|
|
||||||
}, [conversationId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
storiesRef.current = stories;
|
|
||||||
}, [stories]);
|
|
||||||
|
|
||||||
// Either we show the next story in the current user's stories or we ask
|
|
||||||
// for the next user's stories.
|
|
||||||
const showNextStory = useCallback(() => {
|
|
||||||
if (currentStoryIndex < stories.length - 1) {
|
|
||||||
setCurrentStoryIndex(currentStoryIndex + 1);
|
|
||||||
} else {
|
|
||||||
setCurrentStoryIndex(0);
|
|
||||||
onNextUserStories?.();
|
|
||||||
}
|
|
||||||
}, [currentStoryIndex, onNextUserStories, stories.length]);
|
|
||||||
|
|
||||||
// Either we show the previous story in the current user's stories or we ask
|
|
||||||
// for the prior user's stories.
|
|
||||||
const showPrevStory = useCallback(() => {
|
|
||||||
if (currentStoryIndex === 0) {
|
|
||||||
onPrevUserStories?.();
|
|
||||||
} else {
|
|
||||||
setCurrentStoryIndex(currentStoryIndex - 1);
|
|
||||||
}
|
|
||||||
}, [currentStoryIndex, onPrevUserStories]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let shouldCancel = false;
|
let shouldCancel = false;
|
||||||
(async function hydrateStoryDuration() {
|
(async function hydrateStoryDuration() {
|
||||||
|
@ -247,12 +201,16 @@ export const StoryViewer = ({
|
||||||
onRest: {
|
onRest: {
|
||||||
width: ({ value }) => {
|
width: ({ value }) => {
|
||||||
if (value === 100) {
|
if (value === 100) {
|
||||||
showNextStory();
|
viewStory(
|
||||||
|
story.messageId,
|
||||||
|
storyViewMode,
|
||||||
|
StoryViewDirectionType.Next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[showNextStory]
|
[story.messageId, storyViewMode, viewStory]
|
||||||
);
|
);
|
||||||
|
|
||||||
// We need to be careful about this effect refreshing, it should only run
|
// We need to be careful about this effect refreshing, it should only run
|
||||||
|
@ -274,7 +232,7 @@ export const StoryViewer = ({
|
||||||
return () => {
|
return () => {
|
||||||
spring.stop();
|
spring.stop();
|
||||||
};
|
};
|
||||||
}, [currentStoryIndex, spring, storyDuration]);
|
}, [currentIndex, spring, storyDuration]);
|
||||||
|
|
||||||
const [pauseStory, setPauseStory] = useState(false);
|
const [pauseStory, setPauseStory] = useState(false);
|
||||||
|
|
||||||
|
@ -299,32 +257,23 @@ export const StoryViewer = ({
|
||||||
log.info('stories.markStoryRead', { messageId });
|
log.info('stories.markStoryRead', { messageId });
|
||||||
}, [markStoryRead, messageId]);
|
}, [markStoryRead, messageId]);
|
||||||
|
|
||||||
// Queue all undownloaded stories once we're viewing someone's stories
|
|
||||||
const storiesToDownload = useMemo(() => {
|
|
||||||
return stories
|
|
||||||
.filter(
|
|
||||||
story =>
|
|
||||||
!isDownloaded(story.attachment) && !isDownloading(story.attachment)
|
|
||||||
)
|
|
||||||
.map(story => story.messageId);
|
|
||||||
}, [stories]);
|
|
||||||
useEffect(() => {
|
|
||||||
storiesToDownload.forEach(storyId => queueStoryDownload(storyId));
|
|
||||||
}, [queueStoryDownload, storiesToDownload]);
|
|
||||||
|
|
||||||
const navigateStories = useCallback(
|
const navigateStories = useCallback(
|
||||||
(ev: KeyboardEvent) => {
|
(ev: KeyboardEvent) => {
|
||||||
if (ev.key === 'ArrowRight') {
|
if (ev.key === 'ArrowRight') {
|
||||||
showNextStory();
|
viewStory(story.messageId, storyViewMode, StoryViewDirectionType.Next);
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
} else if (ev.key === 'ArrowLeft') {
|
} else if (ev.key === 'ArrowLeft') {
|
||||||
showPrevStory();
|
viewStory(
|
||||||
|
story.messageId,
|
||||||
|
storyViewMode,
|
||||||
|
StoryViewDirectionType.Previous
|
||||||
|
);
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[showPrevStory, showNextStory]
|
[story.messageId, storyViewMode, viewStory]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -335,13 +284,14 @@ export const StoryViewer = ({
|
||||||
};
|
};
|
||||||
}, [navigateStories]);
|
}, [navigateStories]);
|
||||||
|
|
||||||
const isGroupStory = Boolean(group?.id);
|
const groupId = group?.id;
|
||||||
|
const isGroupStory = Boolean(groupId);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isGroupStory) {
|
if (!groupId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadStoryReplies(conversationId, messageId);
|
loadStoryReplies(groupId, messageId);
|
||||||
}, [conversationId, isGroupStory, loadStoryReplies, messageId]);
|
}, [groupId, loadStoryReplies, messageId]);
|
||||||
|
|
||||||
const [arrowToShow, setArrowToShow] = useState<Arrow>(Arrow.None);
|
const [arrowToShow, setArrowToShow] = useState<Arrow>(Arrow.None);
|
||||||
|
|
||||||
|
@ -385,6 +335,8 @@ export const StoryViewer = ({
|
||||||
|
|
||||||
const shouldShowContextMenu = !sendState;
|
const shouldShowContextMenu = !sendState;
|
||||||
|
|
||||||
|
const hasPrevNextArrows = storyViewMode !== StoryViewModeType.Single;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
|
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
|
||||||
<div className="StoryViewer">
|
<div className="StoryViewer">
|
||||||
|
@ -393,7 +345,7 @@ export const StoryViewer = ({
|
||||||
style={{ background: getStoryBackground(attachment) }}
|
style={{ background: getStoryBackground(attachment) }}
|
||||||
/>
|
/>
|
||||||
<div className="StoryViewer__content">
|
<div className="StoryViewer__content">
|
||||||
{onPrevUserStories && (
|
{hasPrevNextArrows && (
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('back')}
|
aria-label={i18n('back')}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -402,7 +354,13 @@ export const StoryViewer = ({
|
||||||
'StoryViewer__arrow--visible': arrowToShow === Arrow.Left,
|
'StoryViewer__arrow--visible': arrowToShow === Arrow.Left,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={showPrevStory}
|
onClick={() =>
|
||||||
|
viewStory(
|
||||||
|
story.messageId,
|
||||||
|
storyViewMode,
|
||||||
|
StoryViewDirectionType.Previous
|
||||||
|
)
|
||||||
|
}
|
||||||
onMouseMove={() => setArrowToShow(Arrow.Left)}
|
onMouseMove={() => setArrowToShow(Arrow.Left)}
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
|
@ -549,12 +507,9 @@ export const StoryViewer = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="StoryViewer__progress">
|
<div className="StoryViewer__progress">
|
||||||
{stories.map((story, index) => (
|
{Array.from(Array(numStories), (_, index) => (
|
||||||
<div
|
<div className="StoryViewer__progress--container" key={index}>
|
||||||
className="StoryViewer__progress--container"
|
{currentIndex === index ? (
|
||||||
key={story.messageId}
|
|
||||||
>
|
|
||||||
{currentStoryIndex === index ? (
|
|
||||||
<animated.div
|
<animated.div
|
||||||
className="StoryViewer__progress--bar"
|
className="StoryViewer__progress--bar"
|
||||||
style={{
|
style={{
|
||||||
|
@ -565,7 +520,7 @@ export const StoryViewer = ({
|
||||||
<div
|
<div
|
||||||
className="StoryViewer__progress--bar"
|
className="StoryViewer__progress--bar"
|
||||||
style={{
|
style={{
|
||||||
width: currentStoryIndex < index ? '0%' : '100%',
|
width: currentIndex < index ? '0%' : '100%',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -626,7 +581,7 @@ export const StoryViewer = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{onNextUserStories && (
|
{hasPrevNextArrows && (
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('forward')}
|
aria-label={i18n('forward')}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -635,7 +590,13 @@ export const StoryViewer = ({
|
||||||
'StoryViewer__arrow--visible': arrowToShow === Arrow.Right,
|
'StoryViewer__arrow--visible': arrowToShow === Arrow.Right,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={showNextStory}
|
onClick={() =>
|
||||||
|
viewStory(
|
||||||
|
story.messageId,
|
||||||
|
storyViewMode,
|
||||||
|
StoryViewDirectionType.Next
|
||||||
|
)
|
||||||
|
}
|
||||||
onMouseMove={() => setArrowToShow(Arrow.Right)}
|
onMouseMove={() => setArrowToShow(Arrow.Right)}
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
|
@ -686,7 +647,7 @@ export const StoryViewer = ({
|
||||||
isMyStory={isMe}
|
isMyStory={isMe}
|
||||||
onClose={() => setHasReplyModal(false)}
|
onClose={() => setHasReplyModal(false)}
|
||||||
onReact={emoji => {
|
onReact={emoji => {
|
||||||
onReactToStory(emoji, visibleStory);
|
onReactToStory(emoji, story);
|
||||||
setHasReplyModal(false);
|
setHasReplyModal(false);
|
||||||
setReactionEmoji(emoji);
|
setReactionEmoji(emoji);
|
||||||
}}
|
}}
|
||||||
|
@ -694,7 +655,7 @@ export const StoryViewer = ({
|
||||||
if (!isGroupStory) {
|
if (!isGroupStory) {
|
||||||
setHasReplyModal(false);
|
setHasReplyModal(false);
|
||||||
}
|
}
|
||||||
onReplyToStory(message, mentions, replyTimestamp, visibleStory);
|
onReplyToStory(message, mentions, replyTimestamp, story);
|
||||||
}}
|
}}
|
||||||
onSetSkinTone={onSetSkinTone}
|
onSetSkinTone={onSetSkinTone}
|
||||||
onTextTooLong={onTextTooLong}
|
onTextTooLong={onTextTooLong}
|
||||||
|
|
|
@ -253,6 +253,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
},
|
},
|
||||||
theme: ThemeType.light,
|
theme: ThemeType.light,
|
||||||
timestamp: number('timestamp', overrideProps.timestamp || Date.now()),
|
timestamp: number('timestamp', overrideProps.timestamp || Date.now()),
|
||||||
|
viewStory: action('viewStory'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createTimelineItem = (data: undefined | Props) =>
|
const createTimelineItem = (data: undefined | Props) =>
|
||||||
|
|
|
@ -16,6 +16,7 @@ import type {
|
||||||
ConversationTypeType,
|
ConversationTypeType,
|
||||||
InteractionModeType,
|
InteractionModeType,
|
||||||
} from '../../state/ducks/conversations';
|
} from '../../state/ducks/conversations';
|
||||||
|
import type { ViewStoryActionCreatorType } from '../../state/ducks/stories';
|
||||||
import type { TimelineItemType } from './TimelineItem';
|
import type { TimelineItemType } from './TimelineItem';
|
||||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
import { Avatar, AvatarSize } from '../Avatar';
|
import { Avatar, AvatarSize } from '../Avatar';
|
||||||
|
@ -44,6 +45,7 @@ import { shouldUseFullSizeLinkPreviewImage } from '../../linkPreviews/shouldUseF
|
||||||
import { WidthBreakpoint } from '../_util';
|
import { WidthBreakpoint } from '../_util';
|
||||||
import { OutgoingGiftBadgeModal } from '../OutgoingGiftBadgeModal';
|
import { OutgoingGiftBadgeModal } from '../OutgoingGiftBadgeModal';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
import { StoryViewModeType } from '../../types/Stories';
|
||||||
|
|
||||||
import type { AttachmentType } from '../../types/Attachment';
|
import type { AttachmentType } from '../../types/Attachment';
|
||||||
import {
|
import {
|
||||||
|
@ -252,7 +254,7 @@ export type PropsData = {
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
isFromMe: boolean;
|
isFromMe: boolean;
|
||||||
rawAttachment?: QuotedAttachmentType;
|
rawAttachment?: QuotedAttachmentType;
|
||||||
referencedMessageNotFound?: boolean;
|
storyId?: string;
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
previews: Array<LinkPreviewType>;
|
previews: Array<LinkPreviewType>;
|
||||||
|
@ -360,6 +362,7 @@ export type PropsActions = {
|
||||||
|
|
||||||
showExpiredIncomingTapToViewToast: () => unknown;
|
showExpiredIncomingTapToViewToast: () => unknown;
|
||||||
showExpiredOutgoingTapToViewToast: () => unknown;
|
showExpiredOutgoingTapToViewToast: () => unknown;
|
||||||
|
viewStory: ViewStoryActionCreatorType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = PropsData &
|
export type Props = PropsData &
|
||||||
|
@ -1519,6 +1522,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
direction,
|
direction,
|
||||||
i18n,
|
i18n,
|
||||||
storyReplyContext,
|
storyReplyContext,
|
||||||
|
viewStory,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!storyReplyContext) {
|
if (!storyReplyContext) {
|
||||||
|
@ -1546,13 +1550,11 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
isViewOnce={false}
|
isViewOnce={false}
|
||||||
moduleClassName="StoryReplyQuote"
|
moduleClassName="StoryReplyQuote"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// TODO DESKTOP-3255
|
viewStory(storyReplyContext.storyId, StoryViewModeType.Single);
|
||||||
}}
|
}}
|
||||||
rawAttachment={storyReplyContext.rawAttachment}
|
rawAttachment={storyReplyContext.rawAttachment}
|
||||||
reactionEmoji={storyReplyContext.emoji}
|
reactionEmoji={storyReplyContext.emoji}
|
||||||
referencedMessageNotFound={Boolean(
|
referencedMessageNotFound={!storyReplyContext.storyId}
|
||||||
storyReplyContext.referencedMessageNotFound
|
|
||||||
)}
|
|
||||||
text={storyReplyContext.text}
|
text={storyReplyContext.text}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -103,6 +103,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
showForwardMessageModal: action('showForwardMessageModal'),
|
showForwardMessageModal: action('showForwardMessageModal'),
|
||||||
showVisualAttachment: action('showVisualAttachment'),
|
showVisualAttachment: action('showVisualAttachment'),
|
||||||
startConversation: action('startConversation'),
|
startConversation: action('startConversation'),
|
||||||
|
viewStory: action('viewStory'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DeliveredIncoming = (): JSX.Element => {
|
export const DeliveredIncoming = (): JSX.Element => {
|
||||||
|
|
|
@ -96,6 +96,7 @@ export type PropsReduxActions = Pick<
|
||||||
| 'clearSelectedMessage'
|
| 'clearSelectedMessage'
|
||||||
| 'doubleCheckMissingQuoteReference'
|
| 'doubleCheckMissingQuoteReference'
|
||||||
| 'checkForAccount'
|
| 'checkForAccount'
|
||||||
|
| 'viewStory'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type ExternalProps = PropsData & PropsBackboneActions;
|
export type ExternalProps = PropsData & PropsBackboneActions;
|
||||||
|
@ -302,6 +303,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
showVisualAttachment,
|
showVisualAttachment,
|
||||||
startConversation,
|
startConversation,
|
||||||
theme,
|
theme,
|
||||||
|
viewStory,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -371,6 +373,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
showVisualAttachment={showVisualAttachment}
|
showVisualAttachment={showVisualAttachment}
|
||||||
startConversation={startConversation}
|
startConversation={startConversation}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
viewStory={viewStory}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<table className="module-message-detail__info">
|
<table className="module-message-detail__info">
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
// Copyright 2020-2022 Signal Messenger, LLC
|
// Copyright 2020-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { Meta, Story } from '@storybook/react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { isString } from 'lodash';
|
|
||||||
|
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { boolean, text } from '@storybook/addon-knobs';
|
|
||||||
|
|
||||||
import { ConversationColors } from '../../types/Colors';
|
import { ConversationColors } from '../../types/Colors';
|
||||||
import { pngUrl } from '../../storybook/Fixtures';
|
import { pngUrl } from '../../storybook/Fixtures';
|
||||||
|
@ -30,8 +29,49 @@ import { ThemeType } from '../../types/Util';
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
component: Quote,
|
||||||
title: 'Components/Conversation/Quote',
|
title: 'Components/Conversation/Quote',
|
||||||
};
|
argTypes: {
|
||||||
|
authorTitle: {
|
||||||
|
defaultValue: 'Default Sender',
|
||||||
|
},
|
||||||
|
conversationColor: {
|
||||||
|
defaultValue: 'forest',
|
||||||
|
},
|
||||||
|
doubleCheckMissingQuoteReference: { action: true },
|
||||||
|
i18n: {
|
||||||
|
defaultValue: i18n,
|
||||||
|
},
|
||||||
|
isFromMe: {
|
||||||
|
control: { type: 'checkbox' },
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
isGiftBadge: {
|
||||||
|
control: { type: 'checkbox' },
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
isIncoming: {
|
||||||
|
control: { type: 'checkbox' },
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
isViewOnce: {
|
||||||
|
control: { type: 'checkbox' },
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
onClick: { action: true },
|
||||||
|
onClose: { action: true },
|
||||||
|
rawAttachment: {
|
||||||
|
defaultValue: undefined,
|
||||||
|
},
|
||||||
|
referencedMessageNotFound: {
|
||||||
|
control: { type: 'checkbox' },
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
defaultValue: 'A sample message from a pal',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
const defaultMessageProps: MessagesProps = {
|
const defaultMessageProps: MessagesProps = {
|
||||||
author: getDefaultConversation({
|
author: getDefaultConversation({
|
||||||
|
@ -105,6 +145,7 @@ const defaultMessageProps: MessagesProps = {
|
||||||
textDirection: TextDirection.Default,
|
textDirection: TextDirection.Default,
|
||||||
theme: ThemeType.light,
|
theme: ThemeType.light,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
viewStory: action('viewStory'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderInMessage = ({
|
const renderInMessage = ({
|
||||||
|
@ -143,459 +184,332 @@ const renderInMessage = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
const Template: Story<Props> = args => <Quote {...args} />;
|
||||||
authorTitle: text(
|
const TemplateInMessage: Story<Props> = args => renderInMessage(args);
|
||||||
'authorTitle',
|
|
||||||
overrideProps.authorTitle || 'Default Sender'
|
|
||||||
),
|
|
||||||
conversationColor: overrideProps.conversationColor || 'forest',
|
|
||||||
doubleCheckMissingQuoteReference:
|
|
||||||
overrideProps.doubleCheckMissingQuoteReference ||
|
|
||||||
action('doubleCheckMissingQuoteReference'),
|
|
||||||
i18n,
|
|
||||||
isFromMe: boolean('isFromMe', overrideProps.isFromMe || false),
|
|
||||||
isIncoming: boolean('isIncoming', overrideProps.isIncoming || false),
|
|
||||||
onClick: action('onClick'),
|
|
||||||
onClose: action('onClose'),
|
|
||||||
rawAttachment: overrideProps.rawAttachment || undefined,
|
|
||||||
referencedMessageNotFound: boolean(
|
|
||||||
'referencedMessageNotFound',
|
|
||||||
overrideProps.referencedMessageNotFound || false
|
|
||||||
),
|
|
||||||
isGiftBadge: boolean('isGiftBadge', overrideProps.isGiftBadge || false),
|
|
||||||
isViewOnce: boolean('isViewOnce', overrideProps.isViewOnce || false),
|
|
||||||
text: text(
|
|
||||||
'text',
|
|
||||||
isString(overrideProps.text)
|
|
||||||
? overrideProps.text
|
|
||||||
: 'A sample message from a pal'
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const OutgoingByAnotherAuthor = (): JSX.Element => {
|
export const OutgoingByAnotherAuthor = Template.bind({});
|
||||||
const props = createProps({
|
OutgoingByAnotherAuthor.args = {
|
||||||
authorTitle: 'Terrence Malick',
|
authorTitle: getDefaultConversation().title,
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
OutgoingByAnotherAuthor.story = {
|
OutgoingByAnotherAuthor.story = {
|
||||||
name: 'Outgoing by Another Author',
|
name: 'Outgoing by Another Author',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OutgoingByMe = (): JSX.Element => {
|
export const OutgoingByMe = Template.bind({});
|
||||||
const props = createProps({
|
OutgoingByMe.args = {
|
||||||
isFromMe: true,
|
isFromMe: true,
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
OutgoingByMe.story = {
|
OutgoingByMe.story = {
|
||||||
name: 'Outgoing by Me',
|
name: 'Outgoing by Me',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IncomingByAnotherAuthor = (): JSX.Element => {
|
export const IncomingByAnotherAuthor = Template.bind({});
|
||||||
const props = createProps({
|
IncomingByAnotherAuthor.args = {
|
||||||
authorTitle: 'Terrence Malick',
|
authorTitle: getDefaultConversation().title,
|
||||||
isIncoming: true,
|
isIncoming: true,
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
IncomingByAnotherAuthor.story = {
|
IncomingByAnotherAuthor.story = {
|
||||||
name: 'Incoming by Another Author',
|
name: 'Incoming by Another Author',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IncomingByMe = (): JSX.Element => {
|
export const IncomingByMe = Template.bind({});
|
||||||
const props = createProps({
|
IncomingByMe.args = {
|
||||||
isFromMe: true,
|
isFromMe: true,
|
||||||
isIncoming: true,
|
isIncoming: true,
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
IncomingByMe.story = {
|
IncomingByMe.story = {
|
||||||
name: 'Incoming by Me',
|
name: 'Incoming by Me',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IncomingOutgoingColors = (): JSX.Element => {
|
export const IncomingOutgoingColors = (args: Props): JSX.Element => {
|
||||||
const props = createProps({});
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ConversationColors.map(color =>
|
{ConversationColors.map(color =>
|
||||||
renderInMessage({ ...props, conversationColor: color })
|
renderInMessage({ ...args, conversationColor: color })
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
IncomingOutgoingColors.args = {};
|
||||||
IncomingOutgoingColors.story = {
|
IncomingOutgoingColors.story = {
|
||||||
name: 'Incoming/Outgoing Colors',
|
name: 'Incoming/Outgoing Colors',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageOnly = (): JSX.Element => {
|
export const ImageOnly = Template.bind({});
|
||||||
const props = createProps({
|
ImageOnly.args = {
|
||||||
text: '',
|
text: '',
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
|
contentType: IMAGE_PNG,
|
||||||
|
fileName: 'sax.png',
|
||||||
|
isVoiceMessage: false,
|
||||||
|
thumbnail: {
|
||||||
contentType: IMAGE_PNG,
|
contentType: IMAGE_PNG,
|
||||||
fileName: 'sax.png',
|
height: 100,
|
||||||
isVoiceMessage: false,
|
width: 100,
|
||||||
thumbnail: {
|
path: pngUrl,
|
||||||
contentType: IMAGE_PNG,
|
objectUrl: pngUrl,
|
||||||
height: 100,
|
|
||||||
width: 100,
|
|
||||||
path: pngUrl,
|
|
||||||
objectUrl: pngUrl,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageAttachment = (): JSX.Element => {
|
export const ImageAttachment = Template.bind({});
|
||||||
const props = createProps({
|
ImageAttachment.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
|
contentType: IMAGE_PNG,
|
||||||
|
fileName: 'sax.png',
|
||||||
|
isVoiceMessage: false,
|
||||||
|
thumbnail: {
|
||||||
contentType: IMAGE_PNG,
|
contentType: IMAGE_PNG,
|
||||||
fileName: 'sax.png',
|
height: 100,
|
||||||
isVoiceMessage: false,
|
width: 100,
|
||||||
thumbnail: {
|
path: pngUrl,
|
||||||
contentType: IMAGE_PNG,
|
objectUrl: pngUrl,
|
||||||
height: 100,
|
|
||||||
width: 100,
|
|
||||||
path: pngUrl,
|
|
||||||
objectUrl: pngUrl,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageAttachmentWOThumbnail = (): JSX.Element => {
|
export const ImageAttachmentNoThumbnail = Template.bind({});
|
||||||
const props = createProps({
|
ImageAttachmentNoThumbnail.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: IMAGE_PNG,
|
contentType: IMAGE_PNG,
|
||||||
fileName: 'sax.png',
|
fileName: 'sax.png',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
ImageAttachmentNoThumbnail.story = {
|
||||||
ImageAttachmentWOThumbnail.story = {
|
|
||||||
name: 'Image Attachment w/o Thumbnail',
|
name: 'Image Attachment w/o Thumbnail',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageTapToView = (): JSX.Element => {
|
export const ImageTapToView = Template.bind({});
|
||||||
const props = createProps({
|
ImageTapToView.args = {
|
||||||
text: '',
|
text: '',
|
||||||
isViewOnce: true,
|
isViewOnce: true,
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: IMAGE_PNG,
|
contentType: IMAGE_PNG,
|
||||||
fileName: 'sax.png',
|
fileName: 'sax.png',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageTapToView.story = {
|
ImageTapToView.story = {
|
||||||
name: 'Image Tap-to-View',
|
name: 'Image Tap-to-View',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VideoOnly = (): JSX.Element => {
|
export const VideoOnly = Template.bind({});
|
||||||
const props = createProps({
|
VideoOnly.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: VIDEO_MP4,
|
contentType: VIDEO_MP4,
|
||||||
fileName: 'great-video.mp4',
|
fileName: 'great-video.mp4',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
contentType: IMAGE_PNG,
|
contentType: IMAGE_PNG,
|
||||||
height: 100,
|
height: 100,
|
||||||
width: 100,
|
width: 100,
|
||||||
path: pngUrl,
|
path: pngUrl,
|
||||||
objectUrl: pngUrl,
|
objectUrl: pngUrl,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
text: undefined,
|
||||||
props.text = undefined as any;
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VideoAttachment = (): JSX.Element => {
|
export const VideoAttachment = Template.bind({});
|
||||||
const props = createProps({
|
VideoAttachment.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: VIDEO_MP4,
|
contentType: VIDEO_MP4,
|
||||||
fileName: 'great-video.mp4',
|
fileName: 'great-video.mp4',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
contentType: IMAGE_PNG,
|
contentType: IMAGE_PNG,
|
||||||
height: 100,
|
height: 100,
|
||||||
width: 100,
|
width: 100,
|
||||||
path: pngUrl,
|
path: pngUrl,
|
||||||
objectUrl: pngUrl,
|
objectUrl: pngUrl,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VideoAttachmentWOThumbnail = (): JSX.Element => {
|
export const VideoAttachmentNoThumbnail = Template.bind({});
|
||||||
const props = createProps({
|
VideoAttachmentNoThumbnail.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: VIDEO_MP4,
|
contentType: VIDEO_MP4,
|
||||||
fileName: 'great-video.mp4',
|
fileName: 'great-video.mp4',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
VideoAttachmentNoThumbnail.story = {
|
||||||
VideoAttachmentWOThumbnail.story = {
|
|
||||||
name: 'Video Attachment w/o Thumbnail',
|
name: 'Video Attachment w/o Thumbnail',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VideoTapToView = (): JSX.Element => {
|
export const VideoTapToView = Template.bind({});
|
||||||
const props = createProps({
|
VideoTapToView.args = {
|
||||||
text: '',
|
text: '',
|
||||||
isViewOnce: true,
|
isViewOnce: true,
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: VIDEO_MP4,
|
contentType: VIDEO_MP4,
|
||||||
fileName: 'great-video.mp4',
|
fileName: 'great-video.mp4',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
VideoTapToView.story = {
|
VideoTapToView.story = {
|
||||||
name: 'Video Tap-to-View',
|
name: 'Video Tap-to-View',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GiftBadge = (): JSX.Element => {
|
export const GiftBadge = TemplateInMessage.bind({});
|
||||||
const props = createProps({
|
GiftBadge.args = {
|
||||||
text: "Some text which shouldn't be rendered",
|
text: "Some text which shouldn't be rendered",
|
||||||
isGiftBadge: true,
|
isGiftBadge: true,
|
||||||
});
|
|
||||||
|
|
||||||
return renderInMessage(props);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AudioOnly = (): JSX.Element => {
|
export const AudioOnly = Template.bind({});
|
||||||
const props = createProps({
|
AudioOnly.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: AUDIO_MP3,
|
contentType: AUDIO_MP3,
|
||||||
fileName: 'great-video.mp3',
|
fileName: 'great-video.mp3',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
text: undefined,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
props.text = undefined as any;
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AudioAttachment = (): JSX.Element => {
|
export const AudioAttachment = Template.bind({});
|
||||||
const props = createProps({
|
AudioAttachment.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: AUDIO_MP3,
|
contentType: AUDIO_MP3,
|
||||||
fileName: 'great-video.mp3',
|
fileName: 'great-video.mp3',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VoiceMessageOnly = (): JSX.Element => {
|
export const VoiceMessageOnly = Template.bind({});
|
||||||
const props = createProps({
|
VoiceMessageOnly.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: AUDIO_MP3,
|
contentType: AUDIO_MP3,
|
||||||
fileName: 'great-video.mp3',
|
fileName: 'great-video.mp3',
|
||||||
isVoiceMessage: true,
|
isVoiceMessage: true,
|
||||||
},
|
},
|
||||||
});
|
text: undefined,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
props.text = undefined as any;
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VoiceMessageAttachment = (): JSX.Element => {
|
export const VoiceMessageAttachment = Template.bind({});
|
||||||
const props = createProps({
|
VoiceMessageAttachment.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: AUDIO_MP3,
|
contentType: AUDIO_MP3,
|
||||||
fileName: 'great-video.mp3',
|
fileName: 'great-video.mp3',
|
||||||
isVoiceMessage: true,
|
isVoiceMessage: true,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OtherFileOnly = (): JSX.Element => {
|
export const OtherFileOnly = Template.bind({});
|
||||||
const props = createProps({
|
OtherFileOnly.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: stringToMIMEType('application/json'),
|
contentType: stringToMIMEType('application/json'),
|
||||||
fileName: 'great-data.json',
|
fileName: 'great-data.json',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
text: undefined,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
props.text = undefined as any;
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MediaTapToView = (): JSX.Element => {
|
export const MediaTapToView = Template.bind({});
|
||||||
const props = createProps({
|
MediaTapToView.args = {
|
||||||
text: '',
|
text: '',
|
||||||
isViewOnce: true,
|
isViewOnce: true,
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: AUDIO_MP3,
|
contentType: AUDIO_MP3,
|
||||||
fileName: 'great-video.mp3',
|
fileName: 'great-video.mp3',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MediaTapToView.story = {
|
MediaTapToView.story = {
|
||||||
name: 'Media Tap-to-View',
|
name: 'Media Tap-to-View',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OtherFileAttachment = (): JSX.Element => {
|
export const OtherFileAttachment = Template.bind({});
|
||||||
const props = createProps({
|
OtherFileAttachment.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: stringToMIMEType('application/json'),
|
contentType: stringToMIMEType('application/json'),
|
||||||
fileName: 'great-data.json',
|
fileName: 'great-data.json',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LongMessageAttachmentShouldBeHidden = (): JSX.Element => {
|
export const LongMessageAttachmentShouldBeHidden = Template.bind({});
|
||||||
const props = createProps({
|
LongMessageAttachmentShouldBeHidden.args = {
|
||||||
rawAttachment: {
|
rawAttachment: {
|
||||||
contentType: LONG_MESSAGE,
|
contentType: LONG_MESSAGE,
|
||||||
fileName: 'signal-long-message-123.txt',
|
fileName: 'signal-long-message-123.txt',
|
||||||
isVoiceMessage: false,
|
isVoiceMessage: false,
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
LongMessageAttachmentShouldBeHidden.story = {
|
LongMessageAttachmentShouldBeHidden.story = {
|
||||||
name: 'Long message attachment (should be hidden)',
|
name: 'Long message attachment (should be hidden)',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoCloseButton = (): JSX.Element => {
|
export const NoCloseButton = Template.bind({});
|
||||||
const props = createProps();
|
NoCloseButton.args = {
|
||||||
props.onClose = undefined;
|
onClose: undefined,
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MessageNotFound = (): JSX.Element => {
|
export const MessageNotFound = TemplateInMessage.bind({});
|
||||||
const props = createProps({
|
MessageNotFound.args = {
|
||||||
referencedMessageNotFound: true,
|
referencedMessageNotFound: true,
|
||||||
});
|
|
||||||
|
|
||||||
return renderInMessage(props);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MissingTextAttachment = (): JSX.Element => {
|
export const MissingTextAttachment = Template.bind({});
|
||||||
const props = createProps();
|
MissingTextAttachment.args = {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
text: undefined,
|
||||||
props.text = undefined as any;
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MissingTextAttachment.story = {
|
MissingTextAttachment.story = {
|
||||||
name: 'Missing Text & Attachment',
|
name: 'Missing Text & Attachment',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MentionOutgoingAnotherAuthor = (): JSX.Element => {
|
export const MentionOutgoingAnotherAuthor = Template.bind({});
|
||||||
const props = createProps({
|
MentionOutgoingAnotherAuthor.args = {
|
||||||
authorTitle: 'Tony Stark',
|
authorTitle: 'Tony Stark',
|
||||||
text: '@Captain America Lunch later?',
|
text: '@Captain America Lunch later?',
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MentionOutgoingAnotherAuthor.story = {
|
MentionOutgoingAnotherAuthor.story = {
|
||||||
name: '@mention + outgoing + another author',
|
name: '@mention + outgoing + another author',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MentionOutgoingMe = (): JSX.Element => {
|
export const MentionOutgoingMe = Template.bind({});
|
||||||
const props = createProps({
|
MentionOutgoingMe.args = {
|
||||||
isFromMe: true,
|
isFromMe: true,
|
||||||
text: '@Captain America Lunch later?',
|
text: '@Captain America Lunch later?',
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MentionOutgoingMe.story = {
|
MentionOutgoingMe.story = {
|
||||||
name: '@mention + outgoing + me',
|
name: '@mention + outgoing + me',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MentionIncomingAnotherAuthor = (): JSX.Element => {
|
export const MentionIncomingAnotherAuthor = Template.bind({});
|
||||||
const props = createProps({
|
MentionIncomingAnotherAuthor.args = {
|
||||||
authorTitle: 'Captain America',
|
authorTitle: 'Captain America',
|
||||||
isIncoming: true,
|
isIncoming: true,
|
||||||
text: '@Tony Stark sure',
|
text: '@Tony Stark sure',
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MentionIncomingAnotherAuthor.story = {
|
MentionIncomingAnotherAuthor.story = {
|
||||||
name: '@mention + incoming + another author',
|
name: '@mention + incoming + another author',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MentionIncomingMe = (): JSX.Element => {
|
export const MentionIncomingMe = Template.bind({});
|
||||||
const props = createProps({
|
MentionIncomingMe.args = {
|
||||||
isFromMe: true,
|
isFromMe: true,
|
||||||
isIncoming: true,
|
isIncoming: true,
|
||||||
text: '@Tony Stark sure',
|
text: '@Tony Stark sure',
|
||||||
});
|
|
||||||
|
|
||||||
return <Quote {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MentionIncomingMe.story = {
|
MentionIncomingMe.story = {
|
||||||
name: '@mention + incoming + me',
|
name: '@mention + incoming + me',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomColor = (): JSX.Element => (
|
export const CustomColor = (args: Props): JSX.Element => (
|
||||||
<>
|
<>
|
||||||
<Quote
|
<Quote
|
||||||
{...createProps({ isIncoming: true, text: 'Solid + Gradient' })}
|
{...args}
|
||||||
customColor={{
|
customColor={{
|
||||||
start: { hue: 82, saturation: 35 },
|
start: { hue: 82, saturation: 35 },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Quote
|
<Quote
|
||||||
{...createProps()}
|
{...args}
|
||||||
|
isIncoming={false}
|
||||||
|
text="A gradient"
|
||||||
customColor={{
|
customColor={{
|
||||||
deg: 192,
|
deg: 192,
|
||||||
start: { hue: 304, saturation: 85 },
|
start: { hue: 304, saturation: 85 },
|
||||||
|
@ -604,59 +518,48 @@ export const CustomColor = (): JSX.Element => (
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
CustomColor.args = {
|
||||||
export const IsStoryReply = (): JSX.Element => {
|
isIncoming: true,
|
||||||
const props = createProps({
|
text: 'Solid + Gradient',
|
||||||
text: 'Wow!',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Quote
|
|
||||||
{...props}
|
|
||||||
authorTitle="Amanda"
|
|
||||||
isStoryReply
|
|
||||||
moduleClassName="StoryReplyQuote"
|
|
||||||
onClose={undefined}
|
|
||||||
rawAttachment={{
|
|
||||||
contentType: VIDEO_MP4,
|
|
||||||
fileName: 'great-video.mp4',
|
|
||||||
isVoiceMessage: false,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const IsStoryReply = Template.bind({});
|
||||||
|
IsStoryReply.args = {
|
||||||
|
text: 'Wow!',
|
||||||
|
authorTitle: 'Amanda',
|
||||||
|
isStoryReply: true,
|
||||||
|
moduleClassName: 'StoryReplyQuote',
|
||||||
|
onClose: undefined,
|
||||||
|
rawAttachment: {
|
||||||
|
contentType: VIDEO_MP4,
|
||||||
|
fileName: 'great-video.mp4',
|
||||||
|
isVoiceMessage: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
IsStoryReply.story = {
|
IsStoryReply.story = {
|
||||||
name: 'isStoryReply',
|
name: 'isStoryReply',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IsStoryReplyEmoji = (): JSX.Element => {
|
export const IsStoryReplyEmoji = Template.bind({});
|
||||||
const props = createProps();
|
IsStoryReplyEmoji.args = {
|
||||||
|
authorTitle: getDefaultConversation().firstName,
|
||||||
return (
|
isStoryReply: true,
|
||||||
<Quote
|
moduleClassName: 'StoryReplyQuote',
|
||||||
{...props}
|
onClose: undefined,
|
||||||
authorTitle="Charlie"
|
rawAttachment: {
|
||||||
isStoryReply
|
contentType: IMAGE_PNG,
|
||||||
moduleClassName="StoryReplyQuote"
|
fileName: 'sax.png',
|
||||||
onClose={undefined}
|
isVoiceMessage: false,
|
||||||
rawAttachment={{
|
thumbnail: {
|
||||||
contentType: IMAGE_PNG,
|
contentType: IMAGE_PNG,
|
||||||
fileName: 'sax.png',
|
height: 100,
|
||||||
isVoiceMessage: false,
|
width: 100,
|
||||||
thumbnail: {
|
path: pngUrl,
|
||||||
contentType: IMAGE_PNG,
|
objectUrl: pngUrl,
|
||||||
height: 100,
|
},
|
||||||
width: 100,
|
},
|
||||||
path: pngUrl,
|
reactionEmoji: '🏋️',
|
||||||
objectUrl: pngUrl,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
reactionEmoji="🏋️"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
IsStoryReplyEmoji.story = {
|
IsStoryReplyEmoji.story = {
|
||||||
name: 'isStoryReply emoji',
|
name: 'isStoryReply emoji',
|
||||||
};
|
};
|
||||||
|
|
|
@ -418,6 +418,8 @@ const actions = () => ({
|
||||||
|
|
||||||
peekGroupCallForTheFirstTime: action('peekGroupCallForTheFirstTime'),
|
peekGroupCallForTheFirstTime: action('peekGroupCallForTheFirstTime'),
|
||||||
peekGroupCallIfItHasMembers: action('peekGroupCallIfItHasMembers'),
|
peekGroupCallIfItHasMembers: action('peekGroupCallIfItHasMembers'),
|
||||||
|
|
||||||
|
viewStory: action('viewStory'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = ({
|
const renderItem = ({
|
||||||
|
|
|
@ -267,6 +267,8 @@ const getActions = createSelector(
|
||||||
'downloadNewVersion',
|
'downloadNewVersion',
|
||||||
|
|
||||||
'contactSupport',
|
'contactSupport',
|
||||||
|
|
||||||
|
'viewStory',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const safe: AssertProps<PropsActionsType, typeof unsafe> = unsafe;
|
const safe: AssertProps<PropsActionsType, typeof unsafe> = unsafe;
|
||||||
|
|
|
@ -107,6 +107,7 @@ const getDefaultProps = () => ({
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
renderReactionPicker,
|
renderReactionPicker,
|
||||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||||
|
viewStory: action('viewStory'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ThunkAction } from 'redux-thunk';
|
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
|
||||||
import { isEqual, pick } from 'lodash';
|
import { isEqual, 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';
|
||||||
|
@ -18,6 +18,7 @@ import * as log from '../../logging/log';
|
||||||
import dataInterface from '../../sql/Client';
|
import dataInterface from '../../sql/Client';
|
||||||
import { DAY } from '../../util/durations';
|
import { DAY } from '../../util/durations';
|
||||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
|
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
|
||||||
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
|
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
|
||||||
import { UUID } from '../../types/UUID';
|
import { UUID } from '../../types/UUID';
|
||||||
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
|
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
|
||||||
|
@ -32,11 +33,12 @@ import {
|
||||||
isDownloaded,
|
isDownloaded,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
} from '../../types/Attachment';
|
} from '../../types/Attachment';
|
||||||
|
import { getConversationSelector } from '../selectors/conversations';
|
||||||
|
import { getStories } from '../selectors/stories';
|
||||||
|
import { isGroup } from '../../util/whatTypeOfConversation';
|
||||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
||||||
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
||||||
import { isGroup } from '../../util/whatTypeOfConversation';
|
|
||||||
import { getConversationSelector } from '../selectors/conversations';
|
|
||||||
|
|
||||||
export type StoryDataType = {
|
export type StoryDataType = {
|
||||||
attachment?: AttachmentType;
|
attachment?: AttachmentType;
|
||||||
|
@ -56,6 +58,12 @@ export type StoryDataType = {
|
||||||
| 'type'
|
| 'type'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type SelectedStoryDataType = {
|
||||||
|
currentIndex: number;
|
||||||
|
numStories: number;
|
||||||
|
story: StoryDataType;
|
||||||
|
};
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
export type StoriesStateType = {
|
export type StoriesStateType = {
|
||||||
|
@ -64,7 +72,9 @@ export type StoriesStateType = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
replies: Array<MessageAttributesType>;
|
replies: Array<MessageAttributesType>;
|
||||||
};
|
};
|
||||||
|
readonly selectedStoryData?: SelectedStoryDataType;
|
||||||
readonly stories: Array<StoryDataType>;
|
readonly stories: Array<StoryDataType>;
|
||||||
|
readonly storyViewMode?: StoryViewModeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
@ -76,6 +86,7 @@ const REPLY_TO_STORY = 'stories/REPLY_TO_STORY';
|
||||||
export const RESOLVE_ATTACHMENT_URL = 'stories/RESOLVE_ATTACHMENT_URL';
|
export const RESOLVE_ATTACHMENT_URL = 'stories/RESOLVE_ATTACHMENT_URL';
|
||||||
const STORY_CHANGED = 'stories/STORY_CHANGED';
|
const STORY_CHANGED = 'stories/STORY_CHANGED';
|
||||||
const TOGGLE_VIEW = 'stories/TOGGLE_VIEW';
|
const TOGGLE_VIEW = 'stories/TOGGLE_VIEW';
|
||||||
|
const VIEW_STORY = 'stories/VIEW_STORY';
|
||||||
|
|
||||||
type DOEStoryActionType = {
|
type DOEStoryActionType = {
|
||||||
type: typeof DOE_STORY;
|
type: typeof DOE_STORY;
|
||||||
|
@ -117,6 +128,16 @@ type ToggleViewActionType = {
|
||||||
type: typeof TOGGLE_VIEW;
|
type: typeof TOGGLE_VIEW;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ViewStoryActionType = {
|
||||||
|
type: typeof VIEW_STORY;
|
||||||
|
payload:
|
||||||
|
| {
|
||||||
|
selectedStoryData: SelectedStoryDataType;
|
||||||
|
storyViewMode: StoryViewModeType;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export type StoriesActionType =
|
export type StoriesActionType =
|
||||||
| DOEStoryActionType
|
| DOEStoryActionType
|
||||||
| LoadStoryRepliesActionType
|
| LoadStoryRepliesActionType
|
||||||
|
@ -126,23 +147,11 @@ export type StoriesActionType =
|
||||||
| ReplyToStoryActionType
|
| ReplyToStoryActionType
|
||||||
| ResolveAttachmentUrlActionType
|
| ResolveAttachmentUrlActionType
|
||||||
| StoryChangedActionType
|
| StoryChangedActionType
|
||||||
| ToggleViewActionType;
|
| ToggleViewActionType
|
||||||
|
| ViewStoryActionType;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
deleteStoryForEveryone,
|
|
||||||
loadStoryReplies,
|
|
||||||
markStoryRead,
|
|
||||||
queueStoryDownload,
|
|
||||||
reactToStory,
|
|
||||||
replyToStory,
|
|
||||||
storyChanged,
|
|
||||||
toggleStoriesView,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useStoriesActions = (): typeof actions => useBoundActions(actions);
|
|
||||||
|
|
||||||
function deleteStoryForEveryone(
|
function deleteStoryForEveryone(
|
||||||
story: StoryViewType
|
story: StoryViewType
|
||||||
): ThunkAction<void, RootStateType, unknown, DOEStoryActionType> {
|
): ThunkAction<void, RootStateType, unknown, DOEStoryActionType> {
|
||||||
|
@ -414,6 +423,338 @@ function toggleStoriesView(): ToggleViewActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSelectedStoryDataForConversationId = (
|
||||||
|
dispatch: ThunkDispatch<
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
NoopActionType | ResolveAttachmentUrlActionType
|
||||||
|
>,
|
||||||
|
getState: () => RootStateType,
|
||||||
|
conversationId: string,
|
||||||
|
selectedStoryId?: string
|
||||||
|
): {
|
||||||
|
currentIndex: number;
|
||||||
|
hasUnread: boolean;
|
||||||
|
numStories: number;
|
||||||
|
storiesByConversationId: Array<StoryDataType>;
|
||||||
|
} => {
|
||||||
|
const state = getState();
|
||||||
|
const { stories } = state.stories;
|
||||||
|
|
||||||
|
const storiesByConversationId = stories.filter(
|
||||||
|
item => item.conversationId === conversationId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the index of the storyId provided, or if none provided then find the
|
||||||
|
// oldest unread story from the user. If all stories are read then we can
|
||||||
|
// start at the first story.
|
||||||
|
let currentIndex = 0;
|
||||||
|
let hasUnread = false;
|
||||||
|
storiesByConversationId.forEach((item, index) => {
|
||||||
|
if (selectedStoryId && item.messageId === selectedStoryId) {
|
||||||
|
currentIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!selectedStoryId &&
|
||||||
|
!currentIndex &&
|
||||||
|
item.readStatus === ReadStatus.Unread
|
||||||
|
) {
|
||||||
|
hasUnread = true;
|
||||||
|
currentIndex = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const numStories = storiesByConversationId.length;
|
||||||
|
|
||||||
|
// Queue all undownloaded stories once we're viewing someone's stories
|
||||||
|
storiesByConversationId.forEach(item => {
|
||||||
|
if (isDownloaded(item.attachment) || isDownloading(item.attachment)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueStoryDownload(item.messageId)(dispatch, getState, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentIndex,
|
||||||
|
hasUnread,
|
||||||
|
numStories,
|
||||||
|
storiesByConversationId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function viewUserStories(
|
||||||
|
conversationId: string
|
||||||
|
): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { currentIndex, hasUnread, numStories, storiesByConversationId } =
|
||||||
|
getSelectedStoryDataForConversationId(dispatch, getState, conversationId);
|
||||||
|
|
||||||
|
const story = storiesByConversationId[currentIndex];
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: VIEW_STORY,
|
||||||
|
payload: {
|
||||||
|
selectedStoryData: {
|
||||||
|
currentIndex,
|
||||||
|
numStories,
|
||||||
|
story,
|
||||||
|
},
|
||||||
|
storyViewMode: hasUnread
|
||||||
|
? StoryViewModeType.Unread
|
||||||
|
: StoryViewModeType.All,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ViewStoryActionCreatorType = (
|
||||||
|
storyId?: string,
|
||||||
|
storyViewMode?: StoryViewModeType,
|
||||||
|
viewDirection?: StoryViewDirectionType
|
||||||
|
) => unknown;
|
||||||
|
|
||||||
|
const viewStory: ViewStoryActionCreatorType = (
|
||||||
|
storyId,
|
||||||
|
storyViewMode,
|
||||||
|
viewDirection
|
||||||
|
): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> => {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!storyId || !storyViewMode) {
|
||||||
|
dispatch({
|
||||||
|
type: VIEW_STORY,
|
||||||
|
payload: undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
const { stories } = state.stories;
|
||||||
|
|
||||||
|
// Spec:
|
||||||
|
// When opening the story viewer you should always be taken to the oldest
|
||||||
|
// un viewed story of the user you tapped on
|
||||||
|
// If all stories from a user are viewed, opening the viewer should take
|
||||||
|
// you to their oldest story
|
||||||
|
|
||||||
|
const story = stories.find(item => item.messageId === storyId);
|
||||||
|
|
||||||
|
if (!story) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentIndex, numStories, storiesByConversationId } =
|
||||||
|
getSelectedStoryDataForConversationId(
|
||||||
|
dispatch,
|
||||||
|
getState,
|
||||||
|
story.conversationId,
|
||||||
|
storyId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Go directly to the storyId selected
|
||||||
|
if (!viewDirection) {
|
||||||
|
dispatch({
|
||||||
|
type: VIEW_STORY,
|
||||||
|
payload: {
|
||||||
|
selectedStoryData: {
|
||||||
|
currentIndex,
|
||||||
|
numStories,
|
||||||
|
story,
|
||||||
|
},
|
||||||
|
storyViewMode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next story within the same user's stories
|
||||||
|
if (
|
||||||
|
viewDirection === StoryViewDirectionType.Next &&
|
||||||
|
currentIndex < numStories - 1
|
||||||
|
) {
|
||||||
|
const nextIndex = currentIndex + 1;
|
||||||
|
const nextStory = storiesByConversationId[nextIndex];
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: VIEW_STORY,
|
||||||
|
payload: {
|
||||||
|
selectedStoryData: {
|
||||||
|
currentIndex: nextIndex,
|
||||||
|
numStories,
|
||||||
|
story: nextStory,
|
||||||
|
},
|
||||||
|
storyViewMode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prev story within the same user's stories
|
||||||
|
if (viewDirection === StoryViewDirectionType.Previous && currentIndex > 0) {
|
||||||
|
const nextIndex = currentIndex - 1;
|
||||||
|
const nextStory = storiesByConversationId[nextIndex];
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: VIEW_STORY,
|
||||||
|
payload: {
|
||||||
|
selectedStoryData: {
|
||||||
|
currentIndex: nextIndex,
|
||||||
|
numStories,
|
||||||
|
story: nextStory,
|
||||||
|
},
|
||||||
|
storyViewMode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are there any unviewed stories left? If so we should play the unviewed
|
||||||
|
// stories first. But only if we're going "next"
|
||||||
|
if (viewDirection === StoryViewDirectionType.Next) {
|
||||||
|
const unreadStory = stories.find(
|
||||||
|
item => item.readStatus === ReadStatus.Unread
|
||||||
|
);
|
||||||
|
if (unreadStory) {
|
||||||
|
const nextSelectedStoryData = getSelectedStoryDataForConversationId(
|
||||||
|
dispatch,
|
||||||
|
getState,
|
||||||
|
unreadStory.conversationId,
|
||||||
|
unreadStory.messageId
|
||||||
|
);
|
||||||
|
dispatch({
|
||||||
|
type: VIEW_STORY,
|
||||||
|
payload: {
|
||||||
|
selectedStoryData: {
|
||||||
|
currentIndex: nextSelectedStoryData.currentIndex,
|
||||||
|
numStories: nextSelectedStoryData.numStories,
|
||||||
|
story: unreadStory,
|
||||||
|
},
|
||||||
|
storyViewMode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversationStories = getStories(state).stories;
|
||||||
|
const conversationStoryIndex = conversationStories.findIndex(
|
||||||
|
item => item.conversationId === story.conversationId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conversationStoryIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the next user's stories
|
||||||
|
if (
|
||||||
|
viewDirection === StoryViewDirectionType.Next &&
|
||||||
|
conversationStoryIndex < conversationStories.length - 1
|
||||||
|
) {
|
||||||
|
// Spec:
|
||||||
|
// Tapping right advances you to the next un viewed story
|
||||||
|
// If all stories are viewed, advance to the next viewed story
|
||||||
|
// When you reach the newest story from a user, tapping right again
|
||||||
|
// should take you to the next user's oldest un viewed story or oldest
|
||||||
|
// story if all stories for the next user are viewed.
|
||||||
|
// When you reach the newest story from the last user in the story list,
|
||||||
|
// tapping right should close the viewer
|
||||||
|
// Touch area for tapping right should be 80% of width of the screen
|
||||||
|
const nextConversationStoryIndex = conversationStoryIndex + 1;
|
||||||
|
const conversationStory = conversationStories[nextConversationStoryIndex];
|
||||||
|
|
||||||
|
const nextSelectedStoryData = getSelectedStoryDataForConversationId(
|
||||||
|
dispatch,
|
||||||
|
getState,
|
||||||
|
conversationStory.conversationId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close the viewer if we were viewing unread stories only and we've
|
||||||
|
// reached the last unread story.
|
||||||
|
if (
|
||||||
|
!nextSelectedStoryData.hasUnread &&
|
||||||
|
storyViewMode === StoryViewModeType.Unread
|
||||||
|
) {
|
||||||
|
dispatch({
|
||||||
|
type: VIEW_STORY,
|
||||||
|
payload: undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: VIEW_STORY,
|
||||||
|
payload: {
|
||||||
|
selectedStoryData: {
|
||||||
|
currentIndex: 0,
|
||||||
|
numStories: nextSelectedStoryData.numStories,
|
||||||
|
story: nextSelectedStoryData.storiesByConversationId[0],
|
||||||
|
},
|
||||||
|
storyViewMode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the previous user's stories
|
||||||
|
if (
|
||||||
|
viewDirection === StoryViewDirectionType.Previous &&
|
||||||
|
conversationStoryIndex > 0
|
||||||
|
) {
|
||||||
|
// Spec:
|
||||||
|
// Tapping left takes you back to the previous story
|
||||||
|
// When you reach the oldest story from a user, tapping left again takes
|
||||||
|
// you to the previous users oldest un viewed story or newest viewed
|
||||||
|
// story if all stories are viewed
|
||||||
|
// If you tap left on the oldest story from the first user in the story
|
||||||
|
// list, it should re-start playback on that story
|
||||||
|
// Touch area for tapping left should be 20% of width of the screen
|
||||||
|
const nextConversationStoryIndex = conversationStoryIndex - 1;
|
||||||
|
const conversationStory = conversationStories[nextConversationStoryIndex];
|
||||||
|
|
||||||
|
const nextSelectedStoryData = getSelectedStoryDataForConversationId(
|
||||||
|
dispatch,
|
||||||
|
getState,
|
||||||
|
conversationStory.conversationId
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: VIEW_STORY,
|
||||||
|
payload: {
|
||||||
|
selectedStoryData: {
|
||||||
|
currentIndex: 0,
|
||||||
|
numStories: nextSelectedStoryData.numStories,
|
||||||
|
story: nextSelectedStoryData.storiesByConversationId[0],
|
||||||
|
},
|
||||||
|
storyViewMode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could not meet any criteria, close the viewer
|
||||||
|
dispatch({
|
||||||
|
type: VIEW_STORY,
|
||||||
|
payload: undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
deleteStoryForEveryone,
|
||||||
|
loadStoryReplies,
|
||||||
|
markStoryRead,
|
||||||
|
queueStoryDownload,
|
||||||
|
reactToStory,
|
||||||
|
replyToStory,
|
||||||
|
storyChanged,
|
||||||
|
toggleStoriesView,
|
||||||
|
viewUserStories,
|
||||||
|
viewStory,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStoriesActions = (): typeof actions => useBoundActions(actions);
|
||||||
|
|
||||||
// Reducer
|
// Reducer
|
||||||
|
|
||||||
export function getEmptyState(
|
export function getEmptyState(
|
||||||
|
@ -645,5 +986,15 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === VIEW_STORY) {
|
||||||
|
const { selectedStoryData, storyViewMode } = action.payload || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedStoryData,
|
||||||
|
storyViewMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -483,7 +483,7 @@ export const getPropsForStoryReplyContext = createSelectorCreator(
|
||||||
rawAttachment: storyReplyContext.attachment
|
rawAttachment: storyReplyContext.attachment
|
||||||
? processQuoteAttachment(storyReplyContext.attachment)
|
? processQuoteAttachment(storyReplyContext.attachment)
|
||||||
: undefined,
|
: undefined,
|
||||||
referencedMessageNotFound: !storyReplyContext.messageId,
|
storyId: storyReplyContext.messageId,
|
||||||
text: getStoryReplyText(window.i18n, storyReplyContext.attachment),
|
text: getStoryReplyText(window.i18n, storyReplyContext.attachment),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,7 +15,12 @@ import type {
|
||||||
StoryViewType,
|
StoryViewType,
|
||||||
} from '../../types/Stories';
|
} from '../../types/Stories';
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import type { StoryDataType, StoriesStateType } from '../ducks/stories';
|
import type {
|
||||||
|
SelectedStoryDataType,
|
||||||
|
StoryDataType,
|
||||||
|
StoriesStateType,
|
||||||
|
} from '../ducks/stories';
|
||||||
|
import { MY_STORIES_ID } from '../../types/Stories';
|
||||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
import { SendStatus } from '../../messages/MessageSendState';
|
import { SendStatus } from '../../messages/MessageSendState';
|
||||||
import { canReply } from './message';
|
import { canReply } from './message';
|
||||||
|
@ -25,7 +30,6 @@ import {
|
||||||
getMe,
|
getMe,
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
import { getDistributionListSelector } from './storyDistributionLists';
|
import { getDistributionListSelector } from './storyDistributionLists';
|
||||||
import { getUserConversationId } from './user';
|
|
||||||
|
|
||||||
export const getStoriesState = (state: StateType): StoriesStateType =>
|
export const getStoriesState = (state: StateType): StoriesStateType =>
|
||||||
state.stories;
|
state.stories;
|
||||||
|
@ -35,36 +39,35 @@ export const shouldShowStoriesView = createSelector(
|
||||||
({ isShowingStoriesView }): boolean => isShowingStoriesView
|
({ isShowingStoriesView }): boolean => isShowingStoriesView
|
||||||
);
|
);
|
||||||
|
|
||||||
function getNewestStory(x: ConversationStoryType | MyStoryType): StoryViewType {
|
export const getSelectedStoryData = createSelector(
|
||||||
return x.stories[x.stories.length - 1];
|
getStoriesState,
|
||||||
}
|
({ selectedStoryData }): SelectedStoryDataType | undefined =>
|
||||||
|
selectedStoryData
|
||||||
function sortByRecencyAndUnread(
|
);
|
||||||
a: ConversationStoryType | MyStoryType,
|
|
||||||
b: ConversationStoryType | MyStoryType
|
|
||||||
): number {
|
|
||||||
const storyA = getNewestStory(a);
|
|
||||||
const storyB = getNewestStory(b);
|
|
||||||
|
|
||||||
if (storyA.isUnread && storyB.isUnread) {
|
|
||||||
return storyA.timestamp > storyB.timestamp ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storyB.isUnread) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storyA.isUnread) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return storyA.timestamp > storyB.timestamp ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getReactionUniqueId(reaction: MessageReactionType): string {
|
function getReactionUniqueId(reaction: MessageReactionType): string {
|
||||||
return `${reaction.fromId}:${reaction.targetAuthorUuid}:${reaction.timestamp}`;
|
return `${reaction.fromId}:${reaction.targetAuthorUuid}:${reaction.timestamp}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortByRecencyAndUnread(
|
||||||
|
storyA: ConversationStoryType,
|
||||||
|
storyB: ConversationStoryType
|
||||||
|
): number {
|
||||||
|
if (storyA.storyView.isUnread && storyB.storyView.isUnread) {
|
||||||
|
return storyA.storyView.timestamp > storyB.storyView.timestamp ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storyB.storyView.isUnread) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storyA.storyView.isUnread) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return storyA.storyView.timestamp > storyB.storyView.timestamp ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
function getAvatarData(
|
function getAvatarData(
|
||||||
conversation: ConversationType
|
conversation: ConversationType
|
||||||
): Pick<
|
): Pick<
|
||||||
|
@ -90,10 +93,9 @@ function getAvatarData(
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoryView(
|
export function getStoryView(
|
||||||
conversationSelector: GetConversationByIdType,
|
conversationSelector: GetConversationByIdType,
|
||||||
story: StoryDataType,
|
story: StoryDataType
|
||||||
ourConversationId?: string
|
|
||||||
): StoryViewType {
|
): StoryViewType {
|
||||||
const sender = pick(conversationSelector(story.sourceUuid || story.source), [
|
const sender = pick(conversationSelector(story.sourceUuid || story.source), [
|
||||||
'acceptedMessageRequest',
|
'acceptedMessageRequest',
|
||||||
|
@ -113,7 +115,7 @@ function getStoryView(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attachment,
|
attachment,
|
||||||
canReply: canReply(story, ourConversationId, conversationSelector),
|
canReply: canReply(story, undefined, conversationSelector),
|
||||||
isUnread: story.readStatus === ReadStatus.Unread,
|
isUnread: story.readStatus === ReadStatus.Unread,
|
||||||
messageId: story.messageId,
|
messageId: story.messageId,
|
||||||
sender,
|
sender,
|
||||||
|
@ -121,10 +123,9 @@ function getStoryView(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConversationStory(
|
export function getConversationStory(
|
||||||
conversationSelector: GetConversationByIdType,
|
conversationSelector: GetConversationByIdType,
|
||||||
story: StoryDataType,
|
story: StoryDataType
|
||||||
ourConversationId?: string
|
|
||||||
): ConversationStoryType {
|
): ConversationStoryType {
|
||||||
const sender = pick(conversationSelector(story.sourceUuid || story.source), [
|
const sender = pick(conversationSelector(story.sourceUuid || story.source), [
|
||||||
'hideStory',
|
'hideStory',
|
||||||
|
@ -142,59 +143,16 @@ function getConversationStory(
|
||||||
'title',
|
'title',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const storyView = getStoryView(
|
const storyView = getStoryView(conversationSelector, story);
|
||||||
conversationSelector,
|
|
||||||
story,
|
|
||||||
ourConversationId
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
group: conversation.id !== sender.id ? conversation : undefined,
|
group: conversation.id !== sender.id ? conversation : undefined,
|
||||||
isHidden: Boolean(sender.hideStory),
|
isHidden: Boolean(sender.hideStory),
|
||||||
stories: [storyView],
|
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(
|
export const getStoryReplies = createSelector(
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
getContactNameColorSelector,
|
getContactNameColorSelector,
|
||||||
|
@ -262,13 +220,11 @@ export const getStories = createSelector(
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
getDistributionListSelector,
|
getDistributionListSelector,
|
||||||
getStoriesState,
|
getStoriesState,
|
||||||
getUserConversationId,
|
|
||||||
shouldShowStoriesView,
|
shouldShowStoriesView,
|
||||||
(
|
(
|
||||||
conversationSelector,
|
conversationSelector,
|
||||||
distributionListSelector,
|
distributionListSelector,
|
||||||
{ stories }: Readonly<StoriesStateType>,
|
{ stories }: Readonly<StoriesStateType>,
|
||||||
ourConversationId,
|
|
||||||
isShowingStoriesView
|
isShowingStoriesView
|
||||||
): {
|
): {
|
||||||
hiddenStories: Array<ConversationStoryType>;
|
hiddenStories: Array<ConversationStoryType>;
|
||||||
|
@ -293,16 +249,16 @@ export const getStories = createSelector(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (story.sendStateByConversationId && story.storyDistributionListId) {
|
if (story.sendStateByConversationId && story.storyDistributionListId) {
|
||||||
const list = distributionListSelector(story.storyDistributionListId);
|
const list =
|
||||||
|
story.storyDistributionListId === MY_STORIES_ID
|
||||||
|
? { id: MY_STORIES_ID, name: MY_STORIES_ID }
|
||||||
|
: distributionListSelector(story.storyDistributionListId);
|
||||||
|
|
||||||
if (!list) {
|
if (!list) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storyView = getStoryView(
|
const storyView = getStoryView(conversationSelector, story);
|
||||||
conversationSelector,
|
|
||||||
story,
|
|
||||||
ourConversationId
|
|
||||||
);
|
|
||||||
|
|
||||||
const sendState: Array<StorySendStateType> = [];
|
const sendState: Array<StorySendStateType> = [];
|
||||||
const { sendStateByConversationId } = story;
|
const { sendStateByConversationId } = story;
|
||||||
|
@ -352,8 +308,7 @@ export const getStories = createSelector(
|
||||||
|
|
||||||
const conversationStory = getConversationStory(
|
const conversationStory = getConversationStory(
|
||||||
conversationSelector,
|
conversationSelector,
|
||||||
story,
|
story
|
||||||
ourConversationId
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let storiesMap: Map<string, ConversationStoryType>;
|
let storiesMap: Map<string, ConversationStoryType>;
|
||||||
|
@ -366,25 +321,18 @@ export const getStories = createSelector(
|
||||||
|
|
||||||
const existingConversationStory = storiesMap.get(
|
const existingConversationStory = storiesMap.get(
|
||||||
conversationStory.conversationId
|
conversationStory.conversationId
|
||||||
) || { stories: [] };
|
);
|
||||||
|
|
||||||
storiesMap.set(conversationStory.conversationId, {
|
storiesMap.set(conversationStory.conversationId, {
|
||||||
...existingConversationStory,
|
...existingConversationStory,
|
||||||
...conversationStory,
|
...conversationStory,
|
||||||
stories: [
|
storyView: conversationStory.storyView,
|
||||||
...existingConversationStory.stories,
|
|
||||||
...conversationStory.stories,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hiddenStories: Array.from(hiddenStoriesById.values()).sort(
|
hiddenStories: Array.from(hiddenStoriesById.values()),
|
||||||
sortByRecencyAndUnread
|
myStories: Array.from(myStoriesById.values()),
|
||||||
),
|
|
||||||
myStories: Array.from(myStoriesById.values()).sort(
|
|
||||||
sortByRecencyAndUnread
|
|
||||||
),
|
|
||||||
stories: Array.from(storiesById.values()).sort(sortByRecencyAndUnread),
|
stories: Array.from(storiesById.values()).sort(sortByRecencyAndUnread),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { SmartGlobalModalContainer } from './GlobalModalContainer';
|
||||||
import { SmartLeftPane } from './LeftPane';
|
import { SmartLeftPane } from './LeftPane';
|
||||||
import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
|
import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
|
||||||
import { SmartStories } from './Stories';
|
import { SmartStories } from './Stories';
|
||||||
|
import { SmartStoryViewer } from './StoryViewer';
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||||
import {
|
import {
|
||||||
|
@ -23,7 +24,10 @@ import {
|
||||||
getIsMainWindowFullScreen,
|
getIsMainWindowFullScreen,
|
||||||
getMenuOptions,
|
getMenuOptions,
|
||||||
} from '../selectors/user';
|
} from '../selectors/user';
|
||||||
import { shouldShowStoriesView } from '../selectors/stories';
|
import {
|
||||||
|
getSelectedStoryData,
|
||||||
|
shouldShowStoriesView,
|
||||||
|
} from '../selectors/stories';
|
||||||
import { getHideMenuBar } from '../selectors/items';
|
import { getHideMenuBar } from '../selectors/items';
|
||||||
import { getConversationsStoppingSend } from '../selectors/conversations';
|
import { getConversationsStoppingSend } from '../selectors/conversations';
|
||||||
import { getIsCustomizingPreferredReactions } from '../selectors/preferredReactions';
|
import { getIsCustomizingPreferredReactions } from '../selectors/preferredReactions';
|
||||||
|
@ -54,6 +58,8 @@ const mapStateToProps = (state: StateType) => {
|
||||||
),
|
),
|
||||||
isShowingStoriesView: shouldShowStoriesView(state),
|
isShowingStoriesView: shouldShowStoriesView(state),
|
||||||
renderStories: () => <SmartStories />,
|
renderStories: () => <SmartStories />,
|
||||||
|
selectedStoryData: getSelectedStoryData(state),
|
||||||
|
renderStoryViewer: () => <SmartStoryViewer />,
|
||||||
requestVerification: (
|
requestVerification: (
|
||||||
type: 'sms' | 'voice',
|
type: 'sms' | 'voice',
|
||||||
number: string,
|
number: string,
|
||||||
|
|
|
@ -7,12 +7,10 @@ import { useSelector } from 'react-redux';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import type { PropsType as SmartStoryCreatorPropsType } from './StoryCreator';
|
import type { PropsType as SmartStoryCreatorPropsType } from './StoryCreator';
|
||||||
import type { PropsType as SmartStoryViewerPropsType } from './StoryViewer';
|
|
||||||
import { SmartStoryCreator } from './StoryCreator';
|
import { SmartStoryCreator } from './StoryCreator';
|
||||||
import { SmartStoryViewer } from './StoryViewer';
|
|
||||||
import { Stories } from '../../components/Stories';
|
import { Stories } from '../../components/Stories';
|
||||||
import { getMe } from '../selectors/conversations';
|
import { getMe } from '../selectors/conversations';
|
||||||
import { getIntl, getUserConversationId } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getPreferredLeftPaneWidth } from '../selectors/items';
|
import { getPreferredLeftPaneWidth } from '../selectors/items';
|
||||||
import { getStories } from '../selectors/stories';
|
import { getStories } from '../selectors/stories';
|
||||||
import { saveAttachment } from '../../util/saveAttachment';
|
import { saveAttachment } from '../../util/saveAttachment';
|
||||||
|
@ -26,24 +24,6 @@ function renderStoryCreator({
|
||||||
return <SmartStoryCreator onClose={onClose} />;
|
return <SmartStoryCreator onClose={onClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStoryViewer({
|
|
||||||
conversationId,
|
|
||||||
onClose,
|
|
||||||
onNextUserStories,
|
|
||||||
onPrevUserStories,
|
|
||||||
storyToView,
|
|
||||||
}: SmartStoryViewerPropsType): JSX.Element {
|
|
||||||
return (
|
|
||||||
<SmartStoryViewer
|
|
||||||
conversationId={conversationId}
|
|
||||||
onClose={onClose}
|
|
||||||
onNextUserStories={onNextUserStories}
|
|
||||||
onPrevUserStories={onPrevUserStories}
|
|
||||||
storyToView={storyToView}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SmartStories(): JSX.Element | null {
|
export function SmartStories(): JSX.Element | null {
|
||||||
const storiesActions = useStoriesActions();
|
const storiesActions = useStoriesActions();
|
||||||
const { showConversation, toggleHideStories } = useConversationsActions();
|
const { showConversation, toggleHideStories } = useConversationsActions();
|
||||||
|
@ -61,7 +41,6 @@ export function SmartStories(): JSX.Element | null {
|
||||||
|
|
||||||
const { hiddenStories, myStories, stories } = useSelector(getStories);
|
const { hiddenStories, myStories, stories } = useSelector(getStories);
|
||||||
|
|
||||||
const ourConversationId = useSelector(getUserConversationId);
|
|
||||||
const me = useSelector(getMe);
|
const me = useSelector(getMe);
|
||||||
|
|
||||||
if (!isShowingStoriesView) {
|
if (!isShowingStoriesView) {
|
||||||
|
@ -82,10 +61,8 @@ export function SmartStories(): JSX.Element | null {
|
||||||
saveAttachment(story.attachment, story.timestamp);
|
saveAttachment(story.attachment, story.timestamp);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
ourConversationId={String(ourConversationId)}
|
|
||||||
preferredWidthFromStorage={preferredWidthFromStorage}
|
preferredWidthFromStorage={preferredWidthFromStorage}
|
||||||
renderStoryCreator={renderStoryCreator}
|
renderStoryCreator={renderStoryCreator}
|
||||||
renderStoryViewer={renderStoryViewer}
|
|
||||||
showConversation={showConversation}
|
showConversation={showConversation}
|
||||||
stories={stories}
|
stories={stories}
|
||||||
toggleHideStories={toggleHideStories}
|
toggleHideStories={toggleHideStories}
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
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 { GetConversationByIdType } from '../selectors/conversations';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
import type { StoryViewModeType } from '../../types/Stories';
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import type { StoryViewType } from '../../types/Stories';
|
import type { SelectedStoryDataType } from '../ducks/stories';
|
||||||
import { StoryViewer } from '../../components/StoryViewer';
|
import { StoryViewer } from '../../components/StoryViewer';
|
||||||
import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong';
|
import { ToastMessageBodyTooLong } from '../../components/ToastMessageBodyTooLong';
|
||||||
|
import { getConversationSelector } from '../selectors/conversations';
|
||||||
import {
|
import {
|
||||||
getEmojiSkinTone,
|
getEmojiSkinTone,
|
||||||
getHasAllStoriesMuted,
|
getHasAllStoriesMuted,
|
||||||
|
@ -17,30 +19,22 @@ import {
|
||||||
} from '../selectors/items';
|
} from '../selectors/items';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||||
import { getStoriesSelector, getStoryReplies } from '../selectors/stories';
|
import {
|
||||||
|
getConversationStory,
|
||||||
|
getSelectedStoryData,
|
||||||
|
getStoryReplies,
|
||||||
|
getStoryView,
|
||||||
|
} from '../selectors/stories';
|
||||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||||
import { showToast } from '../../util/showToast';
|
import { showToast } from '../../util/showToast';
|
||||||
|
import { strictAssert } from '../../util/assert';
|
||||||
import { useActions as useEmojisActions } from '../ducks/emojis';
|
import { useActions as useEmojisActions } from '../ducks/emojis';
|
||||||
import { useActions as useItemsActions } from '../ducks/items';
|
import { useActions as useItemsActions } from '../ducks/items';
|
||||||
import { useConversationsActions } from '../ducks/conversations';
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
import { useRecentEmojis } from '../selectors/emojis';
|
import { useRecentEmojis } from '../selectors/emojis';
|
||||||
import { useStoriesActions } from '../ducks/stories';
|
import { useStoriesActions } from '../ducks/stories';
|
||||||
|
|
||||||
export type PropsType = {
|
export function SmartStoryViewer(): JSX.Element | null {
|
||||||
conversationId: string;
|
|
||||||
onClose: () => unknown;
|
|
||||||
onNextUserStories?: () => unknown;
|
|
||||||
onPrevUserStories?: () => unknown;
|
|
||||||
storyToView?: StoryViewType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SmartStoryViewer({
|
|
||||||
conversationId,
|
|
||||||
onClose,
|
|
||||||
onNextUserStories,
|
|
||||||
onPrevUserStories,
|
|
||||||
storyToView,
|
|
||||||
}: PropsType): JSX.Element | null {
|
|
||||||
const storiesActions = useStoriesActions();
|
const storiesActions = useStoriesActions();
|
||||||
const { onSetSkinTone, toggleHasAllStoriesMuted } = useItemsActions();
|
const { onSetSkinTone, toggleHasAllStoriesMuted } = useItemsActions();
|
||||||
const { onUseEmoji } = useEmojisActions();
|
const { onUseEmoji } = useEmojisActions();
|
||||||
|
@ -52,14 +46,25 @@ export function SmartStoryViewer({
|
||||||
getPreferredReactionEmoji
|
getPreferredReactionEmoji
|
||||||
);
|
);
|
||||||
|
|
||||||
const getStoriesByConversationId = useSelector<
|
const selectedStoryData = useSelector<
|
||||||
StateType,
|
StateType,
|
||||||
GetStoriesByConversationIdType
|
SelectedStoryDataType | undefined
|
||||||
>(getStoriesSelector);
|
>(getSelectedStoryData);
|
||||||
|
|
||||||
const { group, stories } = storyToView
|
strictAssert(selectedStoryData, 'StoryViewer: !selectedStoryData');
|
||||||
? { group: undefined, stories: [storyToView] }
|
|
||||||
: getStoriesByConversationId(conversationId);
|
const conversationSelector = useSelector<StateType, GetConversationByIdType>(
|
||||||
|
getConversationSelector
|
||||||
|
);
|
||||||
|
|
||||||
|
const storyView = getStoryView(conversationSelector, selectedStoryData.story);
|
||||||
|
const conversationStory = getConversationStory(
|
||||||
|
conversationSelector,
|
||||||
|
selectedStoryData.story
|
||||||
|
);
|
||||||
|
const storyViewMode = useSelector<StateType, StoryViewModeType | undefined>(
|
||||||
|
state => state.stories.storyViewMode
|
||||||
|
);
|
||||||
|
|
||||||
const recentEmojis = useRecentEmojis();
|
const recentEmojis = useRecentEmojis();
|
||||||
const skinTone = useSelector<StateType, number>(getEmojiSkinTone);
|
const skinTone = useSelector<StateType, number>(getEmojiSkinTone);
|
||||||
|
@ -70,26 +75,24 @@ export function SmartStoryViewer({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StoryViewer
|
<StoryViewer
|
||||||
conversationId={conversationId}
|
currentIndex={selectedStoryData.currentIndex}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
group={group}
|
group={conversationStory.group}
|
||||||
hasAllStoriesMuted={hasAllStoriesMuted}
|
hasAllStoriesMuted={hasAllStoriesMuted}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={onClose}
|
numStories={selectedStoryData.numStories}
|
||||||
onHideStory={toggleHideStories}
|
onHideStory={toggleHideStories}
|
||||||
onGoToConversation={senderId => {
|
onGoToConversation={senderId => {
|
||||||
showConversation({ conversationId: senderId });
|
showConversation({ conversationId: senderId });
|
||||||
storiesActions.toggleStoriesView();
|
storiesActions.toggleStoriesView();
|
||||||
}}
|
}}
|
||||||
onNextUserStories={onNextUserStories}
|
|
||||||
onPrevUserStories={onPrevUserStories}
|
|
||||||
onReactToStory={async (emoji, story) => {
|
onReactToStory={async (emoji, story) => {
|
||||||
const { messageId } = story;
|
const { messageId } = story;
|
||||||
storiesActions.reactToStory(emoji, messageId);
|
storiesActions.reactToStory(emoji, messageId);
|
||||||
}}
|
}}
|
||||||
onReplyToStory={(message, mentions, timestamp, story) => {
|
onReplyToStory={(message, mentions, timestamp, story) => {
|
||||||
storiesActions.replyToStory(
|
storiesActions.replyToStory(
|
||||||
conversationId,
|
conversationStory.conversationId,
|
||||||
message,
|
message,
|
||||||
mentions,
|
mentions,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
@ -103,8 +106,9 @@ export function SmartStoryViewer({
|
||||||
recentEmojis={recentEmojis}
|
recentEmojis={recentEmojis}
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
replyState={replyState}
|
replyState={replyState}
|
||||||
stories={stories}
|
|
||||||
skinTone={skinTone}
|
skinTone={skinTone}
|
||||||
|
story={storyView}
|
||||||
|
storyViewMode={storyViewMode}
|
||||||
toggleHasAllStoriesMuted={toggleHasAllStoriesMuted}
|
toggleHasAllStoriesMuted={toggleHasAllStoriesMuted}
|
||||||
{...storiesActions}
|
{...storiesActions}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -69,6 +69,6 @@ export function getFakeStory({
|
||||||
return {
|
return {
|
||||||
conversationId: storyView.sender.id,
|
conversationId: storyView.sender.id,
|
||||||
group,
|
group,
|
||||||
stories: [storyView],
|
storyView,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1936,7 +1936,7 @@ export default class MessageReceiver
|
||||||
distributionListToSentUuid.forEach((sentToUuids, listId) => {
|
distributionListToSentUuid.forEach((sentToUuids, listId) => {
|
||||||
const ev = new SentEvent(
|
const ev = new SentEvent(
|
||||||
{
|
{
|
||||||
destinationUuid: dropNull(sentMessage.destinationUuid),
|
destinationUuid: envelope.destinationUuid.toString(),
|
||||||
timestamp: envelope.timestamp,
|
timestamp: envelope.timestamp,
|
||||||
serverTimestamp: envelope.serverTimestamp,
|
serverTimestamp: envelope.serverTimestamp,
|
||||||
unidentifiedStatus: Array.from(sentToUuids).map(
|
unidentifiedStatus: Array.from(sentToUuids).map(
|
||||||
|
|
|
@ -45,7 +45,7 @@ export type ConversationStoryType = {
|
||||||
>;
|
>;
|
||||||
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
|
||||||
stories: Array<StoryViewType>;
|
storyView: StoryViewType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StorySendStateType = {
|
export type StorySendStateType = {
|
||||||
|
@ -99,3 +99,14 @@ export type MyStoryType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MY_STORIES_ID = '00000000-0000-0000-0000-000000000000';
|
export const MY_STORIES_ID = '00000000-0000-0000-0000-000000000000';
|
||||||
|
|
||||||
|
export enum StoryViewDirectionType {
|
||||||
|
Next = 'Next',
|
||||||
|
Previous = 'Previous',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum StoryViewModeType {
|
||||||
|
Unread = 'Unread',
|
||||||
|
All = 'All',
|
||||||
|
Single = 'Single',
|
||||||
|
}
|
||||||
|
|
|
@ -9286,13 +9286,6 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2022-04-29T23:54:21.656Z"
|
"updated": "2022-04-29T23:54:21.656Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/StoryViewer.tsx",
|
|
||||||
"line": " const storiesRef = useRef(stories);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2022-04-30T00:44:47.213Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/StoryViewsNRepliesModal.tsx",
|
"path": "ts/components/StoryViewsNRepliesModal.tsx",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue