You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
390 lines
12 KiB
390 lines
12 KiB
/* eslint-disable @typescript-eslint/no-unsafe-member-access, |
|
@typescript-eslint/no-unsafe-call, |
|
@typescript-eslint/no-explicit-any, |
|
@typescript-eslint/no-unsafe-assignment */ |
|
|
|
import type { CSSProperties } from 'react'; |
|
import { useState, useRef, useCallback } from 'react'; |
|
|
|
import { FormattedDate, FormattedMessage } from 'react-intl'; |
|
|
|
import classNames from 'classnames'; |
|
import { Link } from 'react-router-dom'; |
|
|
|
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; |
|
import { AnimatedNumber } from 'mastodon/components/animated_number'; |
|
import { ContentWarning } from 'mastodon/components/content_warning'; |
|
import EditedTimestamp from 'mastodon/components/edited_timestamp'; |
|
import type { StatusLike } from 'mastodon/components/hashtag_bar'; |
|
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar'; |
|
import { Icon } from 'mastodon/components/icon'; |
|
import { IconLogo } from 'mastodon/components/logo'; |
|
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; |
|
import { VisibilityIcon } from 'mastodon/components/visibility_icon'; |
|
|
|
import { Avatar } from '../../../components/avatar'; |
|
import { DisplayName } from '../../../components/display_name'; |
|
import MediaGallery from '../../../components/media_gallery'; |
|
import StatusContent from '../../../components/status_content'; |
|
import Audio from '../../audio'; |
|
import scheduleIdleTask from '../../ui/util/schedule_idle_task'; |
|
import Video from '../../video'; |
|
|
|
import Card from './card'; |
|
|
|
interface VideoModalOptions { |
|
startTime: number; |
|
autoPlay?: boolean; |
|
defaultVolume: number; |
|
componentIndex: number; |
|
} |
|
|
|
export const DetailedStatus: React.FC<{ |
|
status: any; |
|
onOpenMedia?: (status: any, index: number, lang: string) => void; |
|
onOpenVideo?: (status: any, lang: string, options: VideoModalOptions) => void; |
|
onTranslate?: (status: any) => void; |
|
measureHeight?: boolean; |
|
onHeightChange?: () => void; |
|
domain: string; |
|
showMedia?: boolean; |
|
withLogo?: boolean; |
|
pictureInPicture: any; |
|
onToggleHidden?: (status: any) => void; |
|
onToggleMediaVisibility?: () => void; |
|
}> = ({ |
|
status, |
|
onOpenMedia, |
|
onOpenVideo, |
|
onTranslate, |
|
measureHeight, |
|
onHeightChange, |
|
domain, |
|
showMedia, |
|
withLogo, |
|
pictureInPicture, |
|
onToggleMediaVisibility, |
|
onToggleHidden, |
|
}) => { |
|
const properStatus = status?.get('reblog') ?? status; |
|
const [height, setHeight] = useState(0); |
|
const nodeRef = useRef<HTMLDivElement>(); |
|
|
|
const handleOpenVideo = useCallback( |
|
(options: VideoModalOptions) => { |
|
const lang = (status.getIn(['translation', 'language']) || |
|
status.get('language')) as string; |
|
if (onOpenVideo) |
|
onOpenVideo(status.getIn(['media_attachments', 0]), lang, options); |
|
}, |
|
[onOpenVideo, status], |
|
); |
|
|
|
const handleExpandedToggle = useCallback(() => { |
|
if (onToggleHidden) onToggleHidden(status); |
|
}, [onToggleHidden, status]); |
|
|
|
const _measureHeight = useCallback( |
|
(heightJustChanged?: boolean) => { |
|
if (measureHeight && nodeRef.current) { |
|
scheduleIdleTask(() => { |
|
if (nodeRef.current) |
|
setHeight(Math.ceil(nodeRef.current.scrollHeight) + 1); |
|
}); |
|
|
|
if (onHeightChange && heightJustChanged) { |
|
onHeightChange(); |
|
} |
|
} |
|
}, |
|
[onHeightChange, measureHeight, setHeight], |
|
); |
|
|
|
const handleRef = useCallback( |
|
(c: HTMLDivElement) => { |
|
nodeRef.current = c; |
|
_measureHeight(); |
|
}, |
|
[_measureHeight], |
|
); |
|
|
|
const handleTranslate = useCallback(() => { |
|
if (onTranslate) onTranslate(status); |
|
}, [onTranslate, status]); |
|
|
|
if (!properStatus) { |
|
return null; |
|
} |
|
|
|
let media; |
|
let applicationLink; |
|
let reblogLink; |
|
let attachmentAspectRatio; |
|
|
|
if (properStatus.get('media_attachments').getIn([0, 'type']) === 'video') { |
|
attachmentAspectRatio = `${properStatus.get('media_attachments').getIn([0, 'meta', 'original', 'width'])} / ${properStatus.get('media_attachments').getIn([0, 'meta', 'original', 'height'])}`; |
|
} else if ( |
|
properStatus.get('media_attachments').getIn([0, 'type']) === 'audio' |
|
) { |
|
attachmentAspectRatio = '16 / 9'; |
|
} else { |
|
attachmentAspectRatio = |
|
properStatus.get('media_attachments').size === 1 && |
|
properStatus |
|
.get('media_attachments') |
|
.getIn([0, 'meta', 'small', 'aspect']) |
|
? properStatus |
|
.get('media_attachments') |
|
.getIn([0, 'meta', 'small', 'aspect']) |
|
: '3 / 2'; |
|
} |
|
|
|
const outerStyle = { boxSizing: 'border-box' } as CSSProperties; |
|
|
|
if (measureHeight) { |
|
outerStyle.height = height; |
|
} |
|
|
|
const language = |
|
status.getIn(['translation', 'language']) || status.get('language'); |
|
|
|
if (pictureInPicture.get('inUse')) { |
|
media = <PictureInPicturePlaceholder aspectRatio={attachmentAspectRatio} />; |
|
} else if (status.get('media_attachments').size > 0) { |
|
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { |
|
const attachment = status.getIn(['media_attachments', 0]); |
|
const description = |
|
attachment.getIn(['translation', 'description']) || |
|
attachment.get('description'); |
|
|
|
media = ( |
|
<Audio |
|
src={attachment.get('url')} |
|
alt={description} |
|
lang={language} |
|
duration={attachment.getIn(['meta', 'original', 'duration'], 0)} |
|
poster={ |
|
attachment.get('preview_url') || |
|
status.getIn(['account', 'avatar_static']) |
|
} |
|
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])} |
|
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])} |
|
accentColor={attachment.getIn(['meta', 'colors', 'accent'])} |
|
sensitive={status.get('sensitive')} |
|
visible={showMedia} |
|
blurhash={attachment.get('blurhash')} |
|
height={150} |
|
onToggleVisibility={onToggleMediaVisibility} |
|
/> |
|
); |
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { |
|
const attachment = status.getIn(['media_attachments', 0]); |
|
const description = |
|
attachment.getIn(['translation', 'description']) || |
|
attachment.get('description'); |
|
|
|
media = ( |
|
<Video |
|
preview={attachment.get('preview_url')} |
|
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])} |
|
aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`} |
|
blurhash={attachment.get('blurhash')} |
|
src={attachment.get('url')} |
|
alt={description} |
|
lang={language} |
|
width={300} |
|
height={150} |
|
onOpenVideo={handleOpenVideo} |
|
sensitive={status.get('sensitive')} |
|
visible={showMedia} |
|
onToggleVisibility={onToggleMediaVisibility} |
|
/> |
|
); |
|
} else { |
|
media = ( |
|
<MediaGallery |
|
standalone |
|
sensitive={status.get('sensitive')} |
|
media={status.get('media_attachments')} |
|
lang={language} |
|
height={300} |
|
onOpenMedia={onOpenMedia} |
|
visible={showMedia} |
|
onToggleVisibility={onToggleMediaVisibility} |
|
/> |
|
); |
|
} |
|
} else if (status.get('spoiler_text').length === 0) { |
|
media = ( |
|
<Card |
|
sensitive={status.get('sensitive')} |
|
onOpenMedia={onOpenMedia} |
|
card={status.get('card', null)} |
|
/> |
|
); |
|
} |
|
|
|
if (status.get('application')) { |
|
applicationLink = ( |
|
<> |
|
· |
|
<a |
|
className='detailed-status__application' |
|
href={status.getIn(['application', 'website'])} |
|
target='_blank' |
|
rel='noopener noreferrer' |
|
> |
|
{status.getIn(['application', 'name'])} |
|
</a> |
|
</> |
|
); |
|
} |
|
|
|
const visibilityLink = ( |
|
<> |
|
·<VisibilityIcon visibility={status.get('visibility')} /> |
|
</> |
|
); |
|
|
|
if (['private', 'direct'].includes(status.get('visibility') as string)) { |
|
reblogLink = ''; |
|
} else { |
|
reblogLink = ( |
|
<Link |
|
to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} |
|
className='detailed-status__link' |
|
> |
|
<span className='detailed-status__reblogs'> |
|
<AnimatedNumber value={status.get('reblogs_count')} /> |
|
</span> |
|
<FormattedMessage |
|
id='status.reblogs' |
|
defaultMessage='{count, plural, one {boost} other {boosts}}' |
|
values={{ count: status.get('reblogs_count') }} |
|
/> |
|
</Link> |
|
); |
|
} |
|
|
|
const favouriteLink = ( |
|
<Link |
|
to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} |
|
className='detailed-status__link' |
|
> |
|
<span className='detailed-status__favorites'> |
|
<AnimatedNumber value={status.get('favourites_count')} /> |
|
</span> |
|
<FormattedMessage |
|
id='status.favourites' |
|
defaultMessage='{count, plural, one {favorite} other {favorites}}' |
|
values={{ count: status.get('favourites_count') }} |
|
/> |
|
</Link> |
|
); |
|
|
|
const { statusContentProps, hashtagBar } = getHashtagBarForStatus( |
|
status as StatusLike, |
|
); |
|
const expanded = |
|
!status.get('hidden') || status.get('spoiler_text').length === 0; |
|
|
|
return ( |
|
<div style={outerStyle}> |
|
<div ref={handleRef} className={classNames('detailed-status')}> |
|
{status.get('visibility') === 'direct' && ( |
|
<div className='status__prepend'> |
|
<div className='status__prepend-icon-wrapper'> |
|
<Icon |
|
id='at' |
|
icon={AlternateEmailIcon} |
|
className='status__prepend-icon' |
|
/> |
|
</div> |
|
<FormattedMessage |
|
id='status.direct_indicator' |
|
defaultMessage='Private mention' |
|
/> |
|
</div> |
|
)} |
|
<Link |
|
to={`/@${status.getIn(['account', 'acct'])}`} |
|
data-hover-card-account={status.getIn(['account', 'id'])} |
|
className='detailed-status__display-name' |
|
> |
|
<div className='detailed-status__display-avatar'> |
|
<Avatar account={status.get('account')} size={46} /> |
|
</div> |
|
<DisplayName account={status.get('account')} localDomain={domain} /> |
|
{withLogo && ( |
|
<> |
|
<div className='spacer' /> |
|
<IconLogo /> |
|
</> |
|
)} |
|
</Link> |
|
|
|
{status.get('spoiler_text').length > 0 && ( |
|
<ContentWarning |
|
text={ |
|
status.getIn(['translation', 'spoilerHtml']) || |
|
status.get('spoilerHtml') |
|
} |
|
expanded={expanded} |
|
onClick={handleExpandedToggle} |
|
/> |
|
)} |
|
|
|
{expanded && ( |
|
<> |
|
<StatusContent |
|
status={status} |
|
onTranslate={handleTranslate} |
|
{...(statusContentProps as any)} |
|
/> |
|
|
|
{media} |
|
{hashtagBar} |
|
</> |
|
)} |
|
|
|
<div className='detailed-status__meta'> |
|
<div className='detailed-status__meta__line'> |
|
<a |
|
className='detailed-status__datetime' |
|
href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} |
|
target='_blank' |
|
rel='noopener noreferrer' |
|
> |
|
<FormattedDate |
|
value={new Date(status.get('created_at') as string)} |
|
year='numeric' |
|
month='short' |
|
day='2-digit' |
|
hour='2-digit' |
|
minute='2-digit' |
|
/> |
|
</a> |
|
|
|
{visibilityLink} |
|
{applicationLink} |
|
</div> |
|
|
|
{status.get('edited_at') && ( |
|
<div className='detailed-status__meta__line'> |
|
<EditedTimestamp |
|
statusId={status.get('id')} |
|
timestamp={status.get('edited_at')} |
|
/> |
|
</div> |
|
)} |
|
|
|
<div className='detailed-status__meta__line'> |
|
{reblogLink} |
|
{reblogLink && <>·</>} |
|
{favouriteLink} |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
};
|
|
|