Merge branch 'master' into glitch-soc/merge-upstream
Conflicts: - `Gemfile.lock`: glitch-soc-only dependency textually too close to updated upstream dependencies. Updated to upsteam dependencies.
This commit is contained in:
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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')}
|
||||
|
@@ -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')}
|
||||
|
@@ -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
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
|
@@ -16,7 +16,7 @@ let sharedConnection;
|
||||
* @property {function(): void} onDisconnect
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* @typedef StreamEvent
|
||||
* @property {string} event
|
||||
* @property {object} payload
|
||||
|
@@ -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));
|
||||
}
|
||||
|
@@ -4439,8 +4439,6 @@ a.status-card.compact:hover {
|
||||
|
||||
.modal-root {
|
||||
position: relative;
|
||||
transition: opacity 0.3s linear;
|
||||
will-change: opacity;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
43
app/models/concerns/account_merging.rb
Normal file
43
app/models/concerns/account_merging.rb
Normal file
@@ -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
|
@@ -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?
|
||||
|
||||
|
Reference in New Issue
Block a user