diff options
Diffstat (limited to 'app')
41 files changed, 281 insertions, 132 deletions
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2f11ccb6f..9ca11d573 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -77,6 +77,18 @@ module ApplicationHelper content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end + def visibility_icon(status) + if status.public_visibility? + fa_icon('globe', title: I18n.t('statuses.visibilities.public')) + elsif status.unlisted_visibility? + fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted')) + elsif status.private_visibility? || status.limited_visibility? + fa_icon('lock', title: I18n.t('statuses.visibilities.private')) + elsif status.direct_visibility? + fa_icon('envelope', title: I18n.t('statuses.visibilities.direct')) + end + end + def custom_emoji_tag(custom_emoji, animate = true) if animate image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:") diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index 866a9902c..a51597cf3 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -15,11 +15,13 @@ module StatusesHelper end def media_summary(status) - attachments = { image: 0, video: 0 } + attachments = { image: 0, video: 0, audio: 0 } status.media_attachments.each do |media| if media.video? attachments[:video] += 1 + elsif media.audio? + attachments[:audio] += 1 else attachments[:image] += 1 end diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 5f42534ba..2dc961936 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -10,7 +10,7 @@ import StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; import AttachmentList from './attachment_list'; import Card from '../features/status/components/card'; -import { injectIntl, FormattedMessage } from 'react-intl'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; import { HotKeys } from 'react-hotkeys'; @@ -51,6 +51,13 @@ export const defaultMediaVisibility = (status) => { return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all'); }; +const messages = defineMessages({ + public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, +}); + export default @injectIntl class Status extends ImmutablePureComponent { @@ -416,6 +423,15 @@ class Status extends ImmutablePureComponent { statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />; } + const visibilityIconInfo = { + 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, + 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, + 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, + 'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) }, + }; + + const visibilityIcon = visibilityIconInfo[status.get('visibility')]; + return ( <HotKeys handlers={handlers}> <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}> @@ -425,6 +441,7 @@ class Status extends ImmutablePureComponent { <div className='status__expand' onClick={this.handleExpandClick} role='presentation' /> <div className='status__info'> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a> + <span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span> <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'> <div className='status__avatar'> diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index bebbbcb5a..a4aa27088 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -237,9 +237,6 @@ class StatusActionBar extends ImmutablePureComponent { const account = status.get('account'); let menu = []; - let reblogIcon = 'retweet'; - let replyIcon; - let replyTitle; menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); @@ -259,10 +256,6 @@ class StatusActionBar extends ImmutablePureComponent { if (status.getIn(['account', 'id']) === me) { if (publicStatus) { menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); - } else { - if (status.get('visibility') === 'private') { - menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick }); - } } menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); @@ -305,12 +298,8 @@ class StatusActionBar extends ImmutablePureComponent { } } - if (status.get('visibility') === 'direct') { - reblogIcon = 'envelope'; - } else if (status.get('visibility') === 'private') { - reblogIcon = 'lock'; - } - + let replyIcon; + let replyTitle; if (status.get('in_reply_to_id', null) === null) { replyIcon = 'reply'; replyTitle = intl.formatMessage(messages.reply); @@ -319,6 +308,19 @@ class StatusActionBar extends ImmutablePureComponent { replyTitle = intl.formatMessage(messages.replyAll); } + const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; + + let reblogTitle = ''; + if (status.get('reblogged')) { + reblogTitle = intl.formatMessage(messages.cancel_reblog_private); + } else if (publicStatus) { + reblogTitle = intl.formatMessage(messages.reblog); + } else if (reblogPrivate) { + reblogTitle = intl.formatMessage(messages.reblog_private); + } else { + reblogTitle = intl.formatMessage(messages.cannot_reblog); + } + const shareButton = ('share' in navigator) && publicStatus && ( <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} /> ); @@ -326,7 +328,7 @@ class StatusActionBar extends ImmutablePureComponent { return ( <div className='status__action-bar'> <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div> - <IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> + <IconButton className='status__action-bar-button' disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /> <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> {shareButton} diff --git a/app/javascript/mastodon/features/audio/index.js b/app/javascript/mastodon/features/audio/index.js index 9da143c96..5f6132f12 100644 --- a/app/javascript/mastodon/features/audio/index.js +++ b/app/javascript/mastodon/features/audio/index.js @@ -154,6 +154,7 @@ class Audio extends React.PureComponent { width: PropTypes.number, height: PropTypes.number, editable: PropTypes.bool, + fullscreen: PropTypes.bool, intl: PropTypes.object.isRequired, cacheWidth: PropTypes.func, }; @@ -180,7 +181,7 @@ class Audio extends React.PureComponent { _setDimensions () { const width = this.player.offsetWidth; - const height = width / (16/9); + const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9)); if (this.props.cacheWidth) { this.props.cacheWidth(width); @@ -291,8 +292,10 @@ class Audio extends React.PureComponent { } handleProgress = () => { - if (this.audio.buffered.length > 0) { - this.setState({ buffer: this.audio.buffered.end(0) / this.audio.duration * 100 }); + const lastTimeRange = this.audio.buffered.length - 1; + + if (lastTimeRange > -1) { + this.setState({ buffer: Math.ceil(this.audio.buffered.end(lastTimeRange) / this.audio.duration * 100) }); } } @@ -349,18 +352,18 @@ class Audio extends React.PureComponent { handleMouseMove = throttle(e => { const { x } = getPointerPosition(this.seek, e); - const currentTime = Math.floor(this.audio.duration * x); + const currentTime = this.audio.duration * x; if (!isNaN(currentTime)) { this.setState({ currentTime }, () => { this.audio.currentTime = currentTime; }); } - }, 60); + }, 15); handleTimeUpdate = () => { this.setState({ - currentTime: Math.floor(this.audio.currentTime), + currentTime: this.audio.currentTime, duration: Math.floor(this.audio.duration), }); } @@ -373,7 +376,7 @@ class Audio extends React.PureComponent { this.audio.volume = x; }); } - }, 60); + }, 15); handleScroll = throttle(() => { if (!this.canvas || !this.audio) { @@ -451,6 +454,7 @@ class Audio extends React.PureComponent { _renderCanvas () { requestAnimationFrame(() => { + this.handleTimeUpdate(); this._clear(); this._draw(); @@ -622,7 +626,7 @@ class Audio extends React.PureComponent { const progress = (currentTime / duration) * 100; return ( - <div className={classNames('audio-player', { editable, 'with-light-background': darkText })} ref={this.setPlayerRef} style={{ width: '100%', height: this.state.height || this.props.height }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> + <div className={classNames('audio-player', { editable, 'with-light-background': darkText })} ref={this.setPlayerRef} style={{ width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <audio src={src} ref={this.setAudioRef} @@ -630,7 +634,6 @@ class Audio extends React.PureComponent { onPlay={this.handlePlay} onPause={this.handlePause} onProgress={this.handleProgress} - onTimeUpdate={this.handleTimeUpdate} crossOrigin='anonymous' /> @@ -691,7 +694,7 @@ class Audio extends React.PureComponent { </div> <span className='video-player__time'> - <span className='video-player__time-current'>{formatTime(currentTime)}</span> + <span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span> <span className='video-player__time-sep'>/</span> <span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span> </span> diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js index d550019f4..9cb36167a 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.js +++ b/app/javascript/mastodon/features/compose/components/upload_button.js @@ -7,11 +7,9 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; const messages = defineMessages({ - upload: { id: 'upload_button.label', defaultMessage: 'Add media ({formats})' }, + upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' }, }); -const SUPPORTED_FORMATS = 'JPEG, PNG, GIF, WebM, MP4, MOV, OGG, WAV, MP3, FLAC'; - const makeMapStateToProps = () => { const mapStateToProps = state => ({ acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), @@ -60,11 +58,13 @@ class UploadButton extends ImmutablePureComponent { return null; } + const message = intl.formatMessage(messages.upload); + return ( <div className='compose-form__upload-button'> - <IconButton icon='paperclip' title={intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> + <IconButton icon='paperclip' title={message} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })}</span> + <span style={{ display: 'none' }}>{message}</span> <input key={resetFileKey} ref={this.setRef} diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index ba62d7b10..1c5d5ca0c 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -201,10 +201,6 @@ class ActionBar extends React.PureComponent { if (me === status.getIn(['account', 'id'])) { if (publicStatus) { menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); - } else { - if (status.get('visibility') === 'private') { - menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick }); - } } menu.push(null); @@ -261,14 +257,23 @@ class ActionBar extends React.PureComponent { replyIcon = 'reply-all'; } - let reblogIcon = 'retweet'; - if (status.get('visibility') === 'direct') reblogIcon = 'envelope'; - else if (status.get('visibility') === 'private') reblogIcon = 'lock'; + const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; + + let reblogTitle; + if (status.get('reblogged')) { + reblogTitle = intl.formatMessage(messages.cancel_reblog_private); + } else if (publicStatus) { + reblogTitle = intl.formatMessage(messages.reblog); + } else if (reblogPrivate) { + reblogTitle = intl.formatMessage(messages.reblog_private); + } else { + reblogTitle = intl.formatMessage(messages.cannot_reblog); + } return ( <div className='detailed-status__action-bar'> <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div> - <div className='detailed-status__button'><IconButton disabled={!publicStatus} active={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> + <div className='detailed-status__button'><IconButton disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div> {shareButton} <div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index 4442ab495..e35b1fd5f 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -191,7 +191,9 @@ export default class Card extends React.PureComponent { this.setState({ previewLoaded: true }); } - handleReveal = () => { + handleReveal = e => { + e.preventDefault(); + e.stopPropagation(); this.setState({ revealed: true }); } @@ -279,7 +281,7 @@ export default class Card extends React.PureComponent { } return ( - <div className={className} ref={this.setRef}> + <div className={className} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}> {embed} {!compact && description} </div> @@ -289,14 +291,12 @@ export default class Card extends React.PureComponent { <div className='status-card__image'> {canvas} {thumbnail} - {!revealed && spoilerButton} </div> ); } else { embed = ( <div className='status-card__image'> <Icon id='file-text' /> - {!revealed && spoilerButton} </div> ); } @@ -305,6 +305,7 @@ export default class Card extends React.PureComponent { <a href={card.get('url')} className={className} target='_blank' rel='noopener noreferrer' ref={this.setRef}> {embed} {description} + {!revealed && spoilerButton} </a> ); } diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 72d15ddf7..935e4207e 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -6,7 +6,7 @@ import DisplayName from '../../../components/display_name'; import StatusContent from '../../../components/status_content'; import MediaGallery from '../../../components/media_gallery'; import { Link } from 'react-router-dom'; -import { FormattedDate } from 'react-intl'; +import { injectIntl, defineMessages, FormattedDate } from 'react-intl'; import Card from './card'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Video from '../../video'; @@ -16,7 +16,15 @@ import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; import AnimatedNumber from 'mastodon/components/animated_number'; -export default class DetailedStatus extends ImmutablePureComponent { +const messages = defineMessages({ + public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, +}); + +export default @injectIntl +class DetailedStatus extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -92,7 +100,7 @@ export default class DetailedStatus extends ImmutablePureComponent { render () { const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; const outerStyle = { boxSizing: 'border-box' }; - const { compact } = this.props; + const { intl, compact } = this.props; if (!status) { return null; @@ -157,34 +165,44 @@ export default class DetailedStatus extends ImmutablePureComponent { } if (status.get('application')) { - applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></span>; + applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>; } - if (status.get('visibility') === 'direct') { - reblogIcon = 'envelope'; - } else if (status.get('visibility') === 'private') { - reblogIcon = 'lock'; - } + const visibilityIconInfo = { + 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, + 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, + 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, + 'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) }, + }; + + const visibilityIcon = visibilityIconInfo[status.get('visibility')]; + const visibilityLink = <React.Fragment> · <Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></React.Fragment>; if (['private', 'direct'].includes(status.get('visibility'))) { - reblogLink = <Icon id={reblogIcon} />; + reblogLink = ''; } else if (this.context.router) { reblogLink = ( - <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> - <Icon id={reblogIcon} /> - <span className='detailed-status__reblogs'> - <AnimatedNumber value={status.get('reblogs_count')} /> - </span> - </Link> + <React.Fragment> + <React.Fragment> · </React.Fragment> + <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> + <Icon id={reblogIcon} /> + <span className='detailed-status__reblogs'> + <AnimatedNumber value={status.get('reblogs_count')} /> + </span> + </Link> + </React.Fragment> ); } else { reblogLink = ( - <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> - <Icon id={reblogIcon} /> - <span className='detailed-status__reblogs'> - <AnimatedNumber value={status.get('reblogs_count')} /> - </span> - </a> + <React.Fragment> + <React.Fragment> · </React.Fragment> + <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> + <Icon id={reblogIcon} /> + <span className='detailed-status__reblogs'> + <AnimatedNumber value={status.get('reblogs_count')} /> + </span> + </a> + </React.Fragment> ); } @@ -210,7 +228,7 @@ export default class DetailedStatus extends ImmutablePureComponent { return ( <div style={outerStyle}> - <div ref={this.setRef} className={classNames('detailed-status', { compact })}> + <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}> <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'> <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div> <DisplayName account={status.get('account')} localDomain={this.props.domain} /> @@ -223,7 +241,7 @@ export default class DetailedStatus extends ImmutablePureComponent { <div className='detailed-status__meta'> <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'> <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> - </a>{applicationLink} · {reblogLink} · {favouriteLink} + </a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink} </div> </div> </div> diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 1f85375ff..135200a3d 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -177,15 +177,26 @@ class Video extends React.PureComponent { handlePlay = () => { this.setState({ paused: false }); + this._updateTime(); } handlePause = () => { this.setState({ paused: true }); } + _updateTime () { + requestAnimationFrame(() => { + this.handleTimeUpdate(); + + if (!this.state.paused) { + this._updateTime(); + } + }); + } + handleTimeUpdate = () => { this.setState({ - currentTime: Math.floor(this.video.currentTime), + currentTime: this.video.currentTime, duration: Math.floor(this.video.duration), }); } @@ -217,7 +228,7 @@ class Video extends React.PureComponent { this.video.volume = x; }); } - }, 60); + }, 15); handleMouseDown = e => { document.addEventListener('mousemove', this.handleMouseMove, true); @@ -245,13 +256,14 @@ class Video extends React.PureComponent { handleMouseMove = throttle(e => { const { x } = getPointerPosition(this.seek, e); - const currentTime = Math.floor(this.video.duration * x); + const currentTime = this.video.duration * x; if (!isNaN(currentTime)) { - this.video.currentTime = currentTime; - this.setState({ currentTime }); + this.setState({ currentTime }, () => { + this.video.currentTime = currentTime; + }); } - }, 60); + }, 15); togglePlay = () => { if (this.state.paused) { @@ -387,8 +399,10 @@ class Video extends React.PureComponent { } handleProgress = () => { - if (this.video.buffered.length > 0) { - this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 }); + const lastTimeRange = this.video.buffered.length - 1; + + if (lastTimeRange > -1) { + this.setState({ buffer: Math.ceil(this.video.buffered.end(lastTimeRange) / this.video.duration * 100) }); } } @@ -484,7 +498,6 @@ class Video extends React.PureComponent { onClick={this.togglePlay} onPlay={this.handlePlay} onPause={this.handlePause} - onTimeUpdate={this.handleTimeUpdate} onLoadedData={this.handleLoadedData} onProgress={this.handleProgress} onVolumeChange={this.handleVolumeChange} @@ -525,7 +538,7 @@ class Video extends React.PureComponent { {(detailed || fullscreen) && ( <span className='video-player__time'> - <span className='video-player__time-current'>{formatTime(currentTime)}</span> + <span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span> <span className='video-player__time-sep'>/</span> <span className='video-player__time-total'>{formatTime(duration)}</span> </span> diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json index 3989978a0..2d4f73975 100644 --- a/app/javascript/mastodon/locales/ast.json +++ b/app/javascript/mastodon/locales/ast.json @@ -422,7 +422,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "El borrador va perdese si coles de Mastodon.", "upload_area.title": "Arrastra y suelta pa xubir", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "La xuba de ficheros nun ta permitida con encuestes.", "upload_form.audio_description": "Descripción pa persones con perda auditiva", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 1d280d710..c7ca77376 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -1206,7 +1206,7 @@ { "descriptors": [ { - "defaultMessage": "Add media ({formats})", + "defaultMessage": "Add images, a video or an audio file", "id": "upload_button.label" } ], diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 1779f4713..b12409a8c 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -427,7 +427,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people with hearing loss", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 19054f716..cc82ee481 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -422,7 +422,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people with hearing loss", diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json index e26b607bb..3c7fe6df4 100644 --- a/app/javascript/mastodon/locales/hi.json +++ b/app/javascript/mastodon/locales/hi.json @@ -422,7 +422,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people with hearing loss", diff --git a/app/javascript/mastodon/locales/kn.json b/app/javascript/mastodon/locales/kn.json index 33fec4a4c..6c68862e0 100644 --- a/app/javascript/mastodon/locales/kn.json +++ b/app/javascript/mastodon/locales/kn.json @@ -422,7 +422,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people with hearing loss", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index 33fec4a4c..6c68862e0 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -422,7 +422,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people with hearing loss", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index d4288f96b..aa8bc183c 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -422,7 +422,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people with hearing loss", diff --git a/app/javascript/mastodon/locales/mk.json b/app/javascript/mastodon/locales/mk.json index 61202ec19..78cc18f53 100644 --- a/app/javascript/mastodon/locales/mk.json +++ b/app/javascript/mastodon/locales/mk.json @@ -422,7 +422,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people with hearing loss", diff --git a/app/javascript/mastodon/locales/ml.json b/app/javascript/mastodon/locales/ml.json index 7b74c10ee..68b89a585 100644 --- a/app/javascript/mastodon/locales/ml.json +++ b/app/javascript/mastodon/locales/ml.json @@ -422,7 +422,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people with hearing loss", diff --git a/app/javascript/mastodon/locales/mr.json b/app/javascript/mastodon/locales/mr.json index 46fd5acc5..2188d02b0 100644 --- a/app/javascript/mastodon/locales/mr.json +++ b/app/javascript/mastodon/locales/mr.json @@ -422,7 +422,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people with hearing loss", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index 9a9fc975a..b55fd4d43 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -422,7 +422,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people with hearing loss", diff --git a/app/javascript/mastodon/locales/ur.json b/app/javascript/mastodon/locales/ur.json index e3639d477..bff992983 100644 --- a/app/javascript/mastodon/locales/ur.json +++ b/app/javascript/mastodon/locales/ur.json @@ -422,7 +422,7 @@ "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media ({formats})", + "upload_button.label": "Add images, a video or an audio file", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.audio_description": "Describe for people with hearing loss", diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 78dea92b9..fea64f45c 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -171,9 +171,7 @@ $content-width: 840px; } .content { - padding: 20px 15px; - padding-top: 60px; - padding-left: 25px; + padding: 55px 15px 20px 25px; @media screen and (max-width: $no-columns-breakpoint) { max-width: none; @@ -184,7 +182,7 @@ $content-width: 840px; &-heading { display: flex; - padding-bottom: 40px; + padding-bottom: 36px; border-bottom: 1px solid lighten($ui-base-color, 8%); margin: -15px -15px 40px 0; @@ -215,7 +213,7 @@ $content-width: 840px; h2 { color: $secondary-text-color; font-size: 24px; - line-height: 28px; + line-height: 36px; font-weight: 400; @media screen and (max-width: $no-columns-breakpoint) { @@ -544,6 +542,16 @@ body, max-width: 100%; } +.simple_form { + .actions { + margin-top: 15px; + } + + .button { + font-size: 15px; + } +} + .batch-form-box { display: flex; flex-wrap: wrap; diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index a5dbe75fb..9e63b1d31 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -68,7 +68,32 @@ body { } &.player { - text-align: center; + padding: 0; + margin: 0; + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; + + & > div { + height: 100%; + } + + .video-player video { + width: 100%; + height: 100%; + max-height: 100vh; + } + + .media-gallery { + margin-top: 0; + height: 100% !important; + border-radius: 0; + } + + .media-gallery__item { + border-radius: 0; + } } &.embed { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 6b47ce211..0a6c20098 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1019,7 +1019,8 @@ } &.light { - .status__relative-time { + .status__relative-time, + .status__visibility-icon { color: $light-text-color; } @@ -1065,12 +1066,18 @@ } .status__relative-time, +.status__visibility-icon, .notification__relative_time { color: $dark-text-color; float: right; font-size: 14px; } +.status__visibility-icon { + margin-left: 4px; + margin-right: 4px; +} + .status__display-name { color: $dark-text-color; } @@ -3003,6 +3010,7 @@ a.account__display-name { } .status-card { + position: relative; display: flex; font-size: 14px; border: 1px solid lighten($ui-base-color, 8%); diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss index ecd166253..fbf26e30b 100644 --- a/app/javascript/styles/mastodon/rtl.scss +++ b/app/javascript/styles/mastodon/rtl.scss @@ -158,6 +158,7 @@ body.rtl { } .status__relative-time, + .status__visibility-icon, .activity-stream .status.light .status__header .status__meta { float: left; } diff --git a/app/javascript/styles/mastodon/statuses.scss b/app/javascript/styles/mastodon/statuses.scss index a8fd2936c..7ae1c5a24 100644 --- a/app/javascript/styles/mastodon/statuses.scss +++ b/app/javascript/styles/mastodon/statuses.scss @@ -140,6 +140,11 @@ .detailed-status { padding: 15px; + + .detailed-status__display-avatar .account__avatar { + width: 48px; + height: 48px; + } } .status { diff --git a/app/lib/language_detector.rb b/app/lib/language_detector.rb index 05a06726d..2cc8ac615 100644 --- a/app/lib/language_detector.rb +++ b/app/lib/language_detector.rb @@ -4,7 +4,7 @@ class LanguageDetector include Singleton WORDS_THRESHOLD = 4 - RELIABLE_CHARACTERS_RE = /[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}\p{Han}\p{Katakana}\p{Hiragana}\p{Hangul}]+/m + RELIABLE_CHARACTERS_RE = /[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}\p{Han}\p{Katakana}\p{Hiragana}\p{Hangul}\p{Thai}]+/m def initialize @identifier = CLD3::NNetLanguageIdentifier.new(1, 2048) diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index f789bdc55..3fe35ceaa 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -194,15 +194,17 @@ class MediaAttachment < ApplicationRecord x, y = (point.is_a?(Enumerable) ? point : point.split(',')).map(&:to_f) - meta = file.instance_read(:meta) || {} + meta = (file.instance_read(:meta) || {}).with_indifferent_access.slice(:focus, :original, :small) meta['focus'] = { 'x' => x, 'y' => y } file.instance_write(:meta, meta) end def focus - x = file.meta['focus']['x'] - y = file.meta['focus']['y'] + x = file.meta&.dig('focus', 'x') + y = file.meta&.dig('focus', 'y') + + return if x.nil? || y.nil? "#{x},#{y}" end @@ -219,12 +221,11 @@ class MediaAttachment < ApplicationRecord before_create :prepare_description, unless: :local? before_create :set_shortcode before_create :set_processing + before_create :set_meta before_post_process :set_type_and_extension before_post_process :check_video_dimensions - before_save :set_meta - class << self def supported_mime_types IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES @@ -306,15 +307,11 @@ class MediaAttachment < ApplicationRecord end def set_meta - meta = populate_meta - - return if meta == {} - - file.instance_write :meta, meta + file.instance_write :meta, populate_meta end def populate_meta - meta = file.instance_read(:meta) || {} + meta = (file.instance_read(:meta) || {}).with_indifferent_access.slice(:focus, :original, :small) file.queued_for_write.each do |style, file| meta[style] = style == :small || image? ? image_geometry(file) : video_metadata(file) diff --git a/app/views/accounts/_og.html.haml b/app/views/accounts/_og.html.haml index 839576372..6350d7ed0 100644 --- a/app/views/accounts/_og.html.haml +++ b/app/views/accounts/_og.html.haml @@ -7,7 +7,7 @@ = opengraph 'og:title', yield(:page_title).strip = opengraph 'og:description', description = opengraph 'og:image', full_asset_url(account.avatar.url(:original)) -= opengraph 'og:image:width', '120' -= opengraph 'og:image:height', '120' += opengraph 'og:image:width', '400' += opengraph 'og:image:height', '400' = opengraph 'twitter:card', 'summary' = opengraph 'profile:username', acct(account)[1..-1] diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 7592161c9..8eac226e0 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -38,7 +38,7 @@ = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.accounts.#{key}") .actions - %button= t('admin.accounts.search') + %button.button= t('admin.accounts.search') = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative' .table-wrapper diff --git a/app/views/admin/custom_emojis/index.html.haml b/app/views/admin/custom_emojis/index.html.haml index 45cb7bee0..b6cf7ba64 100644 --- a/app/views/admin/custom_emojis/index.html.haml +++ b/app/views/admin/custom_emojis/index.html.haml @@ -31,7 +31,7 @@ = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.custom_emojis.#{key}") .actions - %button= t('admin.accounts.search') + %button.button= t('admin.accounts.search') = link_to t('admin.accounts.reset'), admin_custom_emojis_path, class: 'button negative' = form_for(@form, url: batch_admin_custom_emojis_path) do |f| diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index a73b8dc92..696ba3c7f 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -27,7 +27,7 @@ = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}") .actions - %button= t('admin.accounts.search') + %button.button= t('admin.accounts.search') = link_to t('admin.accounts.reset'), admin_instances_path, class: 'button negative' %hr.spacer/ diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 2149fcc46..bb441380e 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -18,7 +18,7 @@ = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.reports.#{key}") .actions - %button= t('admin.accounts.search') + %button.button= t('admin.accounts.search') = link_to t('admin.accounts.reset'), admin_reports_path, class: 'button negative' - @reports.group_by(&:target_account_id).each do |target_account_id, reports| diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml index e64802275..72eef18a9 100644 --- a/app/views/admin/tags/index.html.haml +++ b/app/views/admin/tags/index.html.haml @@ -33,7 +33,7 @@ = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.tags.#{key}") .actions - %button= t('admin.accounts.search') + %button.button= t('admin.accounts.search') = link_to t('admin.accounts.reset'), admin_tags_path, class: 'button negative' %hr.spacer/ diff --git a/app/views/media/player.html.haml b/app/views/media/player.html.haml index ea868b3f6..3d308ee69 100644 --- a/app/views/media/player.html.haml +++ b/app/views/media/player.html.haml @@ -1,2 +1,16 @@ -%video{ poster: @media_attachment.file.url(:small), preload: 'auto', autoplay: 'autoplay', muted: 'muted', loop: 'loop', controls: 'controls', style: "width: #{@media_attachment.file.meta.dig('original', 'width')}px; height: #{@media_attachment.file.meta.dig('original', 'height')}px" } - %source{ src: @media_attachment.file.url(:original), type: @media_attachment.file_content_type } +- content_for :header_tags do + = render_initial_state + = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' + +- if @media_attachment.video? + = react_component :video, src: @media_attachment.file.url(:original), preview: @media_attachment.file.url(:small), blurhash: @media_attachment.blurhash, width: 670, height: 380, editable: true, detailed: true, inline: true, alt: @media_attachment.description do + %video{ controls: 'controls' } + %source{ src: @media_attachment.file.url(:original) } +- elsif @media_attachment.gifv? + = react_component :media_gallery, height: 380, standalone: true, autoplay: true, media: [ActiveModelSerializers::SerializableResource.new(@media_attachment, serializer: REST::MediaAttachmentSerializer).as_json] do + %video{ autoplay: 'autoplay', muted: 'muted', loop: 'loop' } + %source{ src: @media_attachment.file.url(:original) } +- elsif @media_attachment.audio? + = react_component :audio, src: @media_attachment.file.url(:original), poster: full_asset_url(@media_attachment.account.avatar_static_url), width: 670, height: 380, fullscreen: true, alt: @media_attachment.description, duration: @media_attachment.file.meta.dig(:original, :duration) do + %audio{ controls: 'controls' } + %source{ src: @media_attachment.file.url(:original) } diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index 8e409846a..684dd08d1 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -1,4 +1,4 @@ -.detailed-status.detailed-status--flex +.detailed-status.detailed-status--flex{ class: "detailed-status-#{status.visibility}" } .p-author.h-card = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'detailed-status__display-name u-url', target: stream_link_target, rel: 'noopener' do .detailed-status__display-avatar @@ -33,7 +33,7 @@ = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.media_attachments.first.audio? - audio = status.media_attachments.first - = react_component :audio, src: audio.file.url(:original), height: 130, alt: audio.description, preload: true, duration: audio.file.meta.dig(:original, :duration) do + = react_component :audio, src: audio.file.url(:original), poster: full_asset_url(status.account.avatar_static_url), width: 670, height: 380, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - else = react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do @@ -47,6 +47,9 @@ = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) · + %span.detailed-status__visibility-icon + = visibility_icon status + · - if status.application && @account.user&.setting_show_application - if status.application.website.blank? %strong.detailed-status__application= status.application.name @@ -61,18 +64,12 @@ %span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true = " " · - - if status.direct_visibility? - %span.detailed-status__link< - = fa_icon('envelope') - - elsif status.private_visibility? || status.limited_visibility? - %span.detailed-status__link< - = fa_icon('lock') - - else + - if status.public_visibility? || status.unlisted_visibility? = link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do = fa_icon('retweet') %span.detailed-status__reblogs>= number_to_human status.reblogs_count, strip_insignificant_zeros: true = " " - · + · = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do = fa_icon('star') %span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true diff --git a/app/views/statuses/_og_image.html.haml b/app/views/statuses/_og_image.html.haml index 67f9274b6..c8b6147ef 100644 --- a/app/views/statuses/_og_image.html.haml +++ b/app/views/statuses/_og_image.html.haml @@ -27,12 +27,25 @@ = opengraph 'og:video:height', media.file.meta.dig('original', 'height') = opengraph 'twitter:player:width', media.file.meta.dig('original', 'width') = opengraph 'twitter:player:height', media.file.meta.dig('original', 'height') + - elsif media.audio? + - player_card = true + = opengraph 'og:image', full_asset_url(account.avatar.url(:original)) + = opengraph 'og:image:width', '400' + = opengraph 'og:image:height','400' + = opengraph 'og:audio', full_asset_url(media.file.url(:original)) + = opengraph 'og:audio:secure_url', full_asset_url(media.file.url(:original)) + = opengraph 'og:audio:type', media.file_content_type + = opengraph 'twitter:player', medium_player_url(media) + = opengraph 'twitter:player:stream', full_asset_url(media.file.url(:original)) + = opengraph 'twitter:player:stream:content_type', media.file_content_type + = opengraph 'twitter:player:width', '670' + = opengraph 'twitter:player:height', '380' - if player_card = opengraph 'twitter:card', 'player' - else = opengraph 'twitter:card', 'summary_large_image' - else = opengraph 'og:image', full_asset_url(account.avatar.url(:original)) - = opengraph 'og:image:width', '120' - = opengraph 'og:image:height','120' + = opengraph 'og:image:width', '400' + = opengraph 'og:image:height','400' = opengraph 'twitter:card', 'summary' diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index 7a0262c9d..cf6d62b2a 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -1,8 +1,10 @@ -.status +.status{ class: "status-#{status.visibility}" } .status__info = link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do %time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) %data.dt-published{ value: status.created_at.to_time.iso8601 } + %span.status__visibility-icon + = visibility_icon status .p-author.h-card = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener noreferrer' do @@ -37,7 +39,7 @@ = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.media_attachments.first.audio? - audio = status.media_attachments.first - = react_component :audio, src: audio.file.url(:original), height: 110, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) do + = react_component :audio, src: audio.file.url(:original), poster: full_asset_url(status.account.avatar_static_url), width: 610, height: 343, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - else = react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do diff --git a/app/workers/post_process_media_worker.rb b/app/workers/post_process_media_worker.rb index 148ae5e2b..73f9ae2bf 100644 --- a/app/workers/post_process_media_worker.rb +++ b/app/workers/post_process_media_worker.rb @@ -25,8 +25,14 @@ class PostProcessMediaWorker media_attachment = MediaAttachment.find(media_attachment_id) media_attachment.processing = :in_progress media_attachment.save + + # Because paperclip-av-transcover overwrites this attribute + # we will save it here and restore it after reprocess is done + previous_meta = media_attachment.file_meta + media_attachment.file.reprocess!(:original) media_attachment.processing = :complete + media_attachment.file_meta = previous_meta media_attachment.save rescue ActiveRecord::RecordNotFound true |