From 220963c7895e3ba3a5ba3f05c807f1e1c645cc5f Mon Sep 17 00:00:00 2001
From: Josh Perez <60019601+josh-signal@users.noreply.github.com>
Date: Wed, 16 Nov 2022 17:10:11 -0500
Subject: [PATCH] Sending/Failed state for stories
---
_locales/en/messages.json | 24 +++
stylesheets/components/MyStories.scss | 23 +++
stylesheets/components/StoryViewer.scss | 30 +++
ts/components/MyStories.stories.tsx | 30 ++-
ts/components/MyStories.tsx | 262 ++++++++++++++++--------
ts/components/MyStoryButton.stories.tsx | 80 +++++---
ts/components/MyStoryButton.tsx | 68 +++---
ts/components/Stories.stories.tsx | 2 +-
ts/components/Stories.tsx | 5 +-
ts/components/StoriesPane.tsx | 17 +-
ts/components/StoryViewer.stories.tsx | 30 +++
ts/components/StoryViewer.tsx | 80 +++++++-
ts/hooks/useRetryStorySend.tsx | 52 +++++
ts/jobs/helpers/sendStory.ts | 4 +
ts/models/messages.ts | 25 ++-
ts/state/selectors/stories.ts | 25 ++-
ts/state/smart/Stories.tsx | 2 +
ts/state/smart/StoryViewer.tsx | 6 +-
ts/test-both/helpers/getFakeStory.tsx | 6 +-
ts/types/Stories.ts | 8 +
ts/util/resolveStorySendStatus.ts | 81 ++++++++
ts/util/retryMessageSend.ts | 6 +-
22 files changed, 676 insertions(+), 190 deletions(-)
create mode 100644 ts/hooks/useRetryStorySend.tsx
create mode 100644 ts/util/resolveStorySendStatus.ts
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 21fb652da78..3b3f4551c44 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -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"
diff --git a/stylesheets/components/MyStories.scss b/stylesheets/components/MyStories.scss
index 39b195fe3b2..8653120024f 100644
--- a/stylesheets/components/MyStories.scss
+++ b/stylesheets/components/MyStories.scss
@@ -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 {
diff --git a/stylesheets/components/StoryViewer.scss b/stylesheets/components/StoryViewer.scss
index 51ff0c4b1ff..c0342c312c1 100644
--- a/stylesheets/components/StoryViewer.scss
+++ b/stylesheets/components/StoryViewer.scss
@@ -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;
+ }
+ }
}
diff --git a/ts/components/MyStories.stories.tsx b/ts/components/MyStories.stories.tsx
index e2d3dc8123f..36945a4bb56 100644
--- a/ts/components/MyStories.stories.tsx
+++ b/ts/components/MyStories.stories.tsx
@@ -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'),
+ ],
+ };
+}
diff --git a/ts/components/MyStories.tsx b/ts/components/MyStories.tsx
index afa10a3f0cb..7d42cf6bcf5 100644
--- a/ts/components/MyStories.tsx
+++ b/ts/components/MyStories.tsx
@@ -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 = ({
/>
{list.stories.map(story => (
-
-
- {story.attachment &&
- (story.attachment.path || story.attachment.data) && (
-
+
))}
))}
@@ -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 (
+
+ {renderAlert()}
+
+ {story.attachment && (story.attachment.path || story.attachment.data) && (
+
+ );
+}
diff --git a/ts/components/MyStoryButton.stories.tsx b/ts/components/MyStoryButton.stories.tsx
index bdbad995e32..d18c4110d26 100644
--- a/ts/components/MyStoryButton.stories.tsx
+++ b/ts/components/MyStoryButton.stories.tsx
@@ -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 = 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;
diff --git a/ts/components/MyStoryButton.tsx b/ts/components/MyStoryButton.tsx
index b3273c54be5..2858b5449b1 100644
--- a/ts/components/MyStoryButton.tsx
+++ b/ts/components/MyStoryButton.tsx
@@ -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;
onAddStory: () => unknown;
onClick: () => unknown;
queueStoryDownload: (storyId: string) => unknown;
showToast: ShowToastActionCreatorType;
};
-enum ResolvedSendStatus {
- Failed,
- Sending,
- Sent,
-}
-
-function resolveSendStatus(
- sendStates: Array
-): 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 = ({
{i18n('MyStories__list_item')}
- {newStoryResolvedSendStatus === ResolvedSendStatus.Sending && (
+ {reducedSendStatus === ResolvedSendStatus.Sending && (
{i18n('Stories__list--sending')}
)}
- {newStoryResolvedSendStatus === ResolvedSendStatus.Failed && (
+ {reducedSendStatus === ResolvedSendStatus.Failed && (
{i18n('Stories__list--send_failed')}
)}
- {newStoryResolvedSendStatus === ResolvedSendStatus.Sent && (
+ {reducedSendStatus === ResolvedSendStatus.PartiallySent && (
+
+ {i18n('Stories__list--partially-sent')}
+
+ )}
+ {reducedSendStatus === ResolvedSendStatus.Sent && (
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 = ({
{isMyStories && myStories.length ? (
setIsMyStories(false)}
@@ -108,8 +111,8 @@ export const Stories = ({
onForward={onForwardStory}
onSave={onSaveStory}
queueStoryDownload={queueStoryDownload}
+ retrySend={retrySend}
viewStory={viewStory}
- hasViewReceiptSetting={hasViewReceiptSetting}
/>
) : (
result.item);
}
-function getNewestMyStory(story: MyStoryType): StoryViewType {
- return story.stories[0];
-}
-
export type PropsType = {
getPreferredBadge: PreferredBadgeSelectorType;
hiddenStories: Array;
@@ -161,14 +153,9 @@ export const StoriesPane = ({
<>
1 : false
- }
i18n={i18n}
me={me}
- newestStory={
- myStories.length ? getNewestMyStory(myStories[0]) : undefined
- }
+ myStories={myStories}
onAddStory={onAddStory}
onClick={onMyStoriesClicked}
queueStoryDownload={queueStoryDownload}
diff --git a/ts/components/StoryViewer.stories.tsx b/ts/components/StoryViewer.stories.tsx
index 11b598b307b..3fad0c0cef2 100644
--- a/ts/components/StoryViewer.stories.tsx
+++ b/ts/components/StoryViewer.stories.tsx
@@ -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(
diff --git a/ts/components/StoryViewer.tsx b/ts/components/StoryViewer.tsx
index 7862d5bb63a..8d9726cd0f3 100644
--- a/ts/components/StoryViewer.tsx
+++ b/ts/components/StoryViewer.tsx
@@ -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;
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(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 (
+ {alertElement}
- {(canReply || isSent) && (
+ {sendStatus === ResolvedSendStatus.Failed && !wasManuallyRetried && (
+
+ )}
+ {sendStatus === ResolvedSendStatus.PartiallySent &&
+ !wasManuallyRetried && (
+
+ )}
+ {sendStatus === ResolvedSendStatus.Sending && (
+
+
+ {i18n('StoryViewer__sending')}
+
+ )}
+ {sendStatus === ResolvedSendStatus.Sent && (canReply || isSent) && (