Adds logic around downloading stories
This commit is contained in:
parent
9d3f0072a5
commit
3b5cc26fec
29 changed files with 645 additions and 149 deletions
|
@ -59,59 +59,55 @@ function createStory({
|
|||
};
|
||||
}
|
||||
|
||||
function getAttachmentWithThumbnail(url: string): AttachmentType {
|
||||
return fakeAttachment({
|
||||
url,
|
||||
thumbnail: fakeThumbnail(url),
|
||||
});
|
||||
}
|
||||
|
||||
const getDefaultProps = (): PropsType => ({
|
||||
hiddenStories: [],
|
||||
i18n,
|
||||
openConversationInternal: action('openConversationInternal'),
|
||||
preferredWidthFromStorage: 380,
|
||||
queueStoryDownload: action('queueStoryDownload'),
|
||||
renderStoryViewer: () => <div />,
|
||||
stories: [
|
||||
createStory({
|
||||
attachment: fakeAttachment({
|
||||
thumbnail: fakeThumbnail('/fixtures/tina-rolf-269345-unsplash.jpg'),
|
||||
}),
|
||||
attachment: getAttachmentWithThumbnail(
|
||||
'/fixtures/tina-rolf-269345-unsplash.jpg'
|
||||
),
|
||||
timestamp: Date.now() - 2 * durations.MINUTE,
|
||||
}),
|
||||
createStory({
|
||||
attachment: fakeAttachment({
|
||||
thumbnail: fakeThumbnail(
|
||||
'/fixtures/koushik-chowdavarapu-105425-unsplash.jpg'
|
||||
),
|
||||
}),
|
||||
attachment: getAttachmentWithThumbnail(
|
||||
'/fixtures/koushik-chowdavarapu-105425-unsplash.jpg'
|
||||
),
|
||||
timestamp: Date.now() - 5 * durations.MINUTE,
|
||||
}),
|
||||
createStory({
|
||||
group: { title: 'BBQ in the park' },
|
||||
attachment: fakeAttachment({
|
||||
thumbnail: fakeThumbnail(
|
||||
'/fixtures/nathan-anderson-316188-unsplash.jpg'
|
||||
),
|
||||
}),
|
||||
attachment: getAttachmentWithThumbnail(
|
||||
'/fixtures/nathan-anderson-316188-unsplash.jpg'
|
||||
),
|
||||
timestamp: Date.now() - 65 * durations.MINUTE,
|
||||
}),
|
||||
createStory({
|
||||
attachment: fakeAttachment({
|
||||
thumbnail: fakeThumbnail('/fixtures/snow.jpg'),
|
||||
}),
|
||||
attachment: getAttachmentWithThumbnail('/fixtures/snow.jpg'),
|
||||
timestamp: Date.now() - 92 * durations.MINUTE,
|
||||
}),
|
||||
createStory({
|
||||
attachment: fakeAttachment({
|
||||
thumbnail: fakeThumbnail('/fixtures/kitten-1-64-64.jpg'),
|
||||
}),
|
||||
attachment: getAttachmentWithThumbnail('/fixtures/kitten-1-64-64.jpg'),
|
||||
timestamp: Date.now() - 164 * durations.MINUTE,
|
||||
}),
|
||||
createStory({
|
||||
group: { title: 'Breaking Signal for Science' },
|
||||
attachment: fakeAttachment({
|
||||
thumbnail: fakeThumbnail('/fixtures/kitten-2-64-64.jpg'),
|
||||
}),
|
||||
attachment: getAttachmentWithThumbnail('/fixtures/kitten-2-64-64.jpg'),
|
||||
timestamp: Date.now() - 380 * durations.MINUTE,
|
||||
}),
|
||||
createStory({
|
||||
attachment: fakeAttachment({
|
||||
thumbnail: fakeThumbnail('/fixtures/kitten-3-64-64.jpg'),
|
||||
}),
|
||||
attachment: getAttachmentWithThumbnail('/fixtures/kitten-3-64-64.jpg'),
|
||||
timestamp: Date.now() - 421 * durations.MINUTE,
|
||||
}),
|
||||
],
|
||||
|
|
|
@ -16,6 +16,7 @@ export type PropsType = {
|
|||
preferredWidthFromStorage: number;
|
||||
openConversationInternal: (_: { conversationId: string }) => unknown;
|
||||
renderStoryViewer: (props: SmartStoryViewerPropsType) => JSX.Element;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
stories: Array<ConversationStoryType>;
|
||||
toggleHideStories: (conversationId: string) => unknown;
|
||||
toggleStoriesView: () => unknown;
|
||||
|
@ -31,6 +32,7 @@ export const Stories = ({
|
|||
i18n,
|
||||
openConversationInternal,
|
||||
preferredWidthFromStorage,
|
||||
queueStoryDownload,
|
||||
renderStoryViewer,
|
||||
stories,
|
||||
toggleHideStories,
|
||||
|
@ -99,6 +101,7 @@ export const Stories = ({
|
|||
}
|
||||
}}
|
||||
openConversationInternal={openConversationInternal}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
stories={stories}
|
||||
toggleHideStories={toggleHideStories}
|
||||
/>
|
||||
|
|
|
@ -44,12 +44,17 @@ function search(
|
|||
);
|
||||
}
|
||||
|
||||
function getNewestStory(story: ConversationStoryType): StoryViewType {
|
||||
return story.stories[story.stories.length - 1];
|
||||
}
|
||||
|
||||
export type PropsType = {
|
||||
hiddenStories: Array<ConversationStoryType>;
|
||||
i18n: LocalizerType;
|
||||
onBack: () => unknown;
|
||||
onStoryClicked: (conversationId: string) => unknown;
|
||||
openConversationInternal: (_: { conversationId: string }) => unknown;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
stories: Array<ConversationStoryType>;
|
||||
toggleHideStories: (conversationId: string) => unknown;
|
||||
};
|
||||
|
@ -59,6 +64,7 @@ export const StoriesPane = ({
|
|||
onBack,
|
||||
onStoryClicked,
|
||||
openConversationInternal,
|
||||
queueStoryDownload,
|
||||
stories,
|
||||
toggleHideStories,
|
||||
}: PropsType): JSX.Element => {
|
||||
|
@ -103,18 +109,19 @@ export const StoriesPane = ({
|
|||
>
|
||||
{renderedStories.map(story => (
|
||||
<StoryListItem
|
||||
key={story.stories[0].timestamp}
|
||||
key={getNewestStory(story).timestamp}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
onStoryClicked(story.conversationId);
|
||||
}}
|
||||
onHideStory={() => {
|
||||
toggleHideStories(story.stories[0].sender.id);
|
||||
toggleHideStories(getNewestStory(story).sender.id);
|
||||
}}
|
||||
onGoToConversation={conversationId => {
|
||||
openConversationInternal({ conversationId });
|
||||
}}
|
||||
story={story.stories[0]}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
story={getNewestStory(story)}
|
||||
/>
|
||||
))}
|
||||
{!stories.length && i18n('Stories__list-empty')}
|
||||
|
|
89
ts/components/StoryImage.stories.tsx
Normal file
89
ts/components/StoryImage.stories.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import type { PropsType } from './StoryImage';
|
||||
import { StoryImage } from './StoryImage';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import {
|
||||
fakeAttachment,
|
||||
fakeThumbnail,
|
||||
} from '../test-both/helpers/fakeAttachment';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf('Components/StoryImage', module);
|
||||
|
||||
function getDefaultProps(): PropsType {
|
||||
return {
|
||||
attachment: fakeAttachment({
|
||||
url: '/fixtures/nathan-anderson-316188-unsplash.jpg',
|
||||
thumbnail: fakeThumbnail('/fixtures/nathan-anderson-316188-unsplash.jpg'),
|
||||
}),
|
||||
i18n,
|
||||
label: 'A story',
|
||||
queueStoryDownload: action('queueStoryDownload'),
|
||||
storyId: uuid(),
|
||||
};
|
||||
}
|
||||
|
||||
story.add('Good story', () => <StoryImage {...getDefaultProps()} />);
|
||||
|
||||
story.add('Good story (thumbnail)', () => (
|
||||
<StoryImage {...getDefaultProps()} isThumbnail />
|
||||
));
|
||||
|
||||
story.add('Not downloaded', () => (
|
||||
<StoryImage {...getDefaultProps()} attachment={fakeAttachment()} />
|
||||
));
|
||||
|
||||
story.add('Not downloaded (thumbnail)', () => (
|
||||
<StoryImage
|
||||
{...getDefaultProps()}
|
||||
attachment={fakeAttachment()}
|
||||
isThumbnail
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Pending download', () => (
|
||||
<StoryImage
|
||||
{...getDefaultProps()}
|
||||
attachment={fakeAttachment({
|
||||
pending: true,
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Pending download (thumbnail)', () => (
|
||||
<StoryImage
|
||||
{...getDefaultProps()}
|
||||
attachment={fakeAttachment({
|
||||
pending: true,
|
||||
})}
|
||||
isThumbnail
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Broken Image', () => (
|
||||
<StoryImage
|
||||
{...getDefaultProps()}
|
||||
attachment={fakeAttachment({
|
||||
url: '/this/path/does/not/exist.jpg',
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Broken Image (thumbnail)', () => (
|
||||
<StoryImage
|
||||
{...getDefaultProps()}
|
||||
attachment={fakeAttachment({
|
||||
url: '/this/path/does/not/exist.jpg',
|
||||
})}
|
||||
isThumbnail
|
||||
/>
|
||||
));
|
112
ts/components/StoryImage.tsx
Normal file
112
ts/components/StoryImage.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Spinner } from './Spinner';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import {
|
||||
defaultBlurHash,
|
||||
isDownloaded,
|
||||
hasNotResolved,
|
||||
isDownloading,
|
||||
} from '../types/Attachment';
|
||||
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||
|
||||
export type PropsType = {
|
||||
readonly attachment?: AttachmentType;
|
||||
i18n: LocalizerType;
|
||||
readonly isThumbnail?: boolean;
|
||||
readonly label: string;
|
||||
readonly moduleClassName?: string;
|
||||
readonly queueStoryDownload: (storyId: string) => unknown;
|
||||
readonly storyId: string;
|
||||
};
|
||||
|
||||
export const StoryImage = ({
|
||||
attachment,
|
||||
i18n,
|
||||
isThumbnail,
|
||||
label,
|
||||
moduleClassName,
|
||||
queueStoryDownload,
|
||||
storyId,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
const [attachmentBroken, setAttachmentBroken] = useState<boolean>(false);
|
||||
|
||||
const shouldDownloadAttachment =
|
||||
!isDownloaded(attachment) && !isDownloading(attachment);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldDownloadAttachment) {
|
||||
queueStoryDownload(storyId);
|
||||
}
|
||||
}, [queueStoryDownload, shouldDownloadAttachment, storyId]);
|
||||
|
||||
if (!attachment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isPending = Boolean(attachment.pending);
|
||||
const isNotReadyToShow = hasNotResolved(attachment) || isPending;
|
||||
|
||||
const getClassName = getClassNamesFor('StoryImage', moduleClassName);
|
||||
|
||||
let storyElement: JSX.Element;
|
||||
if (isNotReadyToShow) {
|
||||
storyElement = (
|
||||
<Blurhash
|
||||
hash={attachment.blurHash || defaultBlurHash(ThemeType.dark)}
|
||||
height={attachment.height}
|
||||
width={attachment.width}
|
||||
/>
|
||||
);
|
||||
} else if (attachmentBroken) {
|
||||
storyElement = (
|
||||
<div
|
||||
aria-label={i18n('StoryImage__error')}
|
||||
className="StoryImage__error"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
storyElement = (
|
||||
<img
|
||||
alt={label}
|
||||
className={getClassName('__image')}
|
||||
onError={() => setAttachmentBroken(true)}
|
||||
src={
|
||||
isThumbnail && attachment.thumbnail
|
||||
? attachment.thumbnail.url
|
||||
: attachment.url
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let spinner: JSX.Element | undefined;
|
||||
if (isPending) {
|
||||
spinner = (
|
||||
<div className="StoryImage__spinner-container">
|
||||
<div className="StoryImage__spinner-bubble" title={i18n('loading')}>
|
||||
<Spinner moduleClassName="StoryImage__spinner" svgSize="small" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
getClassName(''),
|
||||
isThumbnail ? getClassName('--thumbnail') : undefined
|
||||
)}
|
||||
>
|
||||
{storyElement}
|
||||
{spinner}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -23,6 +23,7 @@ function getDefaultProps(): PropsType {
|
|||
return {
|
||||
i18n,
|
||||
onClick: action('onClick'),
|
||||
queueStoryDownload: action('queueStoryDownload'),
|
||||
story: {
|
||||
messageId: '123',
|
||||
sender: getDefaultConversation(),
|
||||
|
|
|
@ -9,8 +9,9 @@ import type { ConversationType } from '../state/ducks/conversations';
|
|||
import { Avatar, AvatarSize, AvatarStoryRing } from './Avatar';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { ContextMenuPopper } from './ContextMenu';
|
||||
import { getAvatarColor } from '../types/Colors';
|
||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||
import { StoryImage } from './StoryImage';
|
||||
import { getAvatarColor } from '../types/Colors';
|
||||
|
||||
export type ConversationStoryType = {
|
||||
conversationId: string;
|
||||
|
@ -53,6 +54,7 @@ export type PropsType = Pick<
|
|||
onClick: () => unknown;
|
||||
onGoToConversation?: (conversationId: string) => unknown;
|
||||
onHideStory?: (conversationId: string) => unknown;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
story: StoryViewType;
|
||||
};
|
||||
|
||||
|
@ -64,6 +66,7 @@ export const StoryListItem = ({
|
|||
onClick,
|
||||
onGoToConversation,
|
||||
onHideStory,
|
||||
queueStoryDownload,
|
||||
story,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
|
||||
|
@ -182,14 +185,15 @@ export const StoryListItem = ({
|
|||
/>
|
||||
)}
|
||||
{hasMultiple && <div className="StoryListItem__previews--more" />}
|
||||
{attachment && (
|
||||
<div
|
||||
className="StoryListItem__previews--image"
|
||||
style={{
|
||||
backgroundImage: `url("${attachment.thumbnail?.url}")`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<StoryImage
|
||||
attachment={attachment}
|
||||
i18n={i18n}
|
||||
isThumbnail
|
||||
label=""
|
||||
moduleClassName="StoryListItem__previews--image"
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
storyId={story.messageId}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<ContextMenuPopper
|
||||
|
|
|
@ -31,6 +31,7 @@ function getDefaultProps(): PropsType {
|
|||
onTextTooLong: action('onTextTooLong'),
|
||||
onUseEmoji: action('onUseEmoji'),
|
||||
preferredReactionEmoji: ['❤️', '👍', '👎', '😂', '😮', '😢'],
|
||||
queueStoryDownload: action('queueStoryDownload'),
|
||||
renderEmojiPicker: () => <div />,
|
||||
replies: Math.floor(Math.random() * 20),
|
||||
stories: [
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSpring, animated, to } from '@react-spring/web';
|
||||
import type { BodyRangeType, LocalizerType } from '../types/Util';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
|
@ -12,7 +12,9 @@ import type { StoryViewType } from './StoryListItem';
|
|||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { Intl } from './Intl';
|
||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||
import { StoryImage } from './StoryImage';
|
||||
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
||||
import { isDownloaded, isDownloading } from '../types/Attachment';
|
||||
import { getAvatarColor } from '../types/Colors';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
|
||||
|
@ -37,6 +39,7 @@ export type PropsType = {
|
|||
) => unknown;
|
||||
onUseEmoji: (_: EmojiPickDataType) => unknown;
|
||||
preferredReactionEmoji: Array<string>;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
recentEmojis?: Array<string>;
|
||||
replies?: number;
|
||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
||||
|
@ -59,6 +62,7 @@ export const StoryViewer = ({
|
|||
onTextTooLong,
|
||||
onUseEmoji,
|
||||
preferredReactionEmoji,
|
||||
queueStoryDownload,
|
||||
recentEmojis,
|
||||
renderEmojiPicker,
|
||||
replies,
|
||||
|
@ -70,6 +74,7 @@ export const StoryViewer = ({
|
|||
|
||||
const visibleStory = stories[currentStoryIndex];
|
||||
|
||||
const { attachment, messageId, timestamp } = visibleStory;
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
|
@ -93,19 +98,20 @@ export const StoryViewer = ({
|
|||
|
||||
useEscapeHandling(onEscape);
|
||||
|
||||
// Either we show the next story in the current user's stories or we ask
|
||||
// for the next user's stories.
|
||||
const showNextStory = useCallback(() => {
|
||||
// Either we show the next story in the current user's stories or we ask
|
||||
// for the next user's stories.
|
||||
if (currentStoryIndex < stories.length - 1) {
|
||||
setCurrentStoryIndex(currentStoryIndex + 1);
|
||||
} else {
|
||||
setCurrentStoryIndex(0);
|
||||
onNextUserStories();
|
||||
}
|
||||
}, [currentStoryIndex, onNextUserStories, stories.length]);
|
||||
|
||||
// Either we show the previous story in the current user's stories or we ask
|
||||
// for the prior user's stories.
|
||||
const showPrevStory = useCallback(() => {
|
||||
// Either we show the previous story in the current user's stories or we ask
|
||||
// for the prior user's stories.
|
||||
if (currentStoryIndex === 0) {
|
||||
onPrevUserStories();
|
||||
} else {
|
||||
|
@ -128,8 +134,18 @@ export const StoryViewer = ({
|
|||
spring.start({
|
||||
from: { width: 0 },
|
||||
to: { width: 100 },
|
||||
onRest: showNextStory,
|
||||
onRest: {
|
||||
width: ({ value }) => {
|
||||
if (value === 100) {
|
||||
showNextStory();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
spring.stop();
|
||||
};
|
||||
}, [currentStoryIndex, showNextStory, spring]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -141,8 +157,21 @@ export const StoryViewer = ({
|
|||
}, [hasReplyModal, spring]);
|
||||
|
||||
useEffect(() => {
|
||||
markStoryRead(visibleStory.messageId);
|
||||
}, [markStoryRead, visibleStory.messageId]);
|
||||
markStoryRead(messageId);
|
||||
}, [markStoryRead, messageId]);
|
||||
|
||||
// Queue all undownloaded stories once we're viewing someone's stories
|
||||
const storiesToDownload = useMemo(() => {
|
||||
return stories
|
||||
.filter(
|
||||
story =>
|
||||
!isDownloaded(story.attachment) && !isDownloading(story.attachment)
|
||||
)
|
||||
.map(story => story.messageId);
|
||||
}, [stories]);
|
||||
useEffect(() => {
|
||||
storiesToDownload.forEach(id => queueStoryDownload(id));
|
||||
}, [queueStoryDownload, storiesToDownload]);
|
||||
|
||||
const navigateStories = useCallback(
|
||||
(ev: KeyboardEvent) => {
|
||||
|
@ -183,13 +212,14 @@ export const StoryViewer = ({
|
|||
type="button"
|
||||
/>
|
||||
<div className="StoryViewer__container">
|
||||
{visibleStory.attachment && (
|
||||
<img
|
||||
alt={i18n('lightboxImageAlt')}
|
||||
className="StoryViewer__story"
|
||||
src={visibleStory.attachment.url}
|
||||
/>
|
||||
)}
|
||||
<StoryImage
|
||||
attachment={attachment}
|
||||
i18n={i18n}
|
||||
label={i18n('lightboxImageAlt')}
|
||||
moduleClassName="StoryViewer__story"
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
storyId={messageId}
|
||||
/>
|
||||
<div className="StoryViewer__meta">
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
|
@ -233,13 +263,13 @@ export const StoryViewer = ({
|
|||
<MessageTimestamp
|
||||
i18n={i18n}
|
||||
module="StoryViewer__meta--timestamp"
|
||||
timestamp={visibleStory.timestamp}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
<div className="StoryViewer__progress">
|
||||
{stories.map((story, index) => (
|
||||
<div
|
||||
className="StoryViewer__progress--container"
|
||||
key={story.timestamp}
|
||||
key={story.messageId}
|
||||
>
|
||||
{currentStoryIndex === index ? (
|
||||
<animated.div
|
||||
|
@ -315,9 +345,9 @@ export const StoryViewer = ({
|
|||
onReact={emoji => {
|
||||
onReactToStory(emoji, visibleStory);
|
||||
}}
|
||||
onReply={(message, mentions, timestamp) => {
|
||||
onReply={(message, mentions, replyTimestamp) => {
|
||||
setHasReplyModal(false);
|
||||
onReplyToStory(message, mentions, timestamp, visibleStory);
|
||||
onReplyToStory(message, mentions, replyTimestamp, visibleStory);
|
||||
}}
|
||||
onSetSkinTone={onSetSkinTone}
|
||||
onTextTooLong={onTextTooLong}
|
||||
|
@ -327,7 +357,7 @@ export const StoryViewer = ({
|
|||
renderEmojiPicker={renderEmojiPicker}
|
||||
replies={[]}
|
||||
skinTone={skinTone}
|
||||
storyPreviewAttachment={visibleStory.attachment}
|
||||
storyPreviewAttachment={attachment}
|
||||
views={[]}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Spinner } from '../Spinner';
|
|||
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import {
|
||||
hasNotDownloaded,
|
||||
hasNotResolved,
|
||||
getImageDimensions,
|
||||
defaultBlurHash,
|
||||
} from '../../types/Attachment';
|
||||
|
@ -166,10 +166,10 @@ export const GIF: React.FC<Props> = props => {
|
|||
};
|
||||
|
||||
const isPending = Boolean(attachment.pending);
|
||||
const isNotDownloaded = hasNotDownloaded(attachment) && !isPending;
|
||||
const isNotResolved = hasNotResolved(attachment) && !isPending;
|
||||
|
||||
let fileSize: JSX.Element | undefined;
|
||||
if (isNotDownloaded && attachment.fileSize) {
|
||||
if (isNotResolved && attachment.fileSize) {
|
||||
fileSize = (
|
||||
<div className="module-image--gif__filesize">
|
||||
{attachment.fileSize} · GIF
|
||||
|
@ -178,7 +178,7 @@ export const GIF: React.FC<Props> = props => {
|
|||
}
|
||||
|
||||
let gif: JSX.Element | undefined;
|
||||
if (isNotDownloaded || isPending) {
|
||||
if (isNotResolved || isPending) {
|
||||
gif = (
|
||||
<Blurhash
|
||||
hash={attachment.blurHash || defaultBlurHash(theme)}
|
||||
|
@ -213,12 +213,12 @@ export const GIF: React.FC<Props> = props => {
|
|||
}
|
||||
|
||||
let overlay: JSX.Element | undefined;
|
||||
if ((tapToPlay && !isPlaying) || isNotDownloaded) {
|
||||
if ((tapToPlay && !isPlaying) || isNotResolved) {
|
||||
const className = classNames([
|
||||
'module-image__border-overlay',
|
||||
'module-image__border-overlay--with-click-handler',
|
||||
'module-image--soft-corners',
|
||||
isNotDownloaded
|
||||
isNotResolved
|
||||
? 'module-image--not-downloaded'
|
||||
: 'module-image--tap-to-play',
|
||||
]);
|
||||
|
|
|
@ -8,7 +8,10 @@ import { Blurhash } from 'react-blurhash';
|
|||
import { Spinner } from '../Spinner';
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import { hasNotDownloaded, defaultBlurHash } from '../../types/Attachment';
|
||||
import {
|
||||
isDownloaded as isDownloadedFunction,
|
||||
defaultBlurHash,
|
||||
} from '../../types/Attachment';
|
||||
|
||||
export type Props = {
|
||||
alt: string;
|
||||
|
@ -169,7 +172,7 @@ export class Image extends React.Component<Props> {
|
|||
const canClick = this.canClick();
|
||||
const imgNotDownloaded = isDownloaded
|
||||
? false
|
||||
: hasNotDownloaded(attachment);
|
||||
: !isDownloadedFunction(attachment);
|
||||
|
||||
const resolvedBlurHash = blurHash || defaultBlurHash(theme);
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ import {
|
|||
getGridDimensions,
|
||||
getImageDimensions,
|
||||
hasImage,
|
||||
hasNotDownloaded,
|
||||
isDownloaded,
|
||||
hasVideoScreenshot,
|
||||
isAudio,
|
||||
isImage,
|
||||
|
@ -917,7 +917,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
onError={this.handleImageError}
|
||||
tabIndex={tabIndex}
|
||||
onClick={attachment => {
|
||||
if (hasNotDownloaded(attachment)) {
|
||||
if (!isDownloaded(attachment)) {
|
||||
kickOffAttachmentDownload({ attachment, messageId: id });
|
||||
} else {
|
||||
showVisualAttachment({ attachment, messageId: id });
|
||||
|
@ -1093,7 +1093,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
);
|
||||
const onPreviewImageClick = () => {
|
||||
if (first.image && hasNotDownloaded(first.image)) {
|
||||
if (first.image && !isDownloaded(first.image)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: first.image,
|
||||
messageId: id,
|
||||
|
@ -2388,7 +2388,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (attachments && hasNotDownloaded(attachments[0])) {
|
||||
if (attachments && !isDownloaded(attachments[0])) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
kickOffAttachmentDownload({
|
||||
|
@ -2420,7 +2420,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
attachments.length > 0 &&
|
||||
!isAttachmentPending &&
|
||||
(isImage(attachments) || isVideo(attachments)) &&
|
||||
hasNotDownloaded(attachments[0])
|
||||
!isDownloaded(attachments[0])
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -2511,7 +2511,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
const attachment = attachments[0];
|
||||
if (hasNotDownloaded(attachment)) {
|
||||
if (!isDownloaded(attachment)) {
|
||||
kickOffAttachmentDownload({
|
||||
attachment,
|
||||
messageId: id,
|
||||
|
|
|
@ -8,7 +8,7 @@ import { noop } from 'lodash';
|
|||
import { assert } from '../../util/assert';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import type { AttachmentType } from '../../types/Attachment';
|
||||
import { hasNotDownloaded } from '../../types/Attachment';
|
||||
import { isDownloaded } from '../../types/Attachment';
|
||||
import type { DirectionType, MessageStatusType } from './Message';
|
||||
|
||||
import type { ComputePeaksResult } from '../GlobalAudioContext';
|
||||
|
@ -203,7 +203,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
|
||||
if (attachment.pending) {
|
||||
state = State.Pending;
|
||||
} else if (hasNotDownloaded(attachment)) {
|
||||
} else if (!isDownloaded(attachment)) {
|
||||
state = State.NotDownloaded;
|
||||
} else if (!hasPeaks) {
|
||||
state = State.Computing;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue