Adds error states to story images
This commit is contained in:
parent
782838c591
commit
fcf7406dd4
12 changed files with 158 additions and 21 deletions
|
@ -7481,9 +7481,19 @@
|
||||||
"message": "Hide",
|
"message": "Hide",
|
||||||
"description": "Action button for the confirmation dialog to hide a story"
|
"description": "Action button for the confirmation dialog to hide a story"
|
||||||
},
|
},
|
||||||
"StoryImage__error": {
|
"StoryImage__error2": {
|
||||||
"message": "Error displaying image",
|
"message": "Can’t download story. $name$ will need to share it again.",
|
||||||
"description": "aria-label for image errors"
|
"description": "Description for image errors",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Clara"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"StoryImage__error--you": {
|
||||||
|
"message": "Can’t download story. You will need to share it again.",
|
||||||
|
"description": "Description for image errors but when it is your own image"
|
||||||
},
|
},
|
||||||
"StoryCreator__text-bg": {
|
"StoryCreator__text-bg": {
|
||||||
"message": "Toggle text background color",
|
"message": "Toggle text background color",
|
||||||
|
|
|
@ -33,6 +33,18 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__error {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/full-screen-flow/alert-outline.svg',
|
||||||
|
$color-white
|
||||||
|
);
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 32px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
&__spinner-bubble {
|
&__spinner-bubble {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: $color-gray-75;
|
background-color: $color-gray-75;
|
||||||
|
|
|
@ -92,7 +92,9 @@ export const MyStories = ({
|
||||||
>
|
>
|
||||||
<StoryImage
|
<StoryImage
|
||||||
attachment={story.attachment}
|
attachment={story.attachment}
|
||||||
|
firstName={i18n('you')}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
isMe
|
||||||
isThumbnail
|
isThumbnail
|
||||||
label={i18n('MyStories__story')}
|
label={i18n('MyStories__story')}
|
||||||
moduleClassName="MyStories__story__preview"
|
moduleClassName="MyStories__story__preview"
|
||||||
|
|
|
@ -83,7 +83,9 @@ export const MyStoriesButton = ({
|
||||||
{newestStory ? (
|
{newestStory ? (
|
||||||
<StoryImage
|
<StoryImage
|
||||||
attachment={newestStory.attachment}
|
attachment={newestStory.attachment}
|
||||||
|
firstName={i18n('you')}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
isMe
|
||||||
isThumbnail
|
isThumbnail
|
||||||
label=""
|
label=""
|
||||||
moduleClassName="StoryListItem__previews--image"
|
moduleClassName="StoryListItem__previews--image"
|
||||||
|
|
|
@ -27,6 +27,7 @@ function getDefaultProps(): PropsType {
|
||||||
url: '/fixtures/nathan-anderson-316188-unsplash.jpg',
|
url: '/fixtures/nathan-anderson-316188-unsplash.jpg',
|
||||||
thumbnail: fakeThumbnail('/fixtures/nathan-anderson-316188-unsplash.jpg'),
|
thumbnail: fakeThumbnail('/fixtures/nathan-anderson-316188-unsplash.jpg'),
|
||||||
}),
|
}),
|
||||||
|
firstName: 'Charlie',
|
||||||
i18n,
|
i18n,
|
||||||
label: 'A story',
|
label: 'A story',
|
||||||
queueStoryDownload: action('queueStoryDownload'),
|
queueStoryDownload: action('queueStoryDownload'),
|
||||||
|
@ -129,3 +130,39 @@ export const Video = (): JSX.Element => (
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ErrorImage = (): JSX.Element => (
|
||||||
|
<StoryImage
|
||||||
|
{...getDefaultProps()}
|
||||||
|
attachment={fakeAttachment({
|
||||||
|
error: true,
|
||||||
|
url: '/this/path/does/not/exist.jpg',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ErrorImageThumbnail = (): JSX.Element => (
|
||||||
|
<StoryImage
|
||||||
|
{...getDefaultProps()}
|
||||||
|
attachment={fakeAttachment({
|
||||||
|
error: true,
|
||||||
|
url: '/this/path/does/not/exist.jpg',
|
||||||
|
})}
|
||||||
|
isThumbnail
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
ErrorImageThumbnail.story = {
|
||||||
|
name: 'Error Image (thumbnail)',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ErrorImageYou = (): JSX.Element => (
|
||||||
|
<StoryImage
|
||||||
|
{...getDefaultProps()}
|
||||||
|
isMe
|
||||||
|
attachment={fakeAttachment({
|
||||||
|
error: true,
|
||||||
|
url: '/this/path/does/not/exist.jpg',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Blurhash } from 'react-blurhash';
|
import { Blurhash } from 'react-blurhash';
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import { TextAttachment } from './TextAttachment';
|
||||||
import { ThemeType } from '../types/Util';
|
import { ThemeType } from '../types/Util';
|
||||||
import {
|
import {
|
||||||
defaultBlurHash,
|
defaultBlurHash,
|
||||||
|
hasFailed,
|
||||||
hasNotResolved,
|
hasNotResolved,
|
||||||
isDownloaded,
|
isDownloaded,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
|
@ -24,7 +25,9 @@ import { isVideoTypeSupported } from '../util/GoogleChrome';
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
readonly attachment?: AttachmentType;
|
readonly attachment?: AttachmentType;
|
||||||
readonly children?: ReactNode;
|
readonly children?: ReactNode;
|
||||||
|
readonly firstName: string;
|
||||||
readonly i18n: LocalizerType;
|
readonly i18n: LocalizerType;
|
||||||
|
readonly isMe?: boolean;
|
||||||
readonly isMuted?: boolean;
|
readonly isMuted?: boolean;
|
||||||
readonly isPaused?: boolean;
|
readonly isPaused?: boolean;
|
||||||
readonly isThumbnail?: boolean;
|
readonly isThumbnail?: boolean;
|
||||||
|
@ -37,7 +40,9 @@ export type PropsType = {
|
||||||
export const StoryImage = ({
|
export const StoryImage = ({
|
||||||
attachment,
|
attachment,
|
||||||
children,
|
children,
|
||||||
|
firstName,
|
||||||
i18n,
|
i18n,
|
||||||
|
isMe,
|
||||||
isMuted,
|
isMuted,
|
||||||
isPaused,
|
isPaused,
|
||||||
isThumbnail,
|
isThumbnail,
|
||||||
|
@ -50,6 +55,7 @@ export const StoryImage = ({
|
||||||
(!isDownloaded(attachment) && !isDownloading(attachment)) ||
|
(!isDownloaded(attachment) && !isDownloading(attachment)) ||
|
||||||
hasNotResolved(attachment);
|
hasNotResolved(attachment);
|
||||||
|
|
||||||
|
const [hasImgError, setHasImgError] = useState(false);
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -74,8 +80,10 @@ export const StoryImage = ({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPending = Boolean(attachment.pending) && !attachment.textAttachment;
|
const hasError = hasImgError || hasFailed(attachment);
|
||||||
const isNotReadyToShow = hasNotResolved(attachment) || isPending;
|
const isPending =
|
||||||
|
Boolean(attachment.pending) && !attachment.textAttachment && !hasError;
|
||||||
|
const isNotReadyToShow = hasNotResolved(attachment) || isPending || hasError;
|
||||||
const isSupportedVideo = isVideoTypeSupported(attachment.contentType);
|
const isSupportedVideo = isVideoTypeSupported(attachment.contentType);
|
||||||
|
|
||||||
const getClassName = getClassNamesFor('StoryImage', moduleClassName);
|
const getClassName = getClassNamesFor('StoryImage', moduleClassName);
|
||||||
|
@ -118,6 +126,7 @@ export const StoryImage = ({
|
||||||
<img
|
<img
|
||||||
alt={label}
|
alt={label}
|
||||||
className={getClassName('__image')}
|
className={getClassName('__image')}
|
||||||
|
onError={() => setHasImgError(true)}
|
||||||
src={
|
src={
|
||||||
isThumbnail && attachment.thumbnail
|
isThumbnail && attachment.thumbnail
|
||||||
? attachment.thumbnail.url
|
? attachment.thumbnail.url
|
||||||
|
@ -136,6 +145,17 @@ export const StoryImage = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (hasError) {
|
||||||
|
let content = <div className="StoryImage__error" />;
|
||||||
|
if (!isThumbnail) {
|
||||||
|
if (isMe) {
|
||||||
|
content = <>{i18n('StoryImage__error--you')}</>;
|
||||||
|
} else {
|
||||||
|
content = <>{i18n('StoryImage__error2', [firstName])}</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay = <div className="StoryImage__overlay-container">{content}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -179,6 +179,7 @@ export const StoryListItem = ({
|
||||||
<div className="StoryListItem__previews">
|
<div className="StoryListItem__previews">
|
||||||
<StoryImage
|
<StoryImage
|
||||||
attachment={attachment}
|
attachment={attachment}
|
||||||
|
firstName={firstName || title}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isThumbnail
|
isThumbnail
|
||||||
label=""
|
label=""
|
||||||
|
|
|
@ -471,6 +471,7 @@ export const StoryViewer = ({
|
||||||
<div className="StoryViewer__container">
|
<div className="StoryViewer__container">
|
||||||
<StoryImage
|
<StoryImage
|
||||||
attachment={attachment}
|
attachment={attachment}
|
||||||
|
firstName={firstName || title}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isPaused={shouldPauseViewing}
|
isPaused={shouldPauseViewing}
|
||||||
isMuted={isStoryMuted}
|
isMuted={isStoryMuted}
|
||||||
|
|
|
@ -222,7 +222,7 @@ async function _maybeStartJob(): Promise<void> {
|
||||||
|
|
||||||
delete _activeAttachmentDownloadJobs[job.id];
|
delete _activeAttachmentDownloadJobs[job.id];
|
||||||
try {
|
try {
|
||||||
await removeAttachmentDownloadJob(job.id);
|
await _markAttachmentAsFailed(job);
|
||||||
} catch (deleteError) {
|
} catch (deleteError) {
|
||||||
log.error(
|
log.error(
|
||||||
`${logId}: Failed to delete attachment job`,
|
`${logId}: Failed to delete attachment job`,
|
||||||
|
@ -261,20 +261,10 @@ async function _runJob(job?: AttachmentDownloadJobType): Promise<void> {
|
||||||
const pending = true;
|
const pending = true;
|
||||||
await setAttachmentDownloadJobPending(id, pending);
|
await setAttachmentDownloadJobPending(id, pending);
|
||||||
|
|
||||||
message = window.MessageController.getById(messageId);
|
message = await _getMessageById(id, messageId);
|
||||||
if (!message) {
|
|
||||||
const messageAttributes = await getMessageById(messageId);
|
|
||||||
if (!messageAttributes) {
|
|
||||||
logger.error(
|
|
||||||
`attachment_downloads/_runJob(${id}): ` +
|
|
||||||
'Source message not found, deleting job'
|
|
||||||
);
|
|
||||||
await _finishJob(null, id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
strictAssert(messageId === messageAttributes.id, 'message id mismatch');
|
if (!message) {
|
||||||
message = window.MessageController.register(messageId, messageAttributes);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _addAttachmentToMessage(
|
await _addAttachmentToMessage(
|
||||||
|
@ -370,6 +360,48 @@ async function _runJob(job?: AttachmentDownloadJobType): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _markAttachmentAsFailed(
|
||||||
|
job: AttachmentDownloadJobType
|
||||||
|
): Promise<void> {
|
||||||
|
const { id, messageId, attachment, type, index } = job;
|
||||||
|
const message = await _getMessageById(id, messageId);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _addAttachmentToMessage(
|
||||||
|
message,
|
||||||
|
_markAttachmentAsPermanentError(attachment),
|
||||||
|
{ type, index }
|
||||||
|
);
|
||||||
|
await _finishJob(message, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _getMessageById(
|
||||||
|
id: string,
|
||||||
|
messageId: string
|
||||||
|
): Promise<MessageModel | undefined> {
|
||||||
|
const message = window.MessageController.getById(messageId);
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageAttributes = await getMessageById(messageId);
|
||||||
|
if (!messageAttributes) {
|
||||||
|
logger.error(
|
||||||
|
`attachment_downloads/_runJob(${id}): ` +
|
||||||
|
'Source message not found, deleting job'
|
||||||
|
);
|
||||||
|
await _finishJob(null, id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
strictAssert(messageId === messageAttributes.id, 'message id mismatch');
|
||||||
|
return window.MessageController.register(messageId, messageAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
async function _finishJob(
|
async function _finishJob(
|
||||||
message: MessageModel | null | undefined,
|
message: MessageModel | null | undefined,
|
||||||
id: string
|
id: string
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { replaceIndex } from '../../util/replaceIndex';
|
||||||
import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage';
|
import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage';
|
||||||
import { showToast } from '../../util/showToast';
|
import { showToast } from '../../util/showToast';
|
||||||
import {
|
import {
|
||||||
|
hasFailed,
|
||||||
hasNotResolved,
|
hasNotResolved,
|
||||||
isDownloaded,
|
isDownloaded,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
|
@ -378,7 +379,10 @@ function markStoryRead(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDownloaded(matchingStory.attachment)) {
|
if (
|
||||||
|
!isDownloaded(matchingStory.attachment) &&
|
||||||
|
!hasFailed(matchingStory.attachment)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -449,6 +453,10 @@ function queueStoryDownload(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasFailed(attachment)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isDownloaded(attachment)) {
|
if (isDownloaded(attachment)) {
|
||||||
if (!attachment.path) {
|
if (!attachment.path) {
|
||||||
return;
|
return;
|
||||||
|
@ -1001,6 +1009,8 @@ export function reducer(
|
||||||
const hasAttachmentDownloaded =
|
const hasAttachmentDownloaded =
|
||||||
!isDownloaded(prevStory.attachment) &&
|
!isDownloaded(prevStory.attachment) &&
|
||||||
isDownloaded(newStory.attachment);
|
isDownloaded(newStory.attachment);
|
||||||
|
const hasAttachmentFailed =
|
||||||
|
hasFailed(newStory.attachment) && !hasFailed(prevStory.attachment);
|
||||||
const readStatusChanged = prevStory.readStatus !== newStory.readStatus;
|
const readStatusChanged = prevStory.readStatus !== newStory.readStatus;
|
||||||
const reactionsChanged =
|
const reactionsChanged =
|
||||||
prevStory.reactions?.length !== newStory.reactions?.length;
|
prevStory.reactions?.length !== newStory.reactions?.length;
|
||||||
|
@ -1014,6 +1024,7 @@ export function reducer(
|
||||||
const shouldReplace =
|
const shouldReplace =
|
||||||
isDownloadingAttachment ||
|
isDownloadingAttachment ||
|
||||||
hasAttachmentDownloaded ||
|
hasAttachmentDownloaded ||
|
||||||
|
hasAttachmentFailed ||
|
||||||
hasBeenDeleted ||
|
hasBeenDeleted ||
|
||||||
hasSendStateChanged ||
|
hasSendStateChanged ||
|
||||||
readStatusChanged ||
|
readStatusChanged ||
|
||||||
|
|
|
@ -768,6 +768,10 @@ export function isDownloading(attachment?: AttachmentType): boolean {
|
||||||
return Boolean(attachment && attachment.downloadJobId && attachment.pending);
|
return Boolean(attachment && attachment.downloadJobId && attachment.pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasFailed(attachment?: AttachmentType): boolean {
|
||||||
|
return Boolean(attachment && attachment.error);
|
||||||
|
}
|
||||||
|
|
||||||
export function hasVideoBlurHash(attachments?: Array<AttachmentType>): boolean {
|
export function hasVideoBlurHash(attachments?: Array<AttachmentType>): boolean {
|
||||||
const firstAttachment = attachments ? attachments[0] : null;
|
const firstAttachment = attachments ? attachments[0] : null;
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import type { AttachmentType } from '../types/Attachment';
|
import type { AttachmentType } from '../types/Attachment';
|
||||||
import {
|
import {
|
||||||
|
hasFailed,
|
||||||
hasNotResolved,
|
hasNotResolved,
|
||||||
isDownloaded,
|
isDownloaded,
|
||||||
isGIF,
|
isGIF,
|
||||||
|
@ -18,6 +19,10 @@ const MIN_TEXT_DURATION = 3 * SECOND;
|
||||||
export async function getStoryDuration(
|
export async function getStoryDuration(
|
||||||
attachment: AttachmentType
|
attachment: AttachmentType
|
||||||
): Promise<number | undefined> {
|
): Promise<number | undefined> {
|
||||||
|
if (hasFailed(attachment)) {
|
||||||
|
return DEFAULT_DURATION;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isDownloaded(attachment) || hasNotResolved(attachment)) {
|
if (!isDownloaded(attachment) || hasNotResolved(attachment)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue