diff options
Diffstat (limited to 'app')
24 files changed, 132 insertions, 42 deletions
diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index 7afa822ed..2e21ce6a0 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:favourites' } before_action :require_user! - before_action :set_status + before_action :set_status, only: [:create] def create FavouriteService.new.call(current_account, @status) @@ -13,8 +13,19 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController end def destroy - UnfavouriteWorker.perform_async(current_account.id, @status.id) + fav = current_account.favourites.find_by(status_id: params[:status_id]) + + if fav + @status = fav.status + UnfavouriteWorker.perform_async(current_account.id, @status.id) + else + @status = Status.find(params[:status_id]) + authorize @status, :show? + end + render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }) + rescue Mastodon::NotPermittedError + not_found end private diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 7273191b2..9f12df773 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -345,7 +345,7 @@ export function setupBrowserNotifications() { if ('Notification' in window && 'permissions' in navigator) { navigator.permissions.query({ name: 'notifications' }).then((status) => { status.onchange = () => dispatch(setBrowserPermission(Notification.permission)); - }); + }).catch(console.warn); } }; } diff --git a/app/javascript/flavours/glitch/components/modal_root.js b/app/javascript/flavours/glitch/components/modal_root.js index 8d73a1e40..13a8e8702 100644 --- a/app/javascript/flavours/glitch/components/modal_root.js +++ b/app/javascript/flavours/glitch/components/modal_root.js @@ -14,11 +14,7 @@ export default class ModalRoot extends React.PureComponent { noEsc: PropTypes.bool, }; - state = { - revealed: !!this.props.children, - }; - - activeElement = this.state.revealed ? document.activeElement : null; + activeElement = this.props.children ? document.activeElement : null; handleKeyUp = (e) => { if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) @@ -59,8 +55,6 @@ export default class ModalRoot extends React.PureComponent { this.activeElement = document.activeElement; this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); - } else if (!nextProps.children) { - this.setState({ revealed: false }); } } @@ -80,11 +74,8 @@ export default class ModalRoot extends React.PureComponent { this.handleModalClose(); } - if (this.props.children) { - requestAnimationFrame(() => { - this.setState({ revealed: true }); - }); - if (!prevProps.children) this.handleModalOpen(); + if (this.props.children && !prevProps.children) { + this.handleModalOpen(); } } @@ -121,7 +112,6 @@ export default class ModalRoot extends React.PureComponent { render () { const { children, onClose } = this.props; - const { revealed } = this.state; const visible = !!children; if (!visible) { @@ -131,7 +121,7 @@ export default class ModalRoot extends React.PureComponent { } return ( - <div className='modal-root' ref={this.setRef} style={{ opacity: revealed ? 1 : 0 }}> + <div className='modal-root' ref={this.setRef}> <div style={{ pointerEvents: visible ? 'auto' : 'none' }}> <div role='presentation' className='modal-root__overlay' onClick={onClose} /> <div role='dialog' className='modal-root__container'>{children}</div> diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 1b7dce4c4..fcbf4be8c 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -628,6 +628,7 @@ class Status extends ImmutablePureComponent { <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > {Component => (<Component preview={attachment.get('preview_url')} + frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])} blurhash={attachment.get('blurhash')} src={attachment.get('url')} alt={attachment.get('description')} diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 04d350bcb..40bf370f3 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -160,6 +160,7 @@ export default class DetailedStatus extends ImmutablePureComponent { media = ( <Video preview={attachment.get('preview_url')} + frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])} blurhash={attachment.get('blurhash')} src={attachment.get('url')} alt={attachment.get('description')} diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js index 5de3e26d5..87a7de851 100644 --- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js @@ -18,7 +18,9 @@ import { length } from 'stringz'; import { Tesseract as fetchTesseract } from 'flavours/glitch/util/async-components'; import GIFV from 'flavours/glitch/components/gifv'; import { me } from 'flavours/glitch/util/initial_state'; +// eslint-disable-next-line import/no-extraneous-dependencies import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js'; +// eslint-disable-next-line import/extensions import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js'; import { assetHost } from 'flavours/glitch/util/config'; @@ -386,6 +388,7 @@ class FocalPointModal extends ImmutablePureComponent { {media.get('type') === 'video' && ( <Video preview={media.get('preview_url')} + frameRate={media.getIn(['meta', 'original', 'frame_rate'])} blurhash={media.get('blurhash')} src={media.get('url')} detailed diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js index c8d2a81b0..b0a4f3f03 100644 --- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js @@ -40,6 +40,7 @@ export default class VideoModal extends ImmutablePureComponent { <div className='video-modal__container'> <Video preview={media.get('preview_url')} + frameRate={media.getIn(['meta', 'original', 'frame_rate'])} blurhash={media.get('blurhash')} src={media.get('url')} currentTime={options.startTime} diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index ea40b6073..92dcaf473 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -98,6 +98,7 @@ class Video extends React.PureComponent { static propTypes = { preview: PropTypes.string, + frameRate: PropTypes.string, src: PropTypes.string.isRequired, alt: PropTypes.string, width: PropTypes.number, @@ -125,6 +126,10 @@ class Video extends React.PureComponent { muted: PropTypes.bool, }; + static defaultProps = { + frameRate: 25, + }; + state = { currentTime: 0, duration: 0, @@ -298,7 +303,7 @@ class Video extends React.PureComponent { } handleKeyDown = e => { - const frameTime = 1 / 25; + const frameTime = 1 / this.getFrameRate(); switch(e.key) { case 'k': @@ -531,6 +536,17 @@ class Video extends React.PureComponent { this.props.onCloseVideo(); } + getFrameRate () { + if (this.props.frameRate && isNaN(this.props.frameRate)) { + // The frame rate is returned as a fraction string so we + // need to convert it to a number + + return this.props.frameRate.split('/').reduce((p, c) => p / c); + } + + return this.props.frameRate || 25; + } + render () { const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, link, editable, blurhash } = this.props; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index bc0965864..421cbec00 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -4,8 +4,6 @@ .modal-root { position: relative; - transition: opacity 0.3s linear; - will-change: opacity; z-index: 9999; } diff --git a/app/javascript/flavours/glitch/util/notifications.js b/app/javascript/flavours/glitch/util/notifications.js index ab119c2e3..7634cac21 100644 --- a/app/javascript/flavours/glitch/util/notifications.js +++ b/app/javascript/flavours/glitch/util/notifications.js @@ -3,6 +3,7 @@ const checkNotificationPromise = () => { try { + // eslint-disable-next-line promise/catch-or-return Notification.requestPermission().then(); } catch(e) { return false; @@ -22,7 +23,7 @@ const handlePermission = (permission, callback) => { export const requestNotificationPermission = (callback) => { if (checkNotificationPromise()) { - Notification.requestPermission().then((permission) => handlePermission(permission, callback)); + Notification.requestPermission().then((permission) => handlePermission(permission, callback)).catch(console.warn); } else { Notification.requestPermission((permission) => handlePermission(permission, callback)); } diff --git a/app/javascript/flavours/glitch/util/stream.js b/app/javascript/flavours/glitch/util/stream.js index cf1388aed..c6d12cd6f 100644 --- a/app/javascript/flavours/glitch/util/stream.js +++ b/app/javascript/flavours/glitch/util/stream.js @@ -16,7 +16,7 @@ let sharedConnection; * @property {function(): void} onDisconnect */ - /** +/** * @typedef StreamEvent * @property {string} event * @property {object} payload diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index d40b65745..93e18fba9 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -257,7 +257,7 @@ export function setupBrowserNotifications() { if ('Notification' in window && 'permissions' in navigator) { navigator.permissions.query({ name: 'notifications' }).then((status) => { status.onchange = () => dispatch(setBrowserPermission(Notification.permission)); - }); + }).catch(console.warn); } }; } diff --git a/app/javascript/mastodon/components/modal_root.js b/app/javascript/mastodon/components/modal_root.js index 6297b5e29..fe573ffda 100644 --- a/app/javascript/mastodon/components/modal_root.js +++ b/app/javascript/mastodon/components/modal_root.js @@ -9,11 +9,7 @@ export default class ModalRoot extends React.PureComponent { onClose: PropTypes.func.isRequired, }; - state = { - revealed: !!this.props.children, - }; - - activeElement = this.state.revealed ? document.activeElement : null; + activeElement = this.props.children ? document.activeElement : null; handleKeyUp = (e) => { if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) @@ -53,8 +49,6 @@ export default class ModalRoot extends React.PureComponent { this.activeElement = document.activeElement; this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); - } else if (!nextProps.children) { - this.setState({ revealed: false }); } } @@ -72,11 +66,6 @@ export default class ModalRoot extends React.PureComponent { console.error(error); }); } - if (this.props.children) { - requestAnimationFrame(() => { - this.setState({ revealed: true }); - }); - } } componentWillUnmount () { @@ -94,7 +83,6 @@ export default class ModalRoot extends React.PureComponent { render () { const { children, onClose } = this.props; - const { revealed } = this.state; const visible = !!children; if (!visible) { @@ -104,7 +92,7 @@ export default class ModalRoot extends React.PureComponent { } return ( - <div className='modal-root' ref={this.setRef} style={{ opacity: revealed ? 1 : 0 }}> + <div className='modal-root' ref={this.setRef}> <div style={{ pointerEvents: visible ? 'auto' : 'none' }}> <div role='presentation' className='modal-root__overlay' onClick={onClose} /> <div role='dialog' className='modal-root__container'>{children}</div> diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index f4ed25f1e..8f288bdf9 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -391,6 +391,7 @@ class Status extends ImmutablePureComponent { {Component => ( <Component preview={attachment.get('preview_url')} + frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])} blurhash={attachment.get('blurhash')} src={attachment.get('url')} alt={attachment.get('description')} diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index c2b883f7f..cd29b5489 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -142,6 +142,7 @@ class DetailedStatus extends ImmutablePureComponent { media = ( <Video preview={attachment.get('preview_url')} + frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])} blurhash={attachment.get('blurhash')} src={attachment.get('url')} alt={attachment.get('description')} diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js index e19f277d8..578375a7f 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js @@ -18,7 +18,9 @@ import { length } from 'stringz'; import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; import GIFV from 'mastodon/components/gifv'; import { me } from 'mastodon/initial_state'; +// eslint-disable-next-line import/no-extraneous-dependencies import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js'; +// eslint-disable-next-line import/extensions import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js'; import { assetHost } from 'mastodon/utils/config'; @@ -386,6 +388,7 @@ class FocalPointModal extends ImmutablePureComponent { {media.get('type') === 'video' && ( <Video preview={media.get('preview_url')} + frameRate={media.getIn(['meta', 'original', 'frame_rate'])} blurhash={media.get('blurhash')} src={media.get('url')} detailed diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js index 9a07e7c4d..cce6756c3 100644 --- a/app/javascript/mastodon/features/ui/components/video_modal.js +++ b/app/javascript/mastodon/features/ui/components/video_modal.js @@ -64,6 +64,7 @@ export default class VideoModal extends ImmutablePureComponent { <div className='video-modal__container'> <Video preview={media.get('preview_url')} + frameRate={media.getIn(['meta', 'original', 'frame_rate'])} blurhash={media.get('blurhash')} src={media.get('url')} currentTime={options.startTime} diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index e6c6f4b67..83e638ff6 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -99,6 +99,7 @@ class Video extends React.PureComponent { static propTypes = { preview: PropTypes.string, + frameRate: PropTypes.string, src: PropTypes.string.isRequired, alt: PropTypes.string, width: PropTypes.number, @@ -123,6 +124,10 @@ class Video extends React.PureComponent { muted: PropTypes.bool, }; + static defaultProps = { + frameRate: 25, + }; + state = { currentTime: 0, duration: 0, @@ -288,7 +293,7 @@ class Video extends React.PureComponent { } handleKeyDown = e => { - const frameTime = 1 / 25; + const frameTime = 1 / this.getFrameRate(); switch(e.key) { case 'k': @@ -517,6 +522,17 @@ class Video extends React.PureComponent { this.props.onCloseVideo(); } + getFrameRate () { + if (this.props.frameRate && isNaN(this.props.frameRate)) { + // The frame rate is returned as a fraction string so we + // need to convert it to a number + + return this.props.frameRate.split('/').reduce((p, c) => p / c); + } + + return this.props.frameRate; + } + render () { const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable, blurhash } = this.props; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js index cf1388aed..c6d12cd6f 100644 --- a/app/javascript/mastodon/stream.js +++ b/app/javascript/mastodon/stream.js @@ -16,7 +16,7 @@ let sharedConnection; * @property {function(): void} onDisconnect */ - /** +/** * @typedef StreamEvent * @property {string} event * @property {object} payload diff --git a/app/javascript/mastodon/utils/notifications.js b/app/javascript/mastodon/utils/notifications.js index ab119c2e3..7634cac21 100644 --- a/app/javascript/mastodon/utils/notifications.js +++ b/app/javascript/mastodon/utils/notifications.js @@ -3,6 +3,7 @@ const checkNotificationPromise = () => { try { + // eslint-disable-next-line promise/catch-or-return Notification.requestPermission().then(); } catch(e) { return false; @@ -22,7 +23,7 @@ const handlePermission = (permission, callback) => { export const requestNotificationPermission = (callback) => { if (checkNotificationPromise()) { - Notification.requestPermission().then((permission) => handlePermission(permission, callback)); + Notification.requestPermission().then((permission) => handlePermission(permission, callback)).catch(console.warn); } else { Notification.requestPermission((permission) => handlePermission(permission, callback)); } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 9c4e6d08f..efdc6fa39 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4439,8 +4439,6 @@ a.status-card.compact:hover { .modal-root { position: relative; - transition: opacity 0.3s linear; - will-change: opacity; z-index: 9999; } diff --git a/app/models/account.rb b/app/models/account.rb index 641e984cd..87b89df51 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -67,6 +67,7 @@ class Account < ApplicationRecord include Paginable include AccountCounters include DomainNormalizable + include AccountMerging MAX_DISPLAY_NAME_LENGTH = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i MAX_NOTE_LENGTH = (ENV['MAX_BIO_CHARS'] || 500).to_i diff --git a/app/models/concerns/account_merging.rb b/app/models/concerns/account_merging.rb new file mode 100644 index 000000000..691d02e03 --- /dev/null +++ b/app/models/concerns/account_merging.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module AccountMerging + extend ActiveSupport::Concern + + def merge_with!(other_account) + # Since it's the same remote resource, the remote resource likely + # already believes we are following/blocking, so it's safe to + # re-attribute the relationships too. However, during the presence + # of the index bug users could have *also* followed the reference + # account already, therefore mass update will not work and we need + # to check for (and skip past) uniqueness errors + + owned_classes = [ + Status, StatusPin, MediaAttachment, Poll, Report, Tombstone, Favourite, + Follow, FollowRequest, Block, Mute, AccountIdentityProof, + AccountModerationNote, AccountPin, AccountStat, ListAccount, + PollVote, Mention + ] + + owned_classes.each do |klass| + klass.where(account_id: other_account.id).find_each do |record| + begin + record.update_attribute(:account_id, id) + rescue ActiveRecord::RecordNotUnique + next + end + end + end + + target_classes = [Follow, FollowRequest, Block, Mute, AccountModerationNote, AccountPin] + + target_classes.each do |klass| + klass.where(target_account_id: other_account.id).find_each do |record| + begin + record.update_attribute(:target_account_id, id) + rescue ActiveRecord::RecordNotUnique + next + end + end + end + end +end diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb index 778d064de..9cb80c95a 100644 --- a/app/services/delete_account_service.rb +++ b/app/services/delete_account_service.rb @@ -56,6 +56,7 @@ class DeleteAccountService < BaseService @options[:skip_activitypub] = true if @options[:skip_side_effects] reject_follows! + undo_follows! purge_user! purge_profile! purge_content! @@ -79,6 +80,20 @@ class DeleteAccountService < BaseService end end + def undo_follows! + return if @account.local? || !@account.activitypub? || @options[:skip_activitypub] + + # When deleting a remote account, the account obviously doesn't + # actually become deleted on its origin server, but following relationships + # are severed on our end. Therefore, make the remote server aware that the + # follow relationships are severed to avoid confusion and potential issues + # if the remote account gets un-suspended. + + ActivityPub::DeliveryWorker.push_bulk(Follow.where(target_account: @account)) do |follow| + [Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)), follow.account_id, @account.inbox_url] + end + end + def purge_user! return if !@account.local? || @account.user.nil? |