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",
|
||||
"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": {
|
||||
"message": "Click to view a story",
|
||||
"description": "Placeholder label for the story view"
|
||||
|
@ -5563,6 +5571,10 @@
|
|||
"message": "This story has no sound",
|
||||
"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": {
|
||||
"message": "Story privacy",
|
||||
"description": "Title for the story settings modal"
|
||||
|
@ -5867,6 +5879,18 @@
|
|||
"message": "Views 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": {
|
||||
"message": "Sent $time$",
|
||||
"description": "Sent timestamp"
|
||||
|
|
|
@ -37,6 +37,29 @@
|
|||
flex-direction: column;
|
||||
flex: 1;
|
||||
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 {
|
||||
|
|
|
@ -141,6 +141,26 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
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 {
|
||||
|
@ -358,4 +378,14 @@
|
|||
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 { MY_STORY_ID } from '../types/Stories';
|
||||
import { MyStories } from './MyStories';
|
||||
import { SendStatus } from '../messages/MessageSendState';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { getFakeMyStory } from '../test-both/helpers/getFakeStory';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
|
@ -48,7 +49,7 @@ export default {
|
|||
queueStoryDownload: {
|
||||
action: true,
|
||||
},
|
||||
renderStoryViewer: {
|
||||
retrySend: {
|
||||
action: true,
|
||||
},
|
||||
viewStory: { action: true },
|
||||
|
@ -108,3 +109,30 @@ MultiListStories.play = interactionTest;
|
|||
MultiListStories.story = {
|
||||
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 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 { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
|
||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||
import { StoryImage } from './StoryImage';
|
||||
import { Theme } from '../util/theme';
|
||||
import { resolveStorySendStatus } from '../util/resolveStorySendStatus';
|
||||
import { useRetryStorySend } from '../hooks/useRetryStorySend';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
|
@ -22,6 +27,7 @@ export type PropsType = {
|
|||
onForward: (storyId: string) => unknown;
|
||||
onSave: (story: StoryViewType) => unknown;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
retrySend: (messageId: string) => unknown;
|
||||
viewStory: ViewStoryActionCreatorType;
|
||||
hasViewReceiptSetting: boolean;
|
||||
};
|
||||
|
@ -34,6 +40,7 @@ export const MyStories = ({
|
|||
onForward,
|
||||
onSave,
|
||||
queueStoryDownload,
|
||||
retrySend,
|
||||
viewStory,
|
||||
hasViewReceiptSetting,
|
||||
}: PropsType): JSX.Element => {
|
||||
|
@ -81,89 +88,18 @@ export const MyStories = ({
|
|||
/>
|
||||
</div>
|
||||
{list.stories.map(story => (
|
||||
<div className="MyStories__story" key={story.timestamp}>
|
||||
<button
|
||||
aria-label={i18n('MyStories__story')}
|
||||
className="StoryListItem__button MyStories__story-button"
|
||||
onClick={() =>
|
||||
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">
|
||||
{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>
|
||||
<StorySent
|
||||
hasViewReceiptSetting={hasViewReceiptSetting}
|
||||
i18n={i18n}
|
||||
key={story.messageId}
|
||||
onForward={onForward}
|
||||
onSave={onSave}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
retrySend={retrySend}
|
||||
setConfirmDeleteStory={setConfirmDeleteStory}
|
||||
story={story}
|
||||
viewStory={viewStory}
|
||||
/>
|
||||
))}
|
||||
</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 { MyStoryButton } from './MyStoryButton';
|
||||
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 { SendStatus } from '../messages/MessageSendState';
|
||||
import { ResolvedSendStatus } from '../types/Stories';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -21,18 +22,14 @@ export default {
|
|||
title: 'Components/MyStoriesButton',
|
||||
component: MyStoryButton,
|
||||
argTypes: {
|
||||
hasMultiple: {
|
||||
control: 'checkbox',
|
||||
defaultValue: false,
|
||||
},
|
||||
i18n: {
|
||||
defaultValue: i18n,
|
||||
},
|
||||
me: {
|
||||
defaultValue: getDefaultConversation(),
|
||||
},
|
||||
newestStory: {
|
||||
defaultValue: getFakeStoryView(),
|
||||
myStories: {
|
||||
defaultValue: [getFakeMyStory()],
|
||||
},
|
||||
onAddStory: { action: true },
|
||||
onClick: { action: true },
|
||||
|
@ -60,8 +57,7 @@ const interactionTest: PlayFunction<ReactFramework, PropsType> = async ({
|
|||
|
||||
export const NoStory = Template.bind({});
|
||||
NoStory.args = {
|
||||
hasMultiple: false,
|
||||
newestStory: undefined,
|
||||
myStories: [],
|
||||
};
|
||||
NoStory.story = {
|
||||
name: 'No Story',
|
||||
|
@ -77,7 +73,7 @@ OneStory.play = interactionTest;
|
|||
|
||||
export const ManyStories = Template.bind({});
|
||||
ManyStories.args = {
|
||||
hasMultiple: true,
|
||||
myStories: [getFakeMyStory(), getFakeMyStory()],
|
||||
};
|
||||
ManyStories.story = {
|
||||
name: 'Many Stories',
|
||||
|
@ -88,32 +84,64 @@ export const SendingStory = Template.bind({});
|
|||
SendingStory.story = {
|
||||
name: 'Sending Story',
|
||||
};
|
||||
SendingStory.args = {
|
||||
newestStory: {
|
||||
...getFakeStoryView(),
|
||||
sendState: [
|
||||
{
|
||||
const myStory = getFakeMyStory();
|
||||
SendingStory.args = {
|
||||
myStories: [
|
||||
{
|
||||
status: SendStatus.Pending,
|
||||
recipient: getDefaultConversation(),
|
||||
...myStory,
|
||||
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;
|
||||
|
||||
export const FailedSendStory = Template.bind({});
|
||||
FailedSendStory.story = {
|
||||
name: 'Failed Send Story',
|
||||
};
|
||||
FailedSendStory.args = {
|
||||
newestStory: {
|
||||
...getFakeStoryView(),
|
||||
sendState: [
|
||||
{
|
||||
const myStory = getFakeMyStory();
|
||||
FailedSendStory.args = {
|
||||
myStories: [
|
||||
{
|
||||
status: SendStatus.Failed,
|
||||
recipient: getDefaultConversation(),
|
||||
...myStory,
|
||||
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;
|
||||
|
|
|
@ -5,59 +5,34 @@ import React, { useState } from 'react';
|
|||
import classNames from 'classnames';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { MyStoryType, StoryViewType } from '../types/Stories';
|
||||
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
|
||||
import type { StorySendStateType, StoryViewType } from '../types/Stories';
|
||||
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 { getAvatarColor } from '../types/Colors';
|
||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||
|
||||
import { StoriesAddStoryButton } from './StoriesAddStoryButton';
|
||||
import { isFailed, isPending } from '../messages/MessageSendState';
|
||||
import { reduceStorySendStatus } from '../util/resolveStorySendStatus';
|
||||
|
||||
export type PropsType = {
|
||||
hasMultiple: boolean;
|
||||
i18n: LocalizerType;
|
||||
me: ConversationType;
|
||||
newestStory?: StoryViewType;
|
||||
myStories: Array<MyStoryType>;
|
||||
onAddStory: () => unknown;
|
||||
onClick: () => unknown;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
showToast: ShowToastActionCreatorType;
|
||||
};
|
||||
|
||||
enum ResolvedSendStatus {
|
||||
Failed,
|
||||
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;
|
||||
function getNewestMyStory(story: MyStoryType): StoryViewType {
|
||||
return story.stories[0];
|
||||
}
|
||||
|
||||
export const MyStoryButton = ({
|
||||
hasMultiple,
|
||||
i18n,
|
||||
me,
|
||||
newestStory,
|
||||
myStories,
|
||||
onAddStory,
|
||||
onClick,
|
||||
queueStoryDownload,
|
||||
|
@ -65,6 +40,10 @@ export const MyStoryButton = ({
|
|||
}: PropsType): JSX.Element => {
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
const newestStory = myStories.length
|
||||
? getNewestMyStory(myStories[0])
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
|
@ -113,8 +92,14 @@ export const MyStoryButton = ({
|
|||
);
|
||||
}
|
||||
|
||||
const newStoryResolvedSendStatus = resolveSendStatus(
|
||||
newestStory.sendState ?? []
|
||||
const hasMultiple = myStories.length
|
||||
? myStories[0].stories.length > 1
|
||||
: false;
|
||||
|
||||
const reducedSendStatus: ResolvedSendStatus = myStories.reduce(
|
||||
(acc: ResolvedSendStatus, myStory) =>
|
||||
reduceStorySendStatus(acc, myStory.reducedSendStatus),
|
||||
ResolvedSendStatus.Sent
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -166,17 +151,22 @@ export const MyStoryButton = ({
|
|||
<div className="StoryListItem__info--title StoryListItem__chevron">
|
||||
{i18n('MyStories__list_item')}
|
||||
</div>
|
||||
{newStoryResolvedSendStatus === ResolvedSendStatus.Sending && (
|
||||
{reducedSendStatus === ResolvedSendStatus.Sending && (
|
||||
<span className="StoryListItem__info--sending">
|
||||
{i18n('Stories__list--sending')}
|
||||
</span>
|
||||
)}
|
||||
{newStoryResolvedSendStatus === ResolvedSendStatus.Failed && (
|
||||
{reducedSendStatus === ResolvedSendStatus.Failed && (
|
||||
<span className="StoryListItem__info--send_failed">
|
||||
{i18n('Stories__list--send_failed')}
|
||||
</span>
|
||||
)}
|
||||
{newStoryResolvedSendStatus === ResolvedSendStatus.Sent && (
|
||||
{reducedSendStatus === ResolvedSendStatus.PartiallySent && (
|
||||
<span className="StoryListItem__info--send_failed">
|
||||
{i18n('Stories__list--partially-sent')}
|
||||
</span>
|
||||
)}
|
||||
{reducedSendStatus === ResolvedSendStatus.Sent && (
|
||||
<MessageTimestamp
|
||||
i18n={i18n}
|
||||
isRelativeTime
|
||||
|
|
|
@ -45,7 +45,7 @@ export default {
|
|||
},
|
||||
queueStoryDownload: { action: true },
|
||||
renderStoryCreator: { action: true },
|
||||
renderStoryViewer: { action: true },
|
||||
retrySend: { action: true },
|
||||
showConversation: { action: true },
|
||||
showStoriesSettings: { action: true },
|
||||
showToast: { action: true },
|
||||
|
|
|
@ -42,6 +42,7 @@ export type PropsType = {
|
|||
preferredWidthFromStorage: number;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
renderStoryCreator: () => JSX.Element;
|
||||
retrySend: (messageId: string) => unknown;
|
||||
setAddStoryData: (data: AddStoryData) => unknown;
|
||||
showConversation: ShowConversationType;
|
||||
showStoriesSettings: () => unknown;
|
||||
|
@ -69,6 +70,7 @@ export const Stories = ({
|
|||
preferredWidthFromStorage,
|
||||
queueStoryDownload,
|
||||
renderStoryCreator,
|
||||
retrySend,
|
||||
setAddStoryData,
|
||||
showConversation,
|
||||
showStoriesSettings,
|
||||
|
@ -101,6 +103,7 @@ export const Stories = ({
|
|||
<div className="Stories__pane" style={{ width }}>
|
||||
{isMyStories && myStories.length ? (
|
||||
<MyStories
|
||||
hasViewReceiptSetting={hasViewReceiptSetting}
|
||||
i18n={i18n}
|
||||
myStories={myStories}
|
||||
onBack={() => setIsMyStories(false)}
|
||||
|
@ -108,8 +111,8 @@ export const Stories = ({
|
|||
onForward={onForwardStory}
|
||||
onSave={onSaveStory}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
retrySend={retrySend}
|
||||
viewStory={viewStory}
|
||||
hasViewReceiptSetting={hasViewReceiptSetting}
|
||||
/>
|
||||
) : (
|
||||
<StoriesPane
|
||||
|
|
|
@ -9,11 +9,7 @@ import type {
|
|||
ConversationType,
|
||||
ShowConversationType,
|
||||
} from '../state/ducks/conversations';
|
||||
import type {
|
||||
ConversationStoryType,
|
||||
MyStoryType,
|
||||
StoryViewType,
|
||||
} from '../types/Stories';
|
||||
import type { ConversationStoryType, MyStoryType } from '../types/Stories';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
|
||||
|
@ -59,10 +55,6 @@ function search(
|
|||
.map(result => result.item);
|
||||
}
|
||||
|
||||
function getNewestMyStory(story: MyStoryType): StoryViewType {
|
||||
return story.stories[0];
|
||||
}
|
||||
|
||||
export type PropsType = {
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
hiddenStories: Array<ConversationStoryType>;
|
||||
|
@ -161,14 +153,9 @@ export const StoriesPane = ({
|
|||
<div className="Stories__pane__list">
|
||||
<>
|
||||
<MyStoryButton
|
||||
hasMultiple={
|
||||
myStories.length ? myStories[0].stories.length > 1 : false
|
||||
}
|
||||
i18n={i18n}
|
||||
me={me}
|
||||
newestStory={
|
||||
myStories.length ? getNewestMyStory(myStories[0]) : undefined
|
||||
}
|
||||
myStories={myStories}
|
||||
onAddStory={onAddStory}
|
||||
onClick={onMyStoriesClicked}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
|
|
|
@ -56,6 +56,7 @@ export default {
|
|||
},
|
||||
queueStoryDownload: { action: true },
|
||||
renderEmojiPicker: { action: true },
|
||||
retrySend: { action: true },
|
||||
showToast: { action: true },
|
||||
skinTone: {
|
||||
defaultValue: 0,
|
||||
|
@ -191,6 +192,35 @@ export const YourStory = Template.bind({});
|
|||
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({});
|
||||
{
|
||||
const storyView = getFakeStoryView(
|
||||
|
|
|
@ -28,10 +28,12 @@ import { Emojify } from './conversation/Emojify';
|
|||
import { Intl } from './Intl';
|
||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||
import { SendStatus } from '../messages/MessageSendState';
|
||||
import { Spinner } from './Spinner';
|
||||
import { StoryDetailsModal } from './StoryDetailsModal';
|
||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||
import { StoryImage } from './StoryImage';
|
||||
import {
|
||||
ResolvedSendStatus,
|
||||
StoryViewDirectionType,
|
||||
StoryViewModeType,
|
||||
StoryViewTargetType,
|
||||
|
@ -45,10 +47,14 @@ import { getStoryDuration } from '../util/getStoryDuration';
|
|||
import { graphemeAwareSlice } from '../util/graphemeAwareSlice';
|
||||
import { isVideoAttachment } from '../types/Attachment';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
import { useRetryStorySend } from '../hooks/useRetryStorySend';
|
||||
import { resolveStorySendStatus } from '../util/resolveStorySendStatus';
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
||||
export type PropsType = {
|
||||
currentIndex: number;
|
||||
deleteGroupStoryReply: (id: string) => void;
|
||||
deleteGroupStoryReplyForEveryone: (id: string) => void;
|
||||
deleteStoryForEveryone: (story: StoryViewType) => unknown;
|
||||
distributionList?: { id: string; name: string };
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
|
@ -70,6 +76,7 @@ export type PropsType = {
|
|||
hasViewReceiptSetting: boolean;
|
||||
i18n: LocalizerType;
|
||||
isSignalConversation?: boolean;
|
||||
isWindowActive: boolean;
|
||||
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
|
||||
markStoryRead: (mId: string) => unknown;
|
||||
numStories: number;
|
||||
|
@ -90,16 +97,14 @@ export type PropsType = {
|
|||
recentEmojis?: Array<string>;
|
||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
||||
replyState?: ReplyStateType;
|
||||
viewTarget?: StoryViewTargetType;
|
||||
retrySend: (messageId: string) => unknown;
|
||||
setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown;
|
||||
showToast: ShowToastActionCreatorType;
|
||||
skinTone?: number;
|
||||
story: StoryViewType;
|
||||
storyViewMode: StoryViewModeType;
|
||||
setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown;
|
||||
viewStory: ViewStoryActionCreatorType;
|
||||
isWindowActive: boolean;
|
||||
deleteGroupStoryReply: (id: string) => void;
|
||||
deleteGroupStoryReplyForEveryone: (id: string) => void;
|
||||
viewTarget?: StoryViewTargetType;
|
||||
};
|
||||
|
||||
const CAPTION_BUFFER = 20;
|
||||
|
@ -115,6 +120,8 @@ enum Arrow {
|
|||
|
||||
export const StoryViewer = ({
|
||||
currentIndex,
|
||||
deleteGroupStoryReply,
|
||||
deleteGroupStoryReplyForEveryone,
|
||||
deleteStoryForEveryone,
|
||||
distributionList,
|
||||
getPreferredBadge,
|
||||
|
@ -124,6 +131,7 @@ export const StoryViewer = ({
|
|||
hasViewReceiptSetting,
|
||||
i18n,
|
||||
isSignalConversation,
|
||||
isWindowActive,
|
||||
loadStoryReplies,
|
||||
markStoryRead,
|
||||
numStories,
|
||||
|
@ -139,16 +147,14 @@ export const StoryViewer = ({
|
|||
recentEmojis,
|
||||
renderEmojiPicker,
|
||||
replyState,
|
||||
viewTarget,
|
||||
retrySend,
|
||||
setHasAllStoriesUnmuted,
|
||||
showToast,
|
||||
skinTone,
|
||||
story,
|
||||
storyViewMode,
|
||||
setHasAllStoriesUnmuted,
|
||||
viewStory,
|
||||
isWindowActive,
|
||||
deleteGroupStoryReply,
|
||||
deleteGroupStoryReplyForEveryone,
|
||||
viewTarget,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [isShowingContextMenu, setIsShowingContextMenu] =
|
||||
useState<boolean>(false);
|
||||
|
@ -181,6 +187,10 @@ export const StoryViewer = ({
|
|||
|
||||
const conversationId = group?.id || story.sender.id;
|
||||
|
||||
const sendStatus = sendState ? resolveStorySendStatus(sendState) : undefined;
|
||||
const { renderAlert, setWasManuallyRetried, wasManuallyRetried } =
|
||||
useRetryStorySend(i18n, sendStatus);
|
||||
|
||||
const [currentViewTarget, setCurrentViewTarget] = useState(
|
||||
viewTarget ?? null
|
||||
);
|
||||
|
@ -316,7 +326,10 @@ export const StoryViewer = ({
|
|||
}
|
||||
}, [isWindowActive]);
|
||||
|
||||
const alertElement = renderAlert();
|
||||
|
||||
const shouldPauseViewing =
|
||||
Boolean(alertElement) ||
|
||||
Boolean(confirmDeleteStory) ||
|
||||
currentViewTarget != null ||
|
||||
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 (
|
||||
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
|
||||
<div className="StoryViewer">
|
||||
{alertElement}
|
||||
<div
|
||||
className="StoryViewer__overlay"
|
||||
style={{ background: getStoryBackground(attachment) }}
|
||||
|
@ -753,7 +783,35 @@ export const StoryViewer = ({
|
|||
))}
|
||||
</div>
|
||||
<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
|
||||
className="StoryViewer__reply"
|
||||
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;
|
||||
}
|
||||
|
||||
if (isMe(recipient.attributes)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (recipient.isUnregistered()) {
|
||||
if (!isSent(oldSendState.status)) {
|
||||
// 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
|
||||
const conversation = this.getConversation()!;
|
||||
|
||||
const currentConversationRecipients =
|
||||
conversation.getMemberConversationIds();
|
||||
let currentConversationRecipients: Set<string> | undefined;
|
||||
|
||||
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
|
||||
const oldSendStateByConversationId =
|
||||
|
|
|
@ -22,7 +22,11 @@ import type {
|
|||
StoriesStateType,
|
||||
AddStoryData,
|
||||
} 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 { SendStatus } from '../../messages/MessageSendState';
|
||||
import { canReply } from './message';
|
||||
|
@ -39,6 +43,10 @@ import { calculateExpirationTimestamp } from '../../util/expirationTimer';
|
|||
import { getMessageIdForLogging } from '../../util/idForLogging';
|
||||
import * as log from '../../logging/log';
|
||||
import { SIGNAL_ACI } from '../../types/SignalConversation';
|
||||
import {
|
||||
reduceStorySendStatus,
|
||||
resolveStorySendStatus,
|
||||
} from '../../util/resolveStorySendStatus';
|
||||
|
||||
export const getStoriesState = (state: StateType): StoriesStateType =>
|
||||
state.stories;
|
||||
|
@ -406,12 +414,23 @@ export const getStories = createSelector(
|
|||
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, {
|
||||
id: sentId,
|
||||
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
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
getStories,
|
||||
shouldShowStoriesView,
|
||||
} from '../selectors/stories';
|
||||
import { retryMessageSend } from '../../util/retryMessageSend';
|
||||
import { saveAttachment } from '../../util/saveAttachment';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
|
@ -84,6 +85,7 @@ export function SmartStories(): JSX.Element | null {
|
|||
}}
|
||||
preferredWidthFromStorage={preferredWidthFromStorage}
|
||||
renderStoryCreator={renderStoryCreator}
|
||||
retrySend={retryMessageSend}
|
||||
showConversation={showConversation}
|
||||
showStoriesSettings={showStoriesSettings}
|
||||
showToast={showToast}
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
import { isInFullScreenCall } from '../selectors/calling';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||
import { retryMessageSend } from '../../util/retryMessageSend';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { useActions as useEmojisActions } from '../ducks/emojis';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
|
@ -99,6 +100,7 @@ export function SmartStoryViewer(): JSX.Element | null {
|
|||
isSignalConversation={isSignalConversation({
|
||||
id: conversationStory.conversationId,
|
||||
})}
|
||||
isWindowActive={isWindowActive}
|
||||
numStories={selectedStoryData.numStories}
|
||||
onHideStory={toggleHideStories}
|
||||
onGoToConversation={senderId => {
|
||||
|
@ -125,12 +127,12 @@ export function SmartStoryViewer(): JSX.Element | null {
|
|||
recentEmojis={recentEmojis}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
replyState={replyState}
|
||||
viewTarget={selectedStoryData.viewTarget}
|
||||
retrySend={retryMessageSend}
|
||||
showToast={showToast}
|
||||
skinTone={skinTone}
|
||||
story={storyView}
|
||||
storyViewMode={selectedStoryData.storyViewMode}
|
||||
isWindowActive={isWindowActive}
|
||||
viewTarget={selectedStoryData.viewTarget}
|
||||
{...storiesActions}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -14,12 +14,13 @@ import * as durations from '../../util/durations';
|
|||
import { UUID } from '../../types/UUID';
|
||||
import { getDefaultConversation } from './getDefaultConversation';
|
||||
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 {
|
||||
return fakeAttachment({
|
||||
url,
|
||||
path: url,
|
||||
thumbnail: fakeThumbnail(url),
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -29,6 +30,7 @@ export function getFakeMyStory(id?: string, name?: string): MyStoryType {
|
|||
return {
|
||||
id: id || UUID.generate().toString(),
|
||||
name: name || id === MY_STORY_ID ? 'My Stories' : casual.catch_phrase,
|
||||
reducedSendStatus: ResolvedSendStatus.Sent,
|
||||
stories: Array.from(Array(storyCount), () => ({
|
||||
...getFakeStoryView(),
|
||||
sendState: [],
|
||||
|
|
|
@ -102,6 +102,7 @@ export type StoryViewType = {
|
|||
export type MyStoryType = {
|
||||
id: string;
|
||||
name: string;
|
||||
reducedSendStatus: ResolvedSendStatus;
|
||||
stories: Array<StoryViewType>;
|
||||
};
|
||||
|
||||
|
@ -158,3 +159,10 @@ export enum StorySendMode {
|
|||
Always = 'Always',
|
||||
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
|
||||
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
|
||||
export async function retryMessageSend(messageId: string): Promise<void> {
|
||||
const message = window.MessageController.getById(messageId);
|
||||
const message = await getMessageById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`retryMessageSend: Message ${messageId} missing!`);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue