Render group stories
This commit is contained in:
parent
14ab7b9e0d
commit
e3d537cbd3
24 changed files with 527 additions and 163 deletions
|
@ -86,11 +86,15 @@ export const ModalHost = React.memo(
|
|||
<animated.div
|
||||
role="presentation"
|
||||
className={getClassName('__overlay')}
|
||||
onMouseDown={noMouseClose ? undefined : handleMouseDown}
|
||||
onMouseUp={noMouseClose ? undefined : handleMouseUp}
|
||||
style={overlayStyles}
|
||||
/>
|
||||
<div className={getClassName('__overlay-container')}>{children}</div>
|
||||
<div
|
||||
className={getClassName('__overlay-container')}
|
||||
onMouseDown={noMouseClose ? undefined : handleMouseDown}
|
||||
onMouseUp={noMouseClose ? undefined : handleMouseUp}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { storiesOf } from '@storybook/react';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { PropsType } from './Stories';
|
||||
import { Stories } from './Stories';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
@ -28,7 +29,17 @@ function createStory({
|
|||
timestamp,
|
||||
}: {
|
||||
attachment?: AttachmentType;
|
||||
group?: { title: string };
|
||||
group?: Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'id'
|
||||
| 'name'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
>;
|
||||
timestamp: number;
|
||||
}) {
|
||||
const replies = Math.random() > 0.5;
|
||||
|
@ -87,7 +98,7 @@ const getDefaultProps = (): PropsType => ({
|
|||
timestamp: Date.now() - 5 * durations.MINUTE,
|
||||
}),
|
||||
createStory({
|
||||
group: { title: 'BBQ in the park' },
|
||||
group: getDefaultConversation({ title: 'BBQ in the park' }),
|
||||
attachment: getAttachmentWithThumbnail(
|
||||
'/fixtures/nathan-anderson-316188-unsplash.jpg'
|
||||
),
|
||||
|
@ -102,7 +113,7 @@ const getDefaultProps = (): PropsType => ({
|
|||
timestamp: Date.now() - 164 * durations.MINUTE,
|
||||
}),
|
||||
createStory({
|
||||
group: { title: 'Breaking Signal for Science' },
|
||||
group: getDefaultConversation({ title: 'Breaking Signal for Science' }),
|
||||
attachment: getAttachmentWithThumbnail('/fixtures/kitten-2-64-64.jpg'),
|
||||
timestamp: Date.now() - 380 * durations.MINUTE,
|
||||
}),
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import FocusTrap from 'focus-trap-react';
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { ConversationStoryType, StoryViewType } from './StoryListItem';
|
||||
import type { ConversationStoryType } from './StoryListItem';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { PropsType as SmartStoryViewerPropsType } from '../state/smart/StoryViewer';
|
||||
import { StoriesPane } from './StoriesPane';
|
||||
|
@ -23,11 +23,6 @@ export type PropsType = {
|
|||
toggleStoriesView: () => unknown;
|
||||
};
|
||||
|
||||
type ViewingStoryType = {
|
||||
conversationId: string;
|
||||
stories: Array<StoryViewType>;
|
||||
};
|
||||
|
||||
export const Stories = ({
|
||||
hiddenStories,
|
||||
i18n,
|
||||
|
@ -39,8 +34,8 @@ export const Stories = ({
|
|||
toggleHideStories,
|
||||
toggleStoriesView,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [storiesToView, setStoriesToView] = useState<
|
||||
undefined | ViewingStoryType
|
||||
const [conversationIdToView, setConversationIdToView] = useState<
|
||||
undefined | string
|
||||
>();
|
||||
|
||||
const width = getWidthFromPreferredWidth(preferredWidthFromStorage, {
|
||||
|
@ -49,42 +44,35 @@ export const Stories = ({
|
|||
|
||||
return (
|
||||
<div className={classNames('Stories', themeClassName(Theme.Dark))}>
|
||||
{storiesToView &&
|
||||
{conversationIdToView &&
|
||||
renderStoryViewer({
|
||||
conversationId: storiesToView.conversationId,
|
||||
onClose: () => setStoriesToView(undefined),
|
||||
conversationId: conversationIdToView,
|
||||
onClose: () => setConversationIdToView(undefined),
|
||||
onNextUserStories: () => {
|
||||
const storyIndex = stories.findIndex(
|
||||
x => x.conversationId === storiesToView.conversationId
|
||||
x => x.conversationId === conversationIdToView
|
||||
);
|
||||
if (storyIndex >= stories.length - 1) {
|
||||
setStoriesToView(undefined);
|
||||
setConversationIdToView(undefined);
|
||||
return;
|
||||
}
|
||||
const nextStory = stories[storyIndex + 1];
|
||||
setStoriesToView({
|
||||
conversationId: nextStory.conversationId,
|
||||
stories: nextStory.stories,
|
||||
});
|
||||
setConversationIdToView(nextStory.conversationId);
|
||||
},
|
||||
onPrevUserStories: () => {
|
||||
const storyIndex = stories.findIndex(
|
||||
x => x.conversationId === storiesToView.conversationId
|
||||
x => x.conversationId === conversationIdToView
|
||||
);
|
||||
if (storyIndex === 0) {
|
||||
setStoriesToView(undefined);
|
||||
setConversationIdToView(undefined);
|
||||
return;
|
||||
}
|
||||
const prevStory = stories[storyIndex - 1];
|
||||
setStoriesToView({
|
||||
conversationId: prevStory.conversationId,
|
||||
stories: prevStory.stories,
|
||||
});
|
||||
setConversationIdToView(prevStory.conversationId);
|
||||
},
|
||||
stories: storiesToView.stories,
|
||||
})}
|
||||
<div className="Stories__pane" style={{ width }}>
|
||||
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
|
||||
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
|
||||
<div className="Stories__pane" style={{ width }}>
|
||||
<StoriesPane
|
||||
hiddenStories={hiddenStories}
|
||||
i18n={i18n}
|
||||
|
@ -96,10 +84,7 @@ export const Stories = ({
|
|||
const foundStory = stories[storyIndex];
|
||||
|
||||
if (foundStory) {
|
||||
setStoriesToView({
|
||||
conversationId,
|
||||
stories: foundStory.stories,
|
||||
});
|
||||
setConversationIdToView(conversationId);
|
||||
}
|
||||
}}
|
||||
openConversationInternal={openConversationInternal}
|
||||
|
@ -107,8 +92,8 @@ export const Stories = ({
|
|||
stories={stories}
|
||||
toggleHideStories={toggleHideStories}
|
||||
/>
|
||||
</FocusTrap>
|
||||
</div>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
<div className="Stories__placeholder">
|
||||
<div className="Stories__placeholder__stories" />
|
||||
{i18n('Stories__placeholder--text')}
|
||||
|
|
|
@ -111,8 +111,9 @@ export const StoriesPane = ({
|
|||
>
|
||||
{renderedStories.map(story => (
|
||||
<StoryListItem
|
||||
key={getNewestStory(story).timestamp}
|
||||
group={story.group}
|
||||
i18n={i18n}
|
||||
key={getNewestStory(story).timestamp}
|
||||
onClick={() => {
|
||||
onStoryClicked(story.conversationId);
|
||||
}}
|
||||
|
|
|
@ -63,7 +63,7 @@ story.add('My Story (many)', () => (
|
|||
story.add("Someone's story", () => (
|
||||
<StoryListItem
|
||||
{...getDefaultProps()}
|
||||
group={{ title: 'Sports Group' }}
|
||||
group={getDefaultConversation({ title: 'Sports Group' })}
|
||||
story={{
|
||||
attachment: fakeAttachment({
|
||||
thumbnail: fakeThumbnail('/fixtures/tina-rolf-269345-unsplash.jpg'),
|
||||
|
|
|
@ -15,7 +15,17 @@ import { getAvatarColor } from '../types/Colors';
|
|||
|
||||
export type ConversationStoryType = {
|
||||
conversationId: string;
|
||||
group?: Pick<ConversationType, 'title'>;
|
||||
group?: Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'id'
|
||||
| 'name'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
>;
|
||||
hasMultiple?: boolean;
|
||||
isHidden?: boolean;
|
||||
searchNames?: string; // This is just here to satisfy Fuse's types
|
||||
|
@ -24,6 +34,7 @@ export type ConversationStoryType = {
|
|||
|
||||
export type StoryViewType = {
|
||||
attachment?: AttachmentType;
|
||||
canReply?: boolean;
|
||||
hasReplies?: boolean;
|
||||
hasRepliesFromSelf?: boolean;
|
||||
isHidden?: boolean;
|
||||
|
|
|
@ -17,10 +17,14 @@ const i18n = setupI18n('en', enMessages);
|
|||
const story = storiesOf('Components/StoryViewer', module);
|
||||
|
||||
function getDefaultProps(): PropsType {
|
||||
const sender = getDefaultConversation();
|
||||
|
||||
return {
|
||||
conversationId: sender.id,
|
||||
getPreferredBadge: () => undefined,
|
||||
group: undefined,
|
||||
i18n,
|
||||
loadStoryReplies: action('loadStoryReplies'),
|
||||
markStoryRead: action('markStoryRead'),
|
||||
onClose: action('onClose'),
|
||||
onNextUserStories: action('onNextUserStories'),
|
||||
|
@ -33,18 +37,16 @@ function getDefaultProps(): PropsType {
|
|||
preferredReactionEmoji: ['❤️', '👍', '👎', '😂', '😮', '😢'],
|
||||
queueStoryDownload: action('queueStoryDownload'),
|
||||
renderEmojiPicker: () => <div />,
|
||||
replies: Math.floor(Math.random() * 20),
|
||||
stories: [
|
||||
{
|
||||
attachment: fakeAttachment({
|
||||
url: '/fixtures/snow.jpg',
|
||||
}),
|
||||
messageId: '123',
|
||||
sender: getDefaultConversation(),
|
||||
sender,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
views: Math.floor(Math.random() * 20),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { ConversationType } from '../state/ducks/conversations';
|
|||
import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
|
||||
import type { ReplyStateType } from '../types/Stories';
|
||||
import type { StoryViewType } from './StoryListItem';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { Intl } from './Intl';
|
||||
|
@ -22,9 +23,21 @@ import { isDownloaded, isDownloading } from '../types/Attachment';
|
|||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
|
||||
export type PropsType = {
|
||||
conversationId: string;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
group?: ConversationType;
|
||||
group?: Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'id'
|
||||
| 'name'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
>;
|
||||
i18n: LocalizerType;
|
||||
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
|
||||
markStoryRead: (mId: string) => unknown;
|
||||
onClose: () => unknown;
|
||||
onNextUserStories: () => unknown;
|
||||
|
@ -42,11 +55,10 @@ export type PropsType = {
|
|||
preferredReactionEmoji: Array<string>;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
recentEmojis?: Array<string>;
|
||||
replies?: number;
|
||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
||||
replyState?: ReplyStateType;
|
||||
skinTone?: number;
|
||||
stories: Array<StoryViewType>;
|
||||
views?: number;
|
||||
};
|
||||
|
||||
const CAPTION_BUFFER = 20;
|
||||
|
@ -54,9 +66,11 @@ const CAPTION_INITIAL_LENGTH = 200;
|
|||
const CAPTION_MAX_LENGTH = 700;
|
||||
|
||||
export const StoryViewer = ({
|
||||
conversationId,
|
||||
getPreferredBadge,
|
||||
group,
|
||||
i18n,
|
||||
loadStoryReplies,
|
||||
markStoryRead,
|
||||
onClose,
|
||||
onNextUserStories,
|
||||
|
@ -70,17 +84,16 @@ export const StoryViewer = ({
|
|||
queueStoryDownload,
|
||||
recentEmojis,
|
||||
renderEmojiPicker,
|
||||
replies,
|
||||
replyState,
|
||||
skinTone,
|
||||
stories,
|
||||
views,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [currentStoryIndex, setCurrentStoryIndex] = useState(0);
|
||||
const [storyDuration, setStoryDuration] = useState<number | undefined>();
|
||||
|
||||
const visibleStory = stories[currentStoryIndex];
|
||||
|
||||
const { attachment, messageId, timestamp } = visibleStory;
|
||||
const { attachment, canReply, messageId, timestamp } = visibleStory;
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
|
@ -240,6 +253,20 @@ export const StoryViewer = ({
|
|||
};
|
||||
}, [navigateStories]);
|
||||
|
||||
const isGroupStory = Boolean(group?.id);
|
||||
useEffect(() => {
|
||||
if (!isGroupStory) {
|
||||
return;
|
||||
}
|
||||
loadStoryReplies(conversationId, messageId);
|
||||
}, [conversationId, isGroupStory, loadStoryReplies, messageId]);
|
||||
|
||||
const replies =
|
||||
replyState && replyState.messageId === messageId ? replyState.replies : [];
|
||||
|
||||
const viewCount = 0;
|
||||
const replyCount = replies.length;
|
||||
|
||||
return (
|
||||
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
|
||||
<div className="StoryViewer">
|
||||
|
@ -366,49 +393,51 @@ export const StoryViewer = ({
|
|||
<div className="StoryViewer__actions">
|
||||
{isMe ? (
|
||||
<>
|
||||
{views &&
|
||||
(views === 1 ? (
|
||||
{viewCount &&
|
||||
(viewCount === 1 ? (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="MyStories__views--singular"
|
||||
components={[<strong>{views}</strong>]}
|
||||
components={[<strong>{viewCount}</strong>]}
|
||||
/>
|
||||
) : (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="MyStories__views--plural"
|
||||
components={[<strong>{views}</strong>]}
|
||||
components={[<strong>{viewCount}</strong>]}
|
||||
/>
|
||||
))}
|
||||
{views && replies && ' '}
|
||||
{replies &&
|
||||
(replies === 1 ? (
|
||||
{viewCount && replyCount && ' '}
|
||||
{replyCount &&
|
||||
(replyCount === 1 ? (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="MyStories__replies--singular"
|
||||
components={[<strong>{replies}</strong>]}
|
||||
components={[<strong>{replyCount}</strong>]}
|
||||
/>
|
||||
) : (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="MyStories__replies--plural"
|
||||
components={[<strong>{replies}</strong>]}
|
||||
components={[<strong>{replyCount}</strong>]}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="StoryViewer__reply"
|
||||
onClick={() => setHasReplyModal(true)}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
{i18n('StoryViewer__reply')}
|
||||
</button>
|
||||
canReply && (
|
||||
<button
|
||||
className="StoryViewer__reply"
|
||||
onClick={() => setHasReplyModal(true)}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
{i18n('StoryViewer__reply')}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{hasReplyModal && (
|
||||
{hasReplyModal && canReply && (
|
||||
<StoryViewsNRepliesModal
|
||||
authorTitle={title}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
|
@ -428,7 +457,7 @@ export const StoryViewer = ({
|
|||
preferredReactionEmoji={preferredReactionEmoji}
|
||||
recentEmojis={recentEmojis}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
replies={[]}
|
||||
replies={replies}
|
||||
skinTone={skinTone}
|
||||
storyPreviewAttachment={attachment}
|
||||
views={[]}
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { EmojiPickDataType } from './emoji/EmojiPicker';
|
|||
import type { InputApi } from './CompositionInput';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
|
||||
import type { ReplyType } from '../types/Stories';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { CompositionInput } from './CompositionInput';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
|
@ -23,26 +24,10 @@ import { Modal } from './Modal';
|
|||
import { Quote } from './conversation/Quote';
|
||||
import { ReactionPicker } from './conversation/ReactionPicker';
|
||||
import { Tabs } from './Tabs';
|
||||
import { Theme } from '../util/theme';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { getAvatarColor } from '../types/Colors';
|
||||
|
||||
type ReplyType = Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'isMe'
|
||||
| 'name'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
> & {
|
||||
body?: string;
|
||||
contactNameColor?: ContactNameColorType;
|
||||
reactionEmoji?: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
type ViewType = Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
|
@ -223,7 +208,7 @@ export const StoryViewsNRepliesModal = ({
|
|||
<div className="StoryViewsNRepliesModal__replies">
|
||||
{replies.map(reply =>
|
||||
reply.reactionEmoji ? (
|
||||
<div className="StoryViewsNRepliesModal__reaction">
|
||||
<div className="StoryViewsNRepliesModal__reaction" key={reply.id}>
|
||||
<div className="StoryViewsNRepliesModal__reaction--container">
|
||||
<Avatar
|
||||
acceptedMessageRequest={reply.acceptedMessageRequest}
|
||||
|
@ -257,7 +242,7 @@ export const StoryViewsNRepliesModal = ({
|
|||
<Emojify text={reply.reactionEmoji} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="StoryViewsNRepliesModal__reply">
|
||||
<div className="StoryViewsNRepliesModal__reply" key={reply.id}>
|
||||
<Avatar
|
||||
acceptedMessageRequest={reply.acceptedMessageRequest}
|
||||
avatarPath={reply.avatarPath}
|
||||
|
@ -272,7 +257,13 @@ export const StoryViewsNRepliesModal = ({
|
|||
size={AvatarSize.TWENTY_EIGHT}
|
||||
title={reply.title}
|
||||
/>
|
||||
<div className="StoryViewsNRepliesModal__message-bubble">
|
||||
<div
|
||||
className={classNames('StoryViewsNRepliesModal__message-bubble', {
|
||||
'StoryViewsNRepliesModal__message-bubble--doe': Boolean(
|
||||
reply.deletedForEveryone
|
||||
),
|
||||
})}
|
||||
>
|
||||
<div className="StoryViewsNRepliesModal__reply--title">
|
||||
<ContactName
|
||||
contactNameColor={reply.contactNameColor}
|
||||
|
@ -280,7 +271,14 @@ export const StoryViewsNRepliesModal = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<MessageBody i18n={i18n} text={String(reply.body)} />
|
||||
<MessageBody
|
||||
i18n={i18n}
|
||||
text={
|
||||
reply.deletedForEveryone
|
||||
? i18n('message--deletedForEveryone')
|
||||
: String(reply.body)
|
||||
}
|
||||
/>
|
||||
|
||||
<MessageTimestamp
|
||||
i18n={i18n}
|
||||
|
@ -373,6 +371,7 @@ export const StoryViewsNRepliesModal = ({
|
|||
})}
|
||||
onClose={onClose}
|
||||
useFocusTrap={!hasOnlyViewsElement}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
{tabsElement || (
|
||||
<>
|
||||
|
|
|
@ -47,7 +47,7 @@ type State = {
|
|||
|
||||
export type QuotedAttachmentType = Pick<
|
||||
AttachmentType,
|
||||
'contentType' | 'fileName' | 'isVoiceMessage' | 'thumbnail'
|
||||
'contentType' | 'fileName' | 'isVoiceMessage' | 'thumbnail' | 'textAttachment'
|
||||
>;
|
||||
|
||||
function validateQuote(quote: Props): boolean {
|
||||
|
@ -221,10 +221,11 @@ export class Quote extends React.Component<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { fileName, contentType } = attachment;
|
||||
const { fileName, contentType, textAttachment } = attachment;
|
||||
const isGenericFile =
|
||||
!GoogleChrome.isVideoTypeSupported(contentType) &&
|
||||
!GoogleChrome.isImageTypeSupported(contentType) &&
|
||||
!textAttachment &&
|
||||
!MIME.isAudio(contentType);
|
||||
|
||||
if (!isGenericFile) {
|
||||
|
@ -257,13 +258,18 @@ export class Quote extends React.Component<Props, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { contentType, thumbnail } = attachment;
|
||||
const { contentType, textAttachment, thumbnail } = attachment;
|
||||
const url = getUrl(thumbnail);
|
||||
|
||||
if (isViewOnce) {
|
||||
return this.renderIcon('view-once');
|
||||
}
|
||||
|
||||
// TODO DESKTOP-3433
|
||||
if (textAttachment) {
|
||||
return this.renderIcon('image');
|
||||
}
|
||||
|
||||
if (GoogleChrome.isVideoTypeSupported(contentType)) {
|
||||
return url && !imageBroken
|
||||
? this.renderImage(url, 'play')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue