Sending/Failed state for stories
This commit is contained in:
parent
9bad2301fd
commit
220963c789
22 changed files with 676 additions and 190 deletions
|
@ -5543,6 +5543,14 @@
|
||||||
"message": "Send failed",
|
"message": "Send failed",
|
||||||
"description": "Error text for story failed to send in list view"
|
"description": "Error text for story failed to send in list view"
|
||||||
},
|
},
|
||||||
|
"Stories__list--partially-sent": {
|
||||||
|
"message": "Partially sent",
|
||||||
|
"description": "Error text for story failed partially to send"
|
||||||
|
},
|
||||||
|
"Stories__list--retry-send": {
|
||||||
|
"message": "Click to retry",
|
||||||
|
"description": "Actionable link to retry a send"
|
||||||
|
},
|
||||||
"Stories__placeholder--text": {
|
"Stories__placeholder--text": {
|
||||||
"message": "Click to view a story",
|
"message": "Click to view a story",
|
||||||
"description": "Placeholder label for the story view"
|
"description": "Placeholder label for the story view"
|
||||||
|
@ -5563,6 +5571,10 @@
|
||||||
"message": "This story has no sound",
|
"message": "This story has no sound",
|
||||||
"description": "Toast message"
|
"description": "Toast message"
|
||||||
},
|
},
|
||||||
|
"Stories__failed-send": {
|
||||||
|
"message": "This story could not be sent to some people. Check your connection and try again.",
|
||||||
|
"description": "Alert error message when unable to send a story"
|
||||||
|
},
|
||||||
"StoriesSettings__title": {
|
"StoriesSettings__title": {
|
||||||
"message": "Story privacy",
|
"message": "Story privacy",
|
||||||
"description": "Title for the story settings modal"
|
"description": "Title for the story settings modal"
|
||||||
|
@ -5867,6 +5879,18 @@
|
||||||
"message": "Views off",
|
"message": "Views off",
|
||||||
"description": "(deleted 2022/10/13) When the user has read receipts turned off"
|
"description": "(deleted 2022/10/13) When the user has read receipts turned off"
|
||||||
},
|
},
|
||||||
|
"StoryViewer__sending": {
|
||||||
|
"message": "Sending...",
|
||||||
|
"description": "Label for when a story is sending"
|
||||||
|
},
|
||||||
|
"StoryViewer__failed": {
|
||||||
|
"message": "Send failed. Click to retry",
|
||||||
|
"description": "Label for when a send failed"
|
||||||
|
},
|
||||||
|
"StoryViewer__partial-fail": {
|
||||||
|
"message": "Partially sent. Click to retry",
|
||||||
|
"description": "Label for when a send partially failed"
|
||||||
|
},
|
||||||
"StoryDetailsModal__sent-time": {
|
"StoryDetailsModal__sent-time": {
|
||||||
"message": "Sent $time$",
|
"message": "Sent $time$",
|
||||||
"description": "Sent timestamp"
|
"description": "Sent timestamp"
|
||||||
|
|
|
@ -37,6 +37,29 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
|
|
||||||
|
&__failed {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
width: 12px;
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/error-outline-24.svg',
|
||||||
|
$color-accent-red
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
@include button-reset;
|
||||||
|
@include font-subtitle;
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__timestamp {
|
&__timestamp {
|
||||||
|
|
|
@ -141,6 +141,26 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
|
|
||||||
|
&__failed {
|
||||||
|
@include button-reset;
|
||||||
|
@include font-body-1;
|
||||||
|
align-items: center;
|
||||||
|
color: $color-white;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 18px;
|
||||||
|
margin-right: 12px;
|
||||||
|
width: 18px;
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v2/error-outline-24.svg',
|
||||||
|
$color-accent-red
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__reply {
|
&__reply {
|
||||||
|
@ -358,4 +378,14 @@
|
||||||
height: 256px;
|
height: 256px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__sending {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&__spinner__container {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import type { PropsType } from './MyStories';
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
import { MY_STORY_ID } from '../types/Stories';
|
import { MY_STORY_ID } from '../types/Stories';
|
||||||
import { MyStories } from './MyStories';
|
import { MyStories } from './MyStories';
|
||||||
|
import { SendStatus } from '../messages/MessageSendState';
|
||||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||||
import { getFakeMyStory } from '../test-both/helpers/getFakeStory';
|
import { getFakeMyStory } from '../test-both/helpers/getFakeStory';
|
||||||
import { setupI18n } from '../util/setupI18n';
|
import { setupI18n } from '../util/setupI18n';
|
||||||
|
@ -48,7 +49,7 @@ export default {
|
||||||
queueStoryDownload: {
|
queueStoryDownload: {
|
||||||
action: true,
|
action: true,
|
||||||
},
|
},
|
||||||
renderStoryViewer: {
|
retrySend: {
|
||||||
action: true,
|
action: true,
|
||||||
},
|
},
|
||||||
viewStory: { action: true },
|
viewStory: { action: true },
|
||||||
|
@ -108,3 +109,30 @@ MultiListStories.play = interactionTest;
|
||||||
MultiListStories.story = {
|
MultiListStories.story = {
|
||||||
name: 'Multiple distribution lists',
|
name: 'Multiple distribution lists',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const FailedSentStory = Template.bind({});
|
||||||
|
{
|
||||||
|
const myStory = getFakeMyStory(MY_STORY_ID);
|
||||||
|
FailedSentStory.args = {
|
||||||
|
myStories: [
|
||||||
|
{
|
||||||
|
...myStory,
|
||||||
|
stories: myStory.stories.map((story, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return {
|
||||||
|
...story,
|
||||||
|
sendState: [
|
||||||
|
{
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
status: SendStatus.Failed,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return story;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
getFakeMyStory(uuid(), 'Cool Peeps'),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -3,16 +3,21 @@
|
||||||
|
|
||||||
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 { StoryViewTargetType, StoryViewModeType } from '../types/Stories';
|
import {
|
||||||
|
ResolvedSendStatus,
|
||||||
|
StoryViewTargetType,
|
||||||
|
StoryViewModeType,
|
||||||
|
} from '../types/Stories';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import { ContextMenu } from './ContextMenu';
|
import { ContextMenu } from './ContextMenu';
|
||||||
|
|
||||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
|
import { resolveStorySendStatus } from '../util/resolveStorySendStatus';
|
||||||
|
import { useRetryStorySend } from '../hooks/useRetryStorySend';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -22,6 +27,7 @@ export type PropsType = {
|
||||||
onForward: (storyId: string) => unknown;
|
onForward: (storyId: string) => unknown;
|
||||||
onSave: (story: StoryViewType) => unknown;
|
onSave: (story: StoryViewType) => unknown;
|
||||||
queueStoryDownload: (storyId: string) => unknown;
|
queueStoryDownload: (storyId: string) => unknown;
|
||||||
|
retrySend: (messageId: string) => unknown;
|
||||||
viewStory: ViewStoryActionCreatorType;
|
viewStory: ViewStoryActionCreatorType;
|
||||||
hasViewReceiptSetting: boolean;
|
hasViewReceiptSetting: boolean;
|
||||||
};
|
};
|
||||||
|
@ -34,6 +40,7 @@ export const MyStories = ({
|
||||||
onForward,
|
onForward,
|
||||||
onSave,
|
onSave,
|
||||||
queueStoryDownload,
|
queueStoryDownload,
|
||||||
|
retrySend,
|
||||||
viewStory,
|
viewStory,
|
||||||
hasViewReceiptSetting,
|
hasViewReceiptSetting,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
|
@ -81,89 +88,18 @@ export const MyStories = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{list.stories.map(story => (
|
{list.stories.map(story => (
|
||||||
<div className="MyStories__story" key={story.timestamp}>
|
<StorySent
|
||||||
<button
|
hasViewReceiptSetting={hasViewReceiptSetting}
|
||||||
aria-label={i18n('MyStories__story')}
|
i18n={i18n}
|
||||||
className="StoryListItem__button MyStories__story-button"
|
key={story.messageId}
|
||||||
onClick={() =>
|
onForward={onForward}
|
||||||
viewStory({
|
onSave={onSave}
|
||||||
storyId: story.messageId,
|
queueStoryDownload={queueStoryDownload}
|
||||||
storyViewMode: StoryViewModeType.MyStories,
|
retrySend={retrySend}
|
||||||
})
|
setConfirmDeleteStory={setConfirmDeleteStory}
|
||||||
}
|
story={story}
|
||||||
type="button"
|
viewStory={viewStory}
|
||||||
>
|
/>
|
||||||
<div className="StoryListItem__previews">
|
|
||||||
<StoryImage
|
|
||||||
attachment={story.attachment}
|
|
||||||
firstName={i18n('you')}
|
|
||||||
i18n={i18n}
|
|
||||||
isMe
|
|
||||||
isThumbnail
|
|
||||||
label={i18n('MyStories__story')}
|
|
||||||
moduleClassName="StoryListItem__previews--image"
|
|
||||||
queueStoryDownload={queueStoryDownload}
|
|
||||||
storyId={story.messageId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="MyStories__story__details">
|
|
||||||
{hasViewReceiptSetting
|
|
||||||
? i18n('icu:MyStories__views', {
|
|
||||||
views: story.views ?? 0,
|
|
||||||
})
|
|
||||||
: i18n('icu:MyStories__views-off')}
|
|
||||||
<MessageTimestamp
|
|
||||||
i18n={i18n}
|
|
||||||
isRelativeTime
|
|
||||||
module="MyStories__story__timestamp"
|
|
||||||
timestamp={story.timestamp}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{story.attachment &&
|
|
||||||
(story.attachment.path || story.attachment.data) && (
|
|
||||||
<button
|
|
||||||
aria-label={i18n('MyStories__download')}
|
|
||||||
className="MyStories__story__download"
|
|
||||||
onClick={() => {
|
|
||||||
onSave(story);
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ContextMenu
|
|
||||||
i18n={i18n}
|
|
||||||
menuOptions={[
|
|
||||||
{
|
|
||||||
icon: 'MyStories__icon--forward',
|
|
||||||
label: i18n('forward'),
|
|
||||||
onClick: () => {
|
|
||||||
onForward(story.messageId);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'StoryListItem__icon--info',
|
|
||||||
label: i18n('StoryListItem__info'),
|
|
||||||
onClick: () => {
|
|
||||||
viewStory({
|
|
||||||
storyId: story.messageId,
|
|
||||||
storyViewMode: StoryViewModeType.MyStories,
|
|
||||||
viewTarget: StoryViewTargetType.Details,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'MyStories__icon--delete',
|
|
||||||
label: i18n('delete'),
|
|
||||||
onClick: () => {
|
|
||||||
setConfirmDeleteStory(story);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
moduleClassName="MyStories__story__more"
|
|
||||||
theme={Theme.Dark}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -176,3 +112,159 @@ export const MyStories = ({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StorySentPropsType = Pick<
|
||||||
|
PropsType,
|
||||||
|
| 'hasViewReceiptSetting'
|
||||||
|
| 'i18n'
|
||||||
|
| 'onForward'
|
||||||
|
| 'onSave'
|
||||||
|
| 'queueStoryDownload'
|
||||||
|
| 'retrySend'
|
||||||
|
| 'viewStory'
|
||||||
|
> & {
|
||||||
|
setConfirmDeleteStory: (_: StoryViewType | undefined) => unknown;
|
||||||
|
story: StoryViewType;
|
||||||
|
};
|
||||||
|
|
||||||
|
function StorySent({
|
||||||
|
hasViewReceiptSetting,
|
||||||
|
i18n,
|
||||||
|
onForward,
|
||||||
|
onSave,
|
||||||
|
queueStoryDownload,
|
||||||
|
retrySend,
|
||||||
|
setConfirmDeleteStory,
|
||||||
|
story,
|
||||||
|
viewStory,
|
||||||
|
}: StorySentPropsType): JSX.Element {
|
||||||
|
const sendStatus = resolveStorySendStatus(story.sendState ?? []);
|
||||||
|
const { renderAlert, setWasManuallyRetried, wasManuallyRetried } =
|
||||||
|
useRetryStorySend(i18n, sendStatus);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="MyStories__story" key={story.timestamp}>
|
||||||
|
{renderAlert()}
|
||||||
|
<button
|
||||||
|
aria-label={i18n('MyStories__story')}
|
||||||
|
className="StoryListItem__button MyStories__story-button"
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
!wasManuallyRetried &&
|
||||||
|
(sendStatus === ResolvedSendStatus.Failed ||
|
||||||
|
sendStatus === ResolvedSendStatus.PartiallySent)
|
||||||
|
) {
|
||||||
|
setWasManuallyRetried(true);
|
||||||
|
retrySend(story.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewStory({
|
||||||
|
storyId: story.messageId,
|
||||||
|
storyViewMode: StoryViewModeType.MyStories,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div className="StoryListItem__previews">
|
||||||
|
<StoryImage
|
||||||
|
attachment={story.attachment}
|
||||||
|
firstName={i18n('you')}
|
||||||
|
i18n={i18n}
|
||||||
|
isMe
|
||||||
|
isThumbnail
|
||||||
|
label={i18n('MyStories__story')}
|
||||||
|
moduleClassName="StoryListItem__previews--image"
|
||||||
|
queueStoryDownload={queueStoryDownload}
|
||||||
|
storyId={story.messageId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="MyStories__story__details">
|
||||||
|
{sendStatus === ResolvedSendStatus.Sending &&
|
||||||
|
i18n('Stories__list--sending')}
|
||||||
|
{sendStatus === ResolvedSendStatus.Failed && (
|
||||||
|
<div className="MyStories__story__details__failed">
|
||||||
|
<div>
|
||||||
|
{i18n('Stories__list--send_failed')}
|
||||||
|
{!wasManuallyRetried && (
|
||||||
|
<div className="MyStories__story__details__failed__button">
|
||||||
|
{i18n('Stories__list--retry-send')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sendStatus === ResolvedSendStatus.PartiallySent && (
|
||||||
|
<div className="MyStories__story__details__failed">
|
||||||
|
<div>
|
||||||
|
{i18n('Stories__list--partially-sent')}
|
||||||
|
{!wasManuallyRetried && (
|
||||||
|
<div className="MyStories__story__details__failed__button">
|
||||||
|
{i18n('Stories__list--retry-send')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sendStatus === ResolvedSendStatus.Sent && (
|
||||||
|
<>
|
||||||
|
{hasViewReceiptSetting
|
||||||
|
? i18n('icu:MyStories__views', {
|
||||||
|
views: story.views ?? 0,
|
||||||
|
})
|
||||||
|
: i18n('icu:MyStories__views-off')}
|
||||||
|
<MessageTimestamp
|
||||||
|
i18n={i18n}
|
||||||
|
isRelativeTime
|
||||||
|
module="MyStories__story__timestamp"
|
||||||
|
timestamp={story.timestamp}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{story.attachment && (story.attachment.path || story.attachment.data) && (
|
||||||
|
<button
|
||||||
|
aria-label={i18n('MyStories__download')}
|
||||||
|
className="MyStories__story__download"
|
||||||
|
onClick={() => {
|
||||||
|
onSave(story);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ContextMenu
|
||||||
|
i18n={i18n}
|
||||||
|
menuOptions={[
|
||||||
|
{
|
||||||
|
icon: 'MyStories__icon--forward',
|
||||||
|
label: i18n('forward'),
|
||||||
|
onClick: () => {
|
||||||
|
onForward(story.messageId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'StoryListItem__icon--info',
|
||||||
|
label: i18n('StoryListItem__info'),
|
||||||
|
onClick: () => {
|
||||||
|
viewStory({
|
||||||
|
storyId: story.messageId,
|
||||||
|
storyViewMode: StoryViewModeType.MyStories,
|
||||||
|
viewTarget: StoryViewTargetType.Details,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'MyStories__icon--delete',
|
||||||
|
label: i18n('delete'),
|
||||||
|
onClick: () => {
|
||||||
|
setConfirmDeleteStory(story);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
moduleClassName="MyStories__story__more"
|
||||||
|
theme={Theme.Dark}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -11,9 +11,10 @@ import type { PropsType } from './MyStoryButton';
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
import { MyStoryButton } from './MyStoryButton';
|
import { MyStoryButton } from './MyStoryButton';
|
||||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||||
import { getFakeStoryView } from '../test-both/helpers/getFakeStory';
|
import { getFakeMyStory } from '../test-both/helpers/getFakeStory';
|
||||||
import { setupI18n } from '../util/setupI18n';
|
import { setupI18n } from '../util/setupI18n';
|
||||||
import { SendStatus } from '../messages/MessageSendState';
|
import { SendStatus } from '../messages/MessageSendState';
|
||||||
|
import { ResolvedSendStatus } from '../types/Stories';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -21,18 +22,14 @@ export default {
|
||||||
title: 'Components/MyStoriesButton',
|
title: 'Components/MyStoriesButton',
|
||||||
component: MyStoryButton,
|
component: MyStoryButton,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
hasMultiple: {
|
|
||||||
control: 'checkbox',
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultValue: i18n,
|
defaultValue: i18n,
|
||||||
},
|
},
|
||||||
me: {
|
me: {
|
||||||
defaultValue: getDefaultConversation(),
|
defaultValue: getDefaultConversation(),
|
||||||
},
|
},
|
||||||
newestStory: {
|
myStories: {
|
||||||
defaultValue: getFakeStoryView(),
|
defaultValue: [getFakeMyStory()],
|
||||||
},
|
},
|
||||||
onAddStory: { action: true },
|
onAddStory: { action: true },
|
||||||
onClick: { action: true },
|
onClick: { action: true },
|
||||||
|
@ -60,8 +57,7 @@ const interactionTest: PlayFunction<ReactFramework, PropsType> = async ({
|
||||||
|
|
||||||
export const NoStory = Template.bind({});
|
export const NoStory = Template.bind({});
|
||||||
NoStory.args = {
|
NoStory.args = {
|
||||||
hasMultiple: false,
|
myStories: [],
|
||||||
newestStory: undefined,
|
|
||||||
};
|
};
|
||||||
NoStory.story = {
|
NoStory.story = {
|
||||||
name: 'No Story',
|
name: 'No Story',
|
||||||
|
@ -77,7 +73,7 @@ OneStory.play = interactionTest;
|
||||||
|
|
||||||
export const ManyStories = Template.bind({});
|
export const ManyStories = Template.bind({});
|
||||||
ManyStories.args = {
|
ManyStories.args = {
|
||||||
hasMultiple: true,
|
myStories: [getFakeMyStory(), getFakeMyStory()],
|
||||||
};
|
};
|
||||||
ManyStories.story = {
|
ManyStories.story = {
|
||||||
name: 'Many Stories',
|
name: 'Many Stories',
|
||||||
|
@ -88,32 +84,64 @@ export const SendingStory = Template.bind({});
|
||||||
SendingStory.story = {
|
SendingStory.story = {
|
||||||
name: 'Sending Story',
|
name: 'Sending Story',
|
||||||
};
|
};
|
||||||
SendingStory.args = {
|
{
|
||||||
newestStory: {
|
const myStory = getFakeMyStory();
|
||||||
...getFakeStoryView(),
|
SendingStory.args = {
|
||||||
sendState: [
|
myStories: [
|
||||||
{
|
{
|
||||||
status: SendStatus.Pending,
|
...myStory,
|
||||||
recipient: getDefaultConversation(),
|
reducedSendStatus: ResolvedSendStatus.Sending,
|
||||||
|
stories: myStory.stories.map((story, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return {
|
||||||
|
...story,
|
||||||
|
sendState: [
|
||||||
|
{
|
||||||
|
status: SendStatus.Pending,
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return story;
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
getFakeMyStory(),
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
};
|
}
|
||||||
SendingStory.play = interactionTest;
|
SendingStory.play = interactionTest;
|
||||||
|
|
||||||
export const FailedSendStory = Template.bind({});
|
export const FailedSendStory = Template.bind({});
|
||||||
FailedSendStory.story = {
|
FailedSendStory.story = {
|
||||||
name: 'Failed Send Story',
|
name: 'Failed Send Story',
|
||||||
};
|
};
|
||||||
FailedSendStory.args = {
|
{
|
||||||
newestStory: {
|
const myStory = getFakeMyStory();
|
||||||
...getFakeStoryView(),
|
FailedSendStory.args = {
|
||||||
sendState: [
|
myStories: [
|
||||||
{
|
{
|
||||||
status: SendStatus.Failed,
|
...myStory,
|
||||||
recipient: getDefaultConversation(),
|
reducedSendStatus: ResolvedSendStatus.Failed,
|
||||||
|
stories: myStory.stories.map((story, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return {
|
||||||
|
...story,
|
||||||
|
sendState: [
|
||||||
|
{
|
||||||
|
status: SendStatus.Failed,
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return story;
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
getFakeMyStory(),
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
};
|
}
|
||||||
FailedSendStory.play = interactionTest;
|
FailedSendStory.play = interactionTest;
|
||||||
|
|
|
@ -5,59 +5,34 @@ import React, { useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import type { MyStoryType, StoryViewType } from '../types/Stories';
|
||||||
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
|
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
|
||||||
import type { StorySendStateType, StoryViewType } from '../types/Stories';
|
|
||||||
import { Avatar, AvatarSize } from './Avatar';
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { HasStories } from '../types/Stories';
|
import { HasStories, ResolvedSendStatus } from '../types/Stories';
|
||||||
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||||
|
import { StoriesAddStoryButton } from './StoriesAddStoryButton';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
import { getAvatarColor } from '../types/Colors';
|
import { getAvatarColor } from '../types/Colors';
|
||||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
import { reduceStorySendStatus } from '../util/resolveStorySendStatus';
|
||||||
|
|
||||||
import { StoriesAddStoryButton } from './StoriesAddStoryButton';
|
|
||||||
import { isFailed, isPending } from '../messages/MessageSendState';
|
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
hasMultiple: boolean;
|
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
me: ConversationType;
|
me: ConversationType;
|
||||||
newestStory?: StoryViewType;
|
myStories: Array<MyStoryType>;
|
||||||
onAddStory: () => unknown;
|
onAddStory: () => unknown;
|
||||||
onClick: () => unknown;
|
onClick: () => unknown;
|
||||||
queueStoryDownload: (storyId: string) => unknown;
|
queueStoryDownload: (storyId: string) => unknown;
|
||||||
showToast: ShowToastActionCreatorType;
|
showToast: ShowToastActionCreatorType;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ResolvedSendStatus {
|
function getNewestMyStory(story: MyStoryType): StoryViewType {
|
||||||
Failed,
|
return story.stories[0];
|
||||||
Sending,
|
|
||||||
Sent,
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveSendStatus(
|
|
||||||
sendStates: Array<StorySendStateType>
|
|
||||||
): ResolvedSendStatus {
|
|
||||||
let anyPending = false;
|
|
||||||
for (const sendState of sendStates) {
|
|
||||||
if (isFailed(sendState.status)) {
|
|
||||||
// Immediately return if any send failed
|
|
||||||
return ResolvedSendStatus.Failed;
|
|
||||||
}
|
|
||||||
if (isPending(sendState.status)) {
|
|
||||||
// Don't return yet in case we have a failure
|
|
||||||
anyPending = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (anyPending) {
|
|
||||||
return ResolvedSendStatus.Sending;
|
|
||||||
}
|
|
||||||
return ResolvedSendStatus.Sent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MyStoryButton = ({
|
export const MyStoryButton = ({
|
||||||
hasMultiple,
|
|
||||||
i18n,
|
i18n,
|
||||||
me,
|
me,
|
||||||
newestStory,
|
myStories,
|
||||||
onAddStory,
|
onAddStory,
|
||||||
onClick,
|
onClick,
|
||||||
queueStoryDownload,
|
queueStoryDownload,
|
||||||
|
@ -65,6 +40,10 @@ export const MyStoryButton = ({
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
|
|
||||||
|
const newestStory = myStories.length
|
||||||
|
? getNewestMyStory(myStories[0])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
acceptedMessageRequest,
|
acceptedMessageRequest,
|
||||||
avatarPath,
|
avatarPath,
|
||||||
|
@ -113,8 +92,14 @@ export const MyStoryButton = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newStoryResolvedSendStatus = resolveSendStatus(
|
const hasMultiple = myStories.length
|
||||||
newestStory.sendState ?? []
|
? myStories[0].stories.length > 1
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const reducedSendStatus: ResolvedSendStatus = myStories.reduce(
|
||||||
|
(acc: ResolvedSendStatus, myStory) =>
|
||||||
|
reduceStorySendStatus(acc, myStory.reducedSendStatus),
|
||||||
|
ResolvedSendStatus.Sent
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -166,17 +151,22 @@ export const MyStoryButton = ({
|
||||||
<div className="StoryListItem__info--title StoryListItem__chevron">
|
<div className="StoryListItem__info--title StoryListItem__chevron">
|
||||||
{i18n('MyStories__list_item')}
|
{i18n('MyStories__list_item')}
|
||||||
</div>
|
</div>
|
||||||
{newStoryResolvedSendStatus === ResolvedSendStatus.Sending && (
|
{reducedSendStatus === ResolvedSendStatus.Sending && (
|
||||||
<span className="StoryListItem__info--sending">
|
<span className="StoryListItem__info--sending">
|
||||||
{i18n('Stories__list--sending')}
|
{i18n('Stories__list--sending')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{newStoryResolvedSendStatus === ResolvedSendStatus.Failed && (
|
{reducedSendStatus === ResolvedSendStatus.Failed && (
|
||||||
<span className="StoryListItem__info--send_failed">
|
<span className="StoryListItem__info--send_failed">
|
||||||
{i18n('Stories__list--send_failed')}
|
{i18n('Stories__list--send_failed')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{newStoryResolvedSendStatus === ResolvedSendStatus.Sent && (
|
{reducedSendStatus === ResolvedSendStatus.PartiallySent && (
|
||||||
|
<span className="StoryListItem__info--send_failed">
|
||||||
|
{i18n('Stories__list--partially-sent')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{reducedSendStatus === ResolvedSendStatus.Sent && (
|
||||||
<MessageTimestamp
|
<MessageTimestamp
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isRelativeTime
|
isRelativeTime
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default {
|
||||||
},
|
},
|
||||||
queueStoryDownload: { action: true },
|
queueStoryDownload: { action: true },
|
||||||
renderStoryCreator: { action: true },
|
renderStoryCreator: { action: true },
|
||||||
renderStoryViewer: { action: true },
|
retrySend: { action: true },
|
||||||
showConversation: { action: true },
|
showConversation: { action: true },
|
||||||
showStoriesSettings: { action: true },
|
showStoriesSettings: { action: true },
|
||||||
showToast: { action: true },
|
showToast: { action: true },
|
||||||
|
|
|
@ -42,6 +42,7 @@ export type PropsType = {
|
||||||
preferredWidthFromStorage: number;
|
preferredWidthFromStorage: number;
|
||||||
queueStoryDownload: (storyId: string) => unknown;
|
queueStoryDownload: (storyId: string) => unknown;
|
||||||
renderStoryCreator: () => JSX.Element;
|
renderStoryCreator: () => JSX.Element;
|
||||||
|
retrySend: (messageId: string) => unknown;
|
||||||
setAddStoryData: (data: AddStoryData) => unknown;
|
setAddStoryData: (data: AddStoryData) => unknown;
|
||||||
showConversation: ShowConversationType;
|
showConversation: ShowConversationType;
|
||||||
showStoriesSettings: () => unknown;
|
showStoriesSettings: () => unknown;
|
||||||
|
@ -69,6 +70,7 @@ export const Stories = ({
|
||||||
preferredWidthFromStorage,
|
preferredWidthFromStorage,
|
||||||
queueStoryDownload,
|
queueStoryDownload,
|
||||||
renderStoryCreator,
|
renderStoryCreator,
|
||||||
|
retrySend,
|
||||||
setAddStoryData,
|
setAddStoryData,
|
||||||
showConversation,
|
showConversation,
|
||||||
showStoriesSettings,
|
showStoriesSettings,
|
||||||
|
@ -101,6 +103,7 @@ export const Stories = ({
|
||||||
<div className="Stories__pane" style={{ width }}>
|
<div className="Stories__pane" style={{ width }}>
|
||||||
{isMyStories && myStories.length ? (
|
{isMyStories && myStories.length ? (
|
||||||
<MyStories
|
<MyStories
|
||||||
|
hasViewReceiptSetting={hasViewReceiptSetting}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
myStories={myStories}
|
myStories={myStories}
|
||||||
onBack={() => setIsMyStories(false)}
|
onBack={() => setIsMyStories(false)}
|
||||||
|
@ -108,8 +111,8 @@ export const Stories = ({
|
||||||
onForward={onForwardStory}
|
onForward={onForwardStory}
|
||||||
onSave={onSaveStory}
|
onSave={onSaveStory}
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
|
retrySend={retrySend}
|
||||||
viewStory={viewStory}
|
viewStory={viewStory}
|
||||||
hasViewReceiptSetting={hasViewReceiptSetting}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<StoriesPane
|
<StoriesPane
|
||||||
|
|
|
@ -9,11 +9,7 @@ import type {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
ShowConversationType,
|
ShowConversationType,
|
||||||
} from '../state/ducks/conversations';
|
} from '../state/ducks/conversations';
|
||||||
import type {
|
import type { ConversationStoryType, MyStoryType } from '../types/Stories';
|
||||||
ConversationStoryType,
|
|
||||||
MyStoryType,
|
|
||||||
StoryViewType,
|
|
||||||
} from '../types/Stories';
|
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||||
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
|
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
|
||||||
|
@ -59,10 +55,6 @@ function search(
|
||||||
.map(result => result.item);
|
.map(result => result.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNewestMyStory(story: MyStoryType): StoryViewType {
|
|
||||||
return story.stories[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
hiddenStories: Array<ConversationStoryType>;
|
hiddenStories: Array<ConversationStoryType>;
|
||||||
|
@ -161,14 +153,9 @@ export const StoriesPane = ({
|
||||||
<div className="Stories__pane__list">
|
<div className="Stories__pane__list">
|
||||||
<>
|
<>
|
||||||
<MyStoryButton
|
<MyStoryButton
|
||||||
hasMultiple={
|
|
||||||
myStories.length ? myStories[0].stories.length > 1 : false
|
|
||||||
}
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
me={me}
|
me={me}
|
||||||
newestStory={
|
myStories={myStories}
|
||||||
myStories.length ? getNewestMyStory(myStories[0]) : undefined
|
|
||||||
}
|
|
||||||
onAddStory={onAddStory}
|
onAddStory={onAddStory}
|
||||||
onClick={onMyStoriesClicked}
|
onClick={onMyStoriesClicked}
|
||||||
queueStoryDownload={queueStoryDownload}
|
queueStoryDownload={queueStoryDownload}
|
||||||
|
|
|
@ -56,6 +56,7 @@ export default {
|
||||||
},
|
},
|
||||||
queueStoryDownload: { action: true },
|
queueStoryDownload: { action: true },
|
||||||
renderEmojiPicker: { action: true },
|
renderEmojiPicker: { action: true },
|
||||||
|
retrySend: { action: true },
|
||||||
showToast: { action: true },
|
showToast: { action: true },
|
||||||
skinTone: {
|
skinTone: {
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
|
@ -191,6 +192,35 @@ export const YourStory = Template.bind({});
|
||||||
YourStory.storyName = 'Your story';
|
YourStory.storyName = 'Your story';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const YourStoryFailed = Template.bind({});
|
||||||
|
{
|
||||||
|
const storyView = getFakeStoryView(
|
||||||
|
'/fixtures/nathan-anderson-316188-unsplash.jpg'
|
||||||
|
);
|
||||||
|
|
||||||
|
YourStoryFailed.args = {
|
||||||
|
distributionList: { id: '123', name: 'Close Friends' },
|
||||||
|
story: {
|
||||||
|
...storyView,
|
||||||
|
sender: {
|
||||||
|
...storyView.sender,
|
||||||
|
isMe: true,
|
||||||
|
},
|
||||||
|
sendState: [
|
||||||
|
{
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
status: SendStatus.Viewed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipient: getDefaultConversation(),
|
||||||
|
status: SendStatus.Failed,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
YourStory.storyName = 'Your story';
|
||||||
|
}
|
||||||
|
|
||||||
export const ReadReceiptsOff = Template.bind({});
|
export const ReadReceiptsOff = Template.bind({});
|
||||||
{
|
{
|
||||||
const storyView = getFakeStoryView(
|
const storyView = getFakeStoryView(
|
||||||
|
|
|
@ -28,10 +28,12 @@ import { Emojify } from './conversation/Emojify';
|
||||||
import { Intl } from './Intl';
|
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 { Spinner } from './Spinner';
|
||||||
import { StoryDetailsModal } from './StoryDetailsModal';
|
import { StoryDetailsModal } from './StoryDetailsModal';
|
||||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
import {
|
import {
|
||||||
|
ResolvedSendStatus,
|
||||||
StoryViewDirectionType,
|
StoryViewDirectionType,
|
||||||
StoryViewModeType,
|
StoryViewModeType,
|
||||||
StoryViewTargetType,
|
StoryViewTargetType,
|
||||||
|
@ -45,10 +47,14 @@ import { getStoryDuration } from '../util/getStoryDuration';
|
||||||
import { graphemeAwareSlice } from '../util/graphemeAwareSlice';
|
import { graphemeAwareSlice } from '../util/graphemeAwareSlice';
|
||||||
import { isVideoAttachment } from '../types/Attachment';
|
import { isVideoAttachment } from '../types/Attachment';
|
||||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||||
|
import { useRetryStorySend } from '../hooks/useRetryStorySend';
|
||||||
|
import { resolveStorySendStatus } from '../util/resolveStorySendStatus';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
currentIndex: number;
|
currentIndex: number;
|
||||||
|
deleteGroupStoryReply: (id: string) => void;
|
||||||
|
deleteGroupStoryReplyForEveryone: (id: string) => void;
|
||||||
deleteStoryForEveryone: (story: StoryViewType) => unknown;
|
deleteStoryForEveryone: (story: StoryViewType) => unknown;
|
||||||
distributionList?: { id: string; name: string };
|
distributionList?: { id: string; name: string };
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
|
@ -70,6 +76,7 @@ export type PropsType = {
|
||||||
hasViewReceiptSetting: boolean;
|
hasViewReceiptSetting: boolean;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isSignalConversation?: boolean;
|
isSignalConversation?: boolean;
|
||||||
|
isWindowActive: boolean;
|
||||||
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
|
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
|
||||||
markStoryRead: (mId: string) => unknown;
|
markStoryRead: (mId: string) => unknown;
|
||||||
numStories: number;
|
numStories: number;
|
||||||
|
@ -90,16 +97,14 @@ export type PropsType = {
|
||||||
recentEmojis?: Array<string>;
|
recentEmojis?: Array<string>;
|
||||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
||||||
replyState?: ReplyStateType;
|
replyState?: ReplyStateType;
|
||||||
viewTarget?: StoryViewTargetType;
|
retrySend: (messageId: string) => unknown;
|
||||||
|
setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown;
|
||||||
showToast: ShowToastActionCreatorType;
|
showToast: ShowToastActionCreatorType;
|
||||||
skinTone?: number;
|
skinTone?: number;
|
||||||
story: StoryViewType;
|
story: StoryViewType;
|
||||||
storyViewMode: StoryViewModeType;
|
storyViewMode: StoryViewModeType;
|
||||||
setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown;
|
|
||||||
viewStory: ViewStoryActionCreatorType;
|
viewStory: ViewStoryActionCreatorType;
|
||||||
isWindowActive: boolean;
|
viewTarget?: StoryViewTargetType;
|
||||||
deleteGroupStoryReply: (id: string) => void;
|
|
||||||
deleteGroupStoryReplyForEveryone: (id: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CAPTION_BUFFER = 20;
|
const CAPTION_BUFFER = 20;
|
||||||
|
@ -115,6 +120,8 @@ enum Arrow {
|
||||||
|
|
||||||
export const StoryViewer = ({
|
export const StoryViewer = ({
|
||||||
currentIndex,
|
currentIndex,
|
||||||
|
deleteGroupStoryReply,
|
||||||
|
deleteGroupStoryReplyForEveryone,
|
||||||
deleteStoryForEveryone,
|
deleteStoryForEveryone,
|
||||||
distributionList,
|
distributionList,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
|
@ -124,6 +131,7 @@ export const StoryViewer = ({
|
||||||
hasViewReceiptSetting,
|
hasViewReceiptSetting,
|
||||||
i18n,
|
i18n,
|
||||||
isSignalConversation,
|
isSignalConversation,
|
||||||
|
isWindowActive,
|
||||||
loadStoryReplies,
|
loadStoryReplies,
|
||||||
markStoryRead,
|
markStoryRead,
|
||||||
numStories,
|
numStories,
|
||||||
|
@ -139,16 +147,14 @@ export const StoryViewer = ({
|
||||||
recentEmojis,
|
recentEmojis,
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
replyState,
|
replyState,
|
||||||
viewTarget,
|
retrySend,
|
||||||
|
setHasAllStoriesUnmuted,
|
||||||
showToast,
|
showToast,
|
||||||
skinTone,
|
skinTone,
|
||||||
story,
|
story,
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
setHasAllStoriesUnmuted,
|
|
||||||
viewStory,
|
viewStory,
|
||||||
isWindowActive,
|
viewTarget,
|
||||||
deleteGroupStoryReply,
|
|
||||||
deleteGroupStoryReplyForEveryone,
|
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
const [isShowingContextMenu, setIsShowingContextMenu] =
|
const [isShowingContextMenu, setIsShowingContextMenu] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
@ -181,6 +187,10 @@ export const StoryViewer = ({
|
||||||
|
|
||||||
const conversationId = group?.id || story.sender.id;
|
const conversationId = group?.id || story.sender.id;
|
||||||
|
|
||||||
|
const sendStatus = sendState ? resolveStorySendStatus(sendState) : undefined;
|
||||||
|
const { renderAlert, setWasManuallyRetried, wasManuallyRetried } =
|
||||||
|
useRetryStorySend(i18n, sendStatus);
|
||||||
|
|
||||||
const [currentViewTarget, setCurrentViewTarget] = useState(
|
const [currentViewTarget, setCurrentViewTarget] = useState(
|
||||||
viewTarget ?? null
|
viewTarget ?? null
|
||||||
);
|
);
|
||||||
|
@ -316,7 +326,10 @@ export const StoryViewer = ({
|
||||||
}
|
}
|
||||||
}, [isWindowActive]);
|
}, [isWindowActive]);
|
||||||
|
|
||||||
|
const alertElement = renderAlert();
|
||||||
|
|
||||||
const shouldPauseViewing =
|
const shouldPauseViewing =
|
||||||
|
Boolean(alertElement) ||
|
||||||
Boolean(confirmDeleteStory) ||
|
Boolean(confirmDeleteStory) ||
|
||||||
currentViewTarget != null ||
|
currentViewTarget != null ||
|
||||||
hasActiveCall ||
|
hasActiveCall ||
|
||||||
|
@ -528,9 +541,26 @@ export const StoryViewer = ({
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function doRetrySend() {
|
||||||
|
if (wasManuallyRetried) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
sendStatus !== ResolvedSendStatus.Failed &&
|
||||||
|
sendStatus !== ResolvedSendStatus.PartiallySent
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setWasManuallyRetried(true);
|
||||||
|
retrySend(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
|
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
|
||||||
<div className="StoryViewer">
|
<div className="StoryViewer">
|
||||||
|
{alertElement}
|
||||||
<div
|
<div
|
||||||
className="StoryViewer__overlay"
|
className="StoryViewer__overlay"
|
||||||
style={{ background: getStoryBackground(attachment) }}
|
style={{ background: getStoryBackground(attachment) }}
|
||||||
|
@ -753,7 +783,35 @@ export const StoryViewer = ({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="StoryViewer__actions">
|
<div className="StoryViewer__actions">
|
||||||
{(canReply || isSent) && (
|
{sendStatus === ResolvedSendStatus.Failed && !wasManuallyRetried && (
|
||||||
|
<button
|
||||||
|
className="StoryViewer__actions__failed"
|
||||||
|
onClick={doRetrySend}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{i18n('StoryViewer__failed')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{sendStatus === ResolvedSendStatus.PartiallySent &&
|
||||||
|
!wasManuallyRetried && (
|
||||||
|
<button
|
||||||
|
className="StoryViewer__actions__failed"
|
||||||
|
onClick={doRetrySend}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{i18n('StoryViewer__partial-fail')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{sendStatus === ResolvedSendStatus.Sending && (
|
||||||
|
<div className="StoryViewer__sending">
|
||||||
|
<Spinner
|
||||||
|
moduleClassName="StoryViewer__sending__spinner"
|
||||||
|
svgSize="small"
|
||||||
|
/>
|
||||||
|
{i18n('StoryViewer__sending')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sendStatus === ResolvedSendStatus.Sent && (canReply || isSent) && (
|
||||||
<button
|
<button
|
||||||
className="StoryViewer__reply"
|
className="StoryViewer__reply"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|
52
ts/hooks/useRetryStorySend.tsx
Normal file
52
ts/hooks/useRetryStorySend.tsx
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import { Alert } from '../components/Alert';
|
||||||
|
import { ResolvedSendStatus } from '../types/Stories';
|
||||||
|
import { usePrevious } from './usePrevious';
|
||||||
|
|
||||||
|
export function useRetryStorySend(
|
||||||
|
i18n: LocalizerType,
|
||||||
|
sendStatus: ResolvedSendStatus | undefined
|
||||||
|
): {
|
||||||
|
renderAlert: () => JSX.Element | null;
|
||||||
|
setWasManuallyRetried: (value: boolean) => unknown;
|
||||||
|
wasManuallyRetried: boolean;
|
||||||
|
} {
|
||||||
|
const [hasSendFailedAlert, setHasSendFailedAlert] = useState(false);
|
||||||
|
const [wasManuallyRetried, setWasManuallyRetried] = useState(false);
|
||||||
|
|
||||||
|
const previousSendStatus = usePrevious(sendStatus, sendStatus);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wasManuallyRetried) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousSendStatus === sendStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
sendStatus === ResolvedSendStatus.Failed ||
|
||||||
|
sendStatus === ResolvedSendStatus.PartiallySent
|
||||||
|
) {
|
||||||
|
setHasSendFailedAlert(true);
|
||||||
|
}
|
||||||
|
}, [previousSendStatus, sendStatus, wasManuallyRetried]);
|
||||||
|
|
||||||
|
function renderAlert(): JSX.Element | null {
|
||||||
|
return hasSendFailedAlert ? (
|
||||||
|
<Alert
|
||||||
|
body={i18n('Stories__failed-send')}
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => setHasSendFailedAlert(false)}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { renderAlert, setWasManuallyRetried, wasManuallyRetried };
|
||||||
|
}
|
|
@ -488,6 +488,10 @@ export async function sendStory(
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMe(recipient.attributes)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
if (recipient.isUnregistered()) {
|
if (recipient.isUnregistered()) {
|
||||||
if (!isSent(oldSendState.status)) {
|
if (!isSent(oldSendState.status)) {
|
||||||
// We should have filtered this out on initial send, but we'll drop them from
|
// We should have filtered this out on initial send, but we'll drop them from
|
||||||
|
|
|
@ -1415,8 +1415,29 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const conversation = this.getConversation()!;
|
const conversation = this.getConversation()!;
|
||||||
|
|
||||||
const currentConversationRecipients =
|
let currentConversationRecipients: Set<string> | undefined;
|
||||||
conversation.getMemberConversationIds();
|
|
||||||
|
const { storyDistributionListId } = this.attributes;
|
||||||
|
|
||||||
|
if (storyDistributionListId) {
|
||||||
|
const storyDistribution =
|
||||||
|
await dataInterface.getStoryDistributionWithMembers(
|
||||||
|
storyDistributionListId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!storyDistribution) {
|
||||||
|
this.markFailed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentConversationRecipients = new Set(
|
||||||
|
storyDistribution.members
|
||||||
|
.map(uuid => window.ConversationController.get(uuid)?.id)
|
||||||
|
.filter(isNotNil)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
currentConversationRecipients = conversation.getMemberConversationIds();
|
||||||
|
}
|
||||||
|
|
||||||
// Determine retry recipients and get their most up-to-date addressing information
|
// Determine retry recipients and get their most up-to-date addressing information
|
||||||
const oldSendStateByConversationId =
|
const oldSendStateByConversationId =
|
||||||
|
|
|
@ -22,7 +22,11 @@ import type {
|
||||||
StoriesStateType,
|
StoriesStateType,
|
||||||
AddStoryData,
|
AddStoryData,
|
||||||
} from '../ducks/stories';
|
} from '../ducks/stories';
|
||||||
import { HasStories, MY_STORY_ID } from '../../types/Stories';
|
import {
|
||||||
|
HasStories,
|
||||||
|
MY_STORY_ID,
|
||||||
|
ResolvedSendStatus,
|
||||||
|
} 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';
|
||||||
|
@ -39,6 +43,10 @@ import { calculateExpirationTimestamp } from '../../util/expirationTimer';
|
||||||
import { getMessageIdForLogging } from '../../util/idForLogging';
|
import { getMessageIdForLogging } from '../../util/idForLogging';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import { SIGNAL_ACI } from '../../types/SignalConversation';
|
import { SIGNAL_ACI } from '../../types/SignalConversation';
|
||||||
|
import {
|
||||||
|
reduceStorySendStatus,
|
||||||
|
resolveStorySendStatus,
|
||||||
|
} from '../../util/resolveStorySendStatus';
|
||||||
|
|
||||||
export const getStoriesState = (state: StateType): StoriesStateType =>
|
export const getStoriesState = (state: StateType): StoriesStateType =>
|
||||||
state.stories;
|
state.stories;
|
||||||
|
@ -406,12 +414,23 @@ export const getStories = createSelector(
|
||||||
story
|
story
|
||||||
);
|
);
|
||||||
|
|
||||||
const existingMyStory = myStoriesById.get(sentId) || { stories: [] };
|
const existingStorySent = myStoriesById.get(sentId) || {
|
||||||
|
// Default to "Sent" since it's the lowest form of SendStatus and all
|
||||||
|
// others take precedence over it.
|
||||||
|
reducedSendStatus: ResolvedSendStatus.Sent,
|
||||||
|
stories: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendStatus = resolveStorySendStatus(storyView.sendState ?? []);
|
||||||
|
|
||||||
myStoriesById.set(sentId, {
|
myStoriesById.set(sentId, {
|
||||||
id: sentId,
|
id: sentId,
|
||||||
name: sentName,
|
name: sentName,
|
||||||
stories: [storyView, ...existingMyStory.stories],
|
reducedSendStatus: reduceStorySendStatus(
|
||||||
|
existingStorySent.reducedSendStatus,
|
||||||
|
sendStatus
|
||||||
|
),
|
||||||
|
stories: [storyView, ...existingStorySent.stories],
|
||||||
});
|
});
|
||||||
|
|
||||||
// If it's a group story we still want it to render as part of regular
|
// If it's a group story we still want it to render as part of regular
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
getStories,
|
getStories,
|
||||||
shouldShowStoriesView,
|
shouldShowStoriesView,
|
||||||
} from '../selectors/stories';
|
} from '../selectors/stories';
|
||||||
|
import { retryMessageSend } from '../../util/retryMessageSend';
|
||||||
import { saveAttachment } from '../../util/saveAttachment';
|
import { saveAttachment } from '../../util/saveAttachment';
|
||||||
import { useConversationsActions } from '../ducks/conversations';
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||||
|
@ -84,6 +85,7 @@ export function SmartStories(): JSX.Element | null {
|
||||||
}}
|
}}
|
||||||
preferredWidthFromStorage={preferredWidthFromStorage}
|
preferredWidthFromStorage={preferredWidthFromStorage}
|
||||||
renderStoryCreator={renderStoryCreator}
|
renderStoryCreator={renderStoryCreator}
|
||||||
|
retrySend={retryMessageSend}
|
||||||
showConversation={showConversation}
|
showConversation={showConversation}
|
||||||
showStoriesSettings={showStoriesSettings}
|
showStoriesSettings={showStoriesSettings}
|
||||||
showToast={showToast}
|
showToast={showToast}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {
|
||||||
import { isInFullScreenCall } from '../selectors/calling';
|
import { isInFullScreenCall } from '../selectors/calling';
|
||||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||||
|
import { retryMessageSend } from '../../util/retryMessageSend';
|
||||||
import { strictAssert } from '../../util/assert';
|
import { strictAssert } from '../../util/assert';
|
||||||
import { useActions as useEmojisActions } from '../ducks/emojis';
|
import { useActions as useEmojisActions } from '../ducks/emojis';
|
||||||
import { useConversationsActions } from '../ducks/conversations';
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
|
@ -99,6 +100,7 @@ export function SmartStoryViewer(): JSX.Element | null {
|
||||||
isSignalConversation={isSignalConversation({
|
isSignalConversation={isSignalConversation({
|
||||||
id: conversationStory.conversationId,
|
id: conversationStory.conversationId,
|
||||||
})}
|
})}
|
||||||
|
isWindowActive={isWindowActive}
|
||||||
numStories={selectedStoryData.numStories}
|
numStories={selectedStoryData.numStories}
|
||||||
onHideStory={toggleHideStories}
|
onHideStory={toggleHideStories}
|
||||||
onGoToConversation={senderId => {
|
onGoToConversation={senderId => {
|
||||||
|
@ -125,12 +127,12 @@ export function SmartStoryViewer(): JSX.Element | null {
|
||||||
recentEmojis={recentEmojis}
|
recentEmojis={recentEmojis}
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
replyState={replyState}
|
replyState={replyState}
|
||||||
viewTarget={selectedStoryData.viewTarget}
|
retrySend={retryMessageSend}
|
||||||
showToast={showToast}
|
showToast={showToast}
|
||||||
skinTone={skinTone}
|
skinTone={skinTone}
|
||||||
story={storyView}
|
story={storyView}
|
||||||
storyViewMode={selectedStoryData.storyViewMode}
|
storyViewMode={selectedStoryData.storyViewMode}
|
||||||
isWindowActive={isWindowActive}
|
viewTarget={selectedStoryData.viewTarget}
|
||||||
{...storiesActions}
|
{...storiesActions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,12 +14,13 @@ import * as durations from '../../util/durations';
|
||||||
import { UUID } from '../../types/UUID';
|
import { UUID } from '../../types/UUID';
|
||||||
import { getDefaultConversation } from './getDefaultConversation';
|
import { getDefaultConversation } from './getDefaultConversation';
|
||||||
import { fakeAttachment, fakeThumbnail } from './fakeAttachment';
|
import { fakeAttachment, fakeThumbnail } from './fakeAttachment';
|
||||||
import { MY_STORY_ID } from '../../types/Stories';
|
import { MY_STORY_ID, ResolvedSendStatus } from '../../types/Stories';
|
||||||
|
|
||||||
function getAttachmentWithThumbnail(url: string): AttachmentType {
|
function getAttachmentWithThumbnail(url: string): AttachmentType {
|
||||||
return fakeAttachment({
|
return fakeAttachment({
|
||||||
url,
|
path: url,
|
||||||
thumbnail: fakeThumbnail(url),
|
thumbnail: fakeThumbnail(url),
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ export function getFakeMyStory(id?: string, name?: string): MyStoryType {
|
||||||
return {
|
return {
|
||||||
id: id || UUID.generate().toString(),
|
id: id || UUID.generate().toString(),
|
||||||
name: name || id === MY_STORY_ID ? 'My Stories' : casual.catch_phrase,
|
name: name || id === MY_STORY_ID ? 'My Stories' : casual.catch_phrase,
|
||||||
|
reducedSendStatus: ResolvedSendStatus.Sent,
|
||||||
stories: Array.from(Array(storyCount), () => ({
|
stories: Array.from(Array(storyCount), () => ({
|
||||||
...getFakeStoryView(),
|
...getFakeStoryView(),
|
||||||
sendState: [],
|
sendState: [],
|
||||||
|
|
|
@ -102,6 +102,7 @@ export type StoryViewType = {
|
||||||
export type MyStoryType = {
|
export type MyStoryType = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
reducedSendStatus: ResolvedSendStatus;
|
||||||
stories: Array<StoryViewType>;
|
stories: Array<StoryViewType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -158,3 +159,10 @@ export enum StorySendMode {
|
||||||
Always = 'Always',
|
Always = 'Always',
|
||||||
Never = 'Never',
|
Never = 'Never',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ResolvedSendStatus {
|
||||||
|
Failed = 'Failed',
|
||||||
|
PartiallySent = 'PartiallySent',
|
||||||
|
Sending = 'Sending',
|
||||||
|
Sent = 'Sent',
|
||||||
|
}
|
||||||
|
|
81
ts/util/resolveStorySendStatus.ts
Normal file
81
ts/util/resolveStorySendStatus.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { StorySendStateType } from '../types/Stories';
|
||||||
|
import { ResolvedSendStatus } from '../types/Stories';
|
||||||
|
import { isFailed, isPending, isSent } from '../messages/MessageSendState';
|
||||||
|
import { softAssert } from './assert';
|
||||||
|
|
||||||
|
export function resolveStorySendStatus(
|
||||||
|
sendStates: Array<StorySendStateType>
|
||||||
|
): ResolvedSendStatus {
|
||||||
|
let anyPending = false;
|
||||||
|
let anySent = false;
|
||||||
|
let anyFailed = false;
|
||||||
|
|
||||||
|
sendStates.forEach(({ status }) => {
|
||||||
|
if (isPending(status)) {
|
||||||
|
anyPending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSent(status)) {
|
||||||
|
anySent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFailed(status)) {
|
||||||
|
anyFailed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (anyPending) {
|
||||||
|
return ResolvedSendStatus.Sending;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyFailed && anySent) {
|
||||||
|
return ResolvedSendStatus.PartiallySent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyFailed && anySent) {
|
||||||
|
return ResolvedSendStatus.Sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyFailed && !anySent) {
|
||||||
|
return ResolvedSendStatus.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shouldn't get to this case but if none have been sent and none have failed
|
||||||
|
// then let's assume that we've sent.
|
||||||
|
softAssert(
|
||||||
|
anySent && sendStates.length,
|
||||||
|
'resolveStorySendStatus no sends, no failures, nothing pending?'
|
||||||
|
);
|
||||||
|
return ResolvedSendStatus.Sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reduceStorySendStatus(
|
||||||
|
currentSendStatus: ResolvedSendStatus,
|
||||||
|
nextSendStatus: ResolvedSendStatus
|
||||||
|
): ResolvedSendStatus {
|
||||||
|
if (
|
||||||
|
currentSendStatus === ResolvedSendStatus.Sending ||
|
||||||
|
nextSendStatus === ResolvedSendStatus.Sending
|
||||||
|
) {
|
||||||
|
return ResolvedSendStatus.Sending;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentSendStatus === ResolvedSendStatus.Failed ||
|
||||||
|
nextSendStatus === ResolvedSendStatus.Failed
|
||||||
|
) {
|
||||||
|
return ResolvedSendStatus.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentSendStatus === ResolvedSendStatus.PartiallySent ||
|
||||||
|
nextSendStatus === ResolvedSendStatus.PartiallySent
|
||||||
|
) {
|
||||||
|
return ResolvedSendStatus.PartiallySent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResolvedSendStatus.Sent;
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { getMessageById } from '../messages/getMessageById';
|
||||||
|
|
||||||
export async function retryMessageSend(messageId: string): Promise<void> {
|
export async function retryMessageSend(messageId: string): Promise<void> {
|
||||||
const message = window.MessageController.getById(messageId);
|
const message = await getMessageById(messageId);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
throw new Error(`retryMessageSend: Message ${messageId} missing!`);
|
throw new Error(`retryMessageSend: Message ${messageId} missing!`);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue