Merge changes from upstream with the CSS reload fix
This commit is contained in:
commit
ed6c60a0c2
98 changed files with 1376 additions and 582 deletions
|
@ -12,9 +12,11 @@ EXPOSE 3000 4000
|
||||||
WORKDIR /mastodon
|
WORKDIR /mastodon
|
||||||
|
|
||||||
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \
|
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \
|
||||||
|
&& echo "@edge https://nl.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \
|
||||||
&& apk -U upgrade \
|
&& apk -U upgrade \
|
||||||
&& apk add -t build-dependencies \
|
&& apk add -t build-dependencies \
|
||||||
build-base \
|
build-base \
|
||||||
|
icu-dev \
|
||||||
libidn-dev \
|
libidn-dev \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
libxslt-dev \
|
libxslt-dev \
|
||||||
|
@ -26,7 +28,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
file \
|
file \
|
||||||
git \
|
git \
|
||||||
icu-dev \
|
icu-libs \
|
||||||
imagemagick@edge \
|
imagemagick@edge \
|
||||||
libidn \
|
libidn \
|
||||||
libpq \
|
libpq \
|
||||||
|
@ -37,7 +39,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
|
||||||
protobuf \
|
protobuf \
|
||||||
su-exec \
|
su-exec \
|
||||||
tini \
|
tini \
|
||||||
&& npm install -g npm@3 && npm install -g yarn \
|
yarn@edge \
|
||||||
&& update-ca-certificates \
|
&& update-ca-certificates \
|
||||||
&& rm -rf /tmp/* /var/cache/apk/*
|
&& rm -rf /tmp/* /var/cache/apk/*
|
||||||
|
|
||||||
|
|
3
Vagrantfile
vendored
3
Vagrantfile
vendored
|
@ -35,9 +35,10 @@ sudo apt-get install \
|
||||||
postgresql-contrib \
|
postgresql-contrib \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
yarn \
|
yarn \
|
||||||
|
libicu-dev \
|
||||||
|
libidn11-dev \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libreadline-dev \
|
libreadline-dev \
|
||||||
libicu-dev \
|
|
||||||
-y
|
-y
|
||||||
|
|
||||||
# Install rvm
|
# Install rvm
|
||||||
|
|
|
@ -13,7 +13,7 @@ class AccountsController < ApplicationController
|
||||||
|
|
||||||
format.atom do
|
format.atom do
|
||||||
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
|
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
|
||||||
render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
|
render xml: Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.feed(@account, @entries.to_a))
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
|
|
|
@ -5,7 +5,14 @@ module Admin
|
||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
before_action :set_report
|
before_action :set_report
|
||||||
before_action :set_status
|
before_action :set_status, only: [:update, :destroy]
|
||||||
|
|
||||||
|
def create
|
||||||
|
@form = Form::StatusBatch.new(form_status_batch_params)
|
||||||
|
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
|
||||||
|
|
||||||
|
redirect_to admin_report_path(@report)
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@status.update(status_params)
|
@status.update(status_params)
|
||||||
|
@ -15,7 +22,7 @@ module Admin
|
||||||
def destroy
|
def destroy
|
||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
RemovalWorker.perform_async(@status.id)
|
RemovalWorker.perform_async(@status.id)
|
||||||
redirect_to admin_report_path(@report)
|
render json: @status
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -24,6 +31,10 @@ module Admin
|
||||||
params.require(:status).permit(:sensitive)
|
params.require(:status).permit(:sensitive)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def form_status_batch_params
|
||||||
|
params.require(:form_status_batch).permit(:action, status_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
def set_report
|
def set_report
|
||||||
@report = Report.find(params[:report_id])
|
@report = Report.find(params[:report_id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,9 @@ module Admin
|
||||||
@reports = filtered_reports.page(params[:page])
|
@reports = filtered_reports.page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
@form = Form::StatusBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
process_report
|
process_report
|
||||||
|
|
69
app/controllers/admin/statuses_controller.rb
Normal file
69
app/controllers/admin/statuses_controller.rb
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class StatusesController < BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
helper_method :current_params
|
||||||
|
|
||||||
|
before_action :set_account
|
||||||
|
before_action :set_status, only: [:update, :destroy]
|
||||||
|
|
||||||
|
PAR_PAGE = 20
|
||||||
|
|
||||||
|
def index
|
||||||
|
@statuses = @account.statuses
|
||||||
|
if params[:media]
|
||||||
|
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
|
||||||
|
@statuses.merge!(Status.where(id: account_media_status_ids))
|
||||||
|
end
|
||||||
|
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PAR_PAGE)
|
||||||
|
|
||||||
|
@form = Form::StatusBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@form = Form::StatusBatch.new(form_status_batch_params)
|
||||||
|
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
|
||||||
|
|
||||||
|
redirect_to admin_account_statuses_path(@account.id, current_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@status.update(status_params)
|
||||||
|
redirect_to admin_account_statuses_path(@account.id, current_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @status, :destroy?
|
||||||
|
RemovalWorker.perform_async(@status.id)
|
||||||
|
render json: @status
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def status_params
|
||||||
|
params.require(:status).permit(:sensitive)
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_status_batch_params
|
||||||
|
params.require(:form_status_batch).permit(:action, status_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_status
|
||||||
|
@status = @account.statuses.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:account_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_params
|
||||||
|
page = (params[:page] || 1).to_i
|
||||||
|
{
|
||||||
|
media: params[:media],
|
||||||
|
page: page > 1 && page,
|
||||||
|
}.select { |_, value| value.present? }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -35,6 +35,7 @@ class Settings::PreferencesController < ApplicationController
|
||||||
params.require(:user).permit(
|
params.require(:user).permit(
|
||||||
:setting_default_privacy,
|
:setting_default_privacy,
|
||||||
:setting_default_sensitive,
|
:setting_default_sensitive,
|
||||||
|
:setting_unfollow_modal,
|
||||||
:setting_boost_modal,
|
:setting_boost_modal,
|
||||||
:setting_delete_modal,
|
:setting_delete_modal,
|
||||||
:setting_auto_play_gif,
|
:setting_auto_play_gif,
|
||||||
|
|
|
@ -19,7 +19,7 @@ class StreamEntriesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
format.atom do
|
format.atom do
|
||||||
render xml: AtomSerializer.render(AtomSerializer.new.entry(@stream_entry, true))
|
render xml: Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.entry(@stream_entry, true))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -186,6 +186,12 @@ export default class MediaGallery extends React.PureComponent {
|
||||||
visible: !this.props.sensitive,
|
visible: !this.props.sensitive,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.sensitive !== this.props.sensitive) {
|
||||||
|
this.setState({ visible: !nextProps.sensitive });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleOpen = () => {
|
handleOpen = () => {
|
||||||
this.setState({ visible: !this.state.visible });
|
this.setState({ visible: !this.state.visible });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import { makeGetAccount } from '../selectors';
|
import { makeGetAccount } from '../selectors';
|
||||||
import Account from '../components/account';
|
import Account from '../components/account';
|
||||||
import {
|
import {
|
||||||
|
@ -9,6 +11,11 @@ import {
|
||||||
muteAccount,
|
muteAccount,
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
|
import { openModal } from '../actions/modal';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getAccount = makeGetAccount();
|
const getAccount = makeGetAccount();
|
||||||
|
@ -16,15 +23,25 @@ const makeMapStateToProps = () => {
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
account: getAccount(state, props.id),
|
account: getAccount(state, props.id),
|
||||||
me: state.getIn(['meta', 'me']),
|
me: state.getIn(['meta', 'me']),
|
||||||
|
unfollowModal: state.getIn(['meta', 'unfollow_modal']),
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onFollow (account) {
|
onFollow (account) {
|
||||||
if (account.getIn(['relationship', 'following'])) {
|
if (account.getIn(['relationship', 'following'])) {
|
||||||
|
if (this.unfollowModal) {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||||
|
confirm: intl.formatMessage(messages.unfollowConfirm),
|
||||||
|
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
dispatch(unfollowAccount(account.get('id')));
|
dispatch(unfollowAccount(account.get('id')));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id')));
|
dispatch(followAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
|
@ -45,6 +62,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
dispatch(muteAccount(account.get('id')));
|
dispatch(muteAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(Account);
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||||
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
|
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||||
|
@ -28,15 +29,25 @@ const makeMapStateToProps = () => {
|
||||||
const mapStateToProps = (state, { accountId }) => ({
|
const mapStateToProps = (state, { accountId }) => ({
|
||||||
account: getAccount(state, Number(accountId)),
|
account: getAccount(state, Number(accountId)),
|
||||||
me: state.getIn(['meta', 'me']),
|
me: state.getIn(['meta', 'me']),
|
||||||
|
unfollowModal: state.getIn(['meta', 'unfollow_modal']),
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onFollow (account) {
|
onFollow (account) {
|
||||||
if (account.getIn(['relationship', 'following'])) {
|
if (account.getIn(['relationship', 'following'])) {
|
||||||
|
if (this.unfollowModal) {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||||
|
confirm: intl.formatMessage(messages.unfollowConfirm),
|
||||||
|
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
dispatch(unfollowAccount(account.get('id')));
|
dispatch(unfollowAccount(account.get('id')));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id')));
|
dispatch(followAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
|
@ -85,6 +96,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
onUnblockDomain (domain, accountId) {
|
onUnblockDomain (domain, accountId) {
|
||||||
dispatch(unblockDomain(domain, accountId));
|
dispatch(unblockDomain(domain, accountId));
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
|
||||||
|
|
|
@ -87,7 +87,7 @@ export default class ModalRoot extends React.PureComponent {
|
||||||
>
|
>
|
||||||
{interpolatedStyles =>
|
{interpolatedStyles =>
|
||||||
<div className='modal-root'>
|
<div className='modal-root'>
|
||||||
{interpolatedStyles.map(({ key, data: { type }, style }) => (
|
{interpolatedStyles.map(({ key, data: { type, props }, style }) => (
|
||||||
<div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
|
<div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
|
||||||
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
|
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
|
||||||
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
||||||
|
|
|
@ -10,7 +10,17 @@ const makeGetStatusIds = () => createSelector([
|
||||||
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
|
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
|
||||||
(state) => state.get('statuses'),
|
(state) => state.get('statuses'),
|
||||||
(state) => state.getIn(['meta', 'me']),
|
(state) => state.getIn(['meta', 'me']),
|
||||||
], (columnSettings, statusIds, statuses, me) => statusIds.filter(id => {
|
], (columnSettings, statusIds, statuses, me) => {
|
||||||
|
const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim();
|
||||||
|
let regex = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
regex = rawRegex && new RegExp(rawRegex, 'i');
|
||||||
|
} catch (e) {
|
||||||
|
// Bad regex, don't affect filters
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusIds.filter(id => {
|
||||||
const statusForId = statuses.get(id);
|
const statusForId = statuses.get(id);
|
||||||
let showStatus = true;
|
let showStatus = true;
|
||||||
|
|
||||||
|
@ -22,19 +32,14 @@ const makeGetStatusIds = () => createSelector([
|
||||||
showStatus = showStatus && (statusForId.get('in_reply_to_id') === null || statusForId.get('in_reply_to_account_id') === me);
|
showStatus = showStatus && (statusForId.get('in_reply_to_id') === null || statusForId.get('in_reply_to_account_id') === me);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {
|
if (showStatus && regex && statusForId.get('account') !== me) {
|
||||||
try {
|
const searchIndex = statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'search_index']) : statusForId.get('search_index');
|
||||||
if (showStatus) {
|
showStatus = !regex.test(searchIndex);
|
||||||
const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
|
|
||||||
showStatus = !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'search_index']) : statusForId.get('search_index'));
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
// Bad regex, don't affect filters
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return showStatus;
|
return showStatus;
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getStatusIds = makeGetStatusIds();
|
const getStatusIds = makeGetStatusIds();
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "أكتم",
|
"confirmations.mute.confirm": "أكتم",
|
||||||
"confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
|
"confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "الأنشطة",
|
"emoji_button.activity": "الأنشطة",
|
||||||
"emoji_button.flags": "الأعلام",
|
"emoji_button.flags": "الأعلام",
|
||||||
"emoji_button.food": "الطعام والشراب",
|
"emoji_button.food": "الطعام والشراب",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Estàs realment, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar és suficient i preferible.",
|
"confirmations.domain_block.message": "Estàs realment, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar és suficient i preferible.",
|
||||||
"confirmations.mute.confirm": "Silenciar",
|
"confirmations.mute.confirm": "Silenciar",
|
||||||
"confirmations.mute.message": "Estàs segur que vols silenciar {name}?",
|
"confirmations.mute.message": "Estàs segur que vols silenciar {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activitat",
|
"emoji_button.activity": "Activitat",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Menjar i Beure",
|
"emoji_button.food": "Menjar i Beure",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -228,6 +228,19 @@
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/components/video_player.json"
|
"path": "app/javascript/mastodon/components/video_player.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unfollow",
|
||||||
|
"id": "confirmations.unfollow.confirm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Are you sure you want to unfollow {name}?",
|
||||||
|
"id": "confirmations.unfollow.message"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "app/javascript/mastodon/containers/account_container.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
{
|
{
|
||||||
|
@ -268,6 +281,10 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"descriptors": [
|
"descriptors": [
|
||||||
|
{
|
||||||
|
"defaultMessage": "Unfollow",
|
||||||
|
"id": "confirmations.unfollow.confirm"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Block",
|
"defaultMessage": "Block",
|
||||||
"id": "confirmations.block.confirm"
|
"id": "confirmations.block.confirm"
|
||||||
|
@ -280,6 +297,10 @@
|
||||||
"defaultMessage": "Hide entire domain",
|
"defaultMessage": "Hide entire domain",
|
||||||
"id": "confirmations.domain_block.confirm"
|
"id": "confirmations.domain_block.confirm"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Are you sure you want to unfollow {name}?",
|
||||||
|
"id": "confirmations.unfollow.message"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Are you sure you want to block {name}?",
|
"defaultMessage": "Are you sure you want to block {name}?",
|
||||||
"id": "confirmations.block.message"
|
"id": "confirmations.block.message"
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "آیا جدی جدی میخواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقتها مسدودکردن یا بیصداکردن چند حساب کاربری خاص کافی است و توصیه میشود.",
|
"confirmations.domain_block.message": "آیا جدی جدی میخواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقتها مسدودکردن یا بیصداکردن چند حساب کاربری خاص کافی است و توصیه میشود.",
|
||||||
"confirmations.mute.confirm": "بیصدا کن",
|
"confirmations.mute.confirm": "بیصدا کن",
|
||||||
"confirmations.mute.message": "آیا واقعاً میخواهید {name} را بیصدا کنید؟",
|
"confirmations.mute.message": "آیا واقعاً میخواهید {name} را بیصدا کنید؟",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "فعالیت",
|
"emoji_button.activity": "فعالیت",
|
||||||
"emoji_button.flags": "پرچمها",
|
"emoji_button.flags": "پرچمها",
|
||||||
"emoji_button.food": "غذا و نوشیدنی",
|
"emoji_button.food": "غذا و نوشیدنی",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables.",
|
"confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables.",
|
||||||
"confirmations.mute.confirm": "Masquer",
|
"confirmations.mute.confirm": "Masquer",
|
||||||
"confirmations.mute.message": "Confirmez vous le masquage de {name} ?",
|
"confirmations.mute.message": "Confirmez vous le masquage de {name} ?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activités",
|
"emoji_button.activity": "Activités",
|
||||||
"emoji_button.flags": "Drapeaux",
|
"emoji_button.flags": "Drapeaux",
|
||||||
"emoji_button.food": "Boire et manger",
|
"emoji_button.food": "Boire et manger",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "באמת באמת לחסום את כל קהילת {domain}? ברב המקרים השתקות נבחרות של מספר משתמשים מסויימים צריכה להספיק.",
|
"confirmations.domain_block.message": "באמת באמת לחסום את כל קהילת {domain}? ברב המקרים השתקות נבחרות של מספר משתמשים מסויימים צריכה להספיק.",
|
||||||
"confirmations.mute.confirm": "להשתיק",
|
"confirmations.mute.confirm": "להשתיק",
|
||||||
"confirmations.mute.message": "להשתיק את {name}?",
|
"confirmations.mute.message": "להשתיק את {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "פעילות",
|
"emoji_button.activity": "פעילות",
|
||||||
"emoji_button.flags": "דגלים",
|
"emoji_button.flags": "דגלים",
|
||||||
"emoji_button.food": "אוכל ושתיה",
|
"emoji_button.food": "אוכל ושתיה",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš blokirati sve sa {domain}? U većini slučajeva nekoliko ciljanih blokiranja ili utišavanja je dostatno i poželjnije.",
|
"confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš blokirati sve sa {domain}? U većini slučajeva nekoliko ciljanih blokiranja ili utišavanja je dostatno i poželjnije.",
|
||||||
"confirmations.mute.confirm": "Utišaj",
|
"confirmations.mute.confirm": "Utišaj",
|
||||||
"confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
|
"confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Aktivnost",
|
"emoji_button.activity": "Aktivnost",
|
||||||
"emoji_button.flags": "Zastave",
|
"emoji_button.flags": "Zastave",
|
||||||
"emoji_button.food": "Hrana & Piće",
|
"emoji_button.food": "Hrana & Piće",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Bisukan",
|
"confirmations.mute.confirm": "Bisukan",
|
||||||
"confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?",
|
"confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Aktivitas",
|
"emoji_button.activity": "Aktivitas",
|
||||||
"emoji_button.flags": "Bendera",
|
"emoji_button.flags": "Bendera",
|
||||||
"emoji_button.food": "Makanan & Minuman",
|
"emoji_button.food": "Makanan & Minuman",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。",
|
"confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。",
|
||||||
"confirmations.mute.confirm": "ミュート",
|
"confirmations.mute.confirm": "ミュート",
|
||||||
"confirmations.mute.message": "本当に{name}をミュートしますか?",
|
"confirmations.mute.message": "本当に{name}をミュートしますか?",
|
||||||
|
"confirmations.unfollow.confirm": "フォロー解除",
|
||||||
|
"confirmations.unfollow.message": "本当に{name}のフォローを解除しますか?",
|
||||||
"emoji_button.activity": "活動",
|
"emoji_button.activity": "活動",
|
||||||
"emoji_button.flags": "国旗",
|
"emoji_button.flags": "国旗",
|
||||||
"emoji_button.food": "食べ物",
|
"emoji_button.food": "食べ物",
|
||||||
|
@ -149,7 +151,7 @@
|
||||||
"report.target": "問題のユーザー",
|
"report.target": "問題のユーザー",
|
||||||
"search.placeholder": "検索",
|
"search.placeholder": "検索",
|
||||||
"search_results.total": "{count, number}件の結果",
|
"search_results.total": "{count, number}件の結果",
|
||||||
"standalone.public_title": "A look inside...",
|
"standalone.public_title": "連合タイムライン",
|
||||||
"status.cannot_reblog": "この投稿はブーストできません",
|
"status.cannot_reblog": "この投稿はブーストできません",
|
||||||
"status.delete": "削除",
|
"status.delete": "削除",
|
||||||
"status.favourite": "お気に入り",
|
"status.favourite": "お気に入り",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "정말로 {domain} 전체를 숨기시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다.",
|
"confirmations.domain_block.message": "정말로 {domain} 전체를 숨기시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다.",
|
||||||
"confirmations.mute.confirm": "뮤트",
|
"confirmations.mute.confirm": "뮤트",
|
||||||
"confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
|
"confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "활동",
|
"emoji_button.activity": "활동",
|
||||||
"emoji_button.flags": "국기",
|
"emoji_button.flags": "국기",
|
||||||
"emoji_button.food": "음식",
|
"emoji_button.food": "음식",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
|
"confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
|
||||||
"confirmations.mute.confirm": "Negeren",
|
"confirmations.mute.confirm": "Negeren",
|
||||||
"confirmations.mute.message": "Weet je zeker dat je {name} wilt negeren?",
|
"confirmations.mute.message": "Weet je zeker dat je {name} wilt negeren?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activiteiten",
|
"emoji_button.activity": "Activiteiten",
|
||||||
"emoji_button.flags": "Vlaggen",
|
"emoji_button.flags": "Vlaggen",
|
||||||
"emoji_button.food": "Eten en drinken",
|
"emoji_button.food": "Eten en drinken",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Er du sikker på at du vil skjule hele domenet {domain}? I de fleste tilfeller er det bedre med målrettet blokkering eller demping.",
|
"confirmations.domain_block.message": "Er du sikker på at du vil skjule hele domenet {domain}? I de fleste tilfeller er det bedre med målrettet blokkering eller demping.",
|
||||||
"confirmations.mute.confirm": "Demp",
|
"confirmations.mute.confirm": "Demp",
|
||||||
"confirmations.mute.message": "Er du sikker på at du vil dempe {name}?",
|
"confirmations.mute.message": "Er du sikker på at du vil dempe {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Aktivitet",
|
"emoji_button.activity": "Aktivitet",
|
||||||
"emoji_button.flags": "Flagg",
|
"emoji_button.flags": "Flagg",
|
||||||
"emoji_button.food": "Mat og drikke",
|
"emoji_button.food": "Mat og drikke",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Sètz segur segur de voler blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
|
"confirmations.domain_block.message": "Sètz segur segur de voler blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
|
||||||
"confirmations.mute.confirm": "Metre en silenci",
|
"confirmations.mute.confirm": "Metre en silenci",
|
||||||
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
|
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activitat",
|
"emoji_button.activity": "Activitat",
|
||||||
"emoji_button.flags": "Drapèus",
|
"emoji_button.flags": "Drapèus",
|
||||||
"emoji_button.food": "Beure e manjar",
|
"emoji_button.food": "Beure e manjar",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Czy na pewno chcesz zablokować całą domenę {domain}? Zwykle lepszym rozwiązaniem jest blokada lub wyciszenie kilku użytkowników.",
|
"confirmations.domain_block.message": "Czy na pewno chcesz zablokować całą domenę {domain}? Zwykle lepszym rozwiązaniem jest blokada lub wyciszenie kilku użytkowników.",
|
||||||
"confirmations.mute.confirm": "Wycisz",
|
"confirmations.mute.confirm": "Wycisz",
|
||||||
"confirmations.mute.message": "Czy na pewno chcesz wyciszyć {name}?",
|
"confirmations.mute.message": "Czy na pewno chcesz wyciszyć {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Aktywność",
|
"emoji_button.activity": "Aktywność",
|
||||||
"emoji_button.flags": "Flagi",
|
"emoji_button.flags": "Flagi",
|
||||||
"emoji_button.food": "Żywność i napoje",
|
"emoji_button.food": "Żywność i napoje",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.",
|
"confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.",
|
||||||
"confirmations.mute.confirm": "Заглушить",
|
"confirmations.mute.confirm": "Заглушить",
|
||||||
"confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
|
"confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Занятия",
|
"emoji_button.activity": "Занятия",
|
||||||
"emoji_button.flags": "Флаги",
|
"emoji_button.flags": "Флаги",
|
||||||
"emoji_button.food": "Еда и напитки",
|
"emoji_button.food": "Еда и напитки",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "Sessize al",
|
"confirmations.mute.confirm": "Sessize al",
|
||||||
"confirmations.mute.message": "{name} kullanıcısını sessize almak istiyor musunuz?",
|
"confirmations.mute.message": "{name} kullanıcısını sessize almak istiyor musunuz?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Aktivite",
|
"emoji_button.activity": "Aktivite",
|
||||||
"emoji_button.flags": "Bayraklar",
|
"emoji_button.flags": "Bayraklar",
|
||||||
"emoji_button.food": "Yiyecek ve İçecek",
|
"emoji_button.food": "Yiyecek ve İçecek",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Ви точно, точно впевнені, що хочете заблокувати весь домен {domain}? У більшості випадків для нормальної роботи краще заблокувати/заглушити лише деяких користувачів.",
|
"confirmations.domain_block.message": "Ви точно, точно впевнені, що хочете заблокувати весь домен {domain}? У більшості випадків для нормальної роботи краще заблокувати/заглушити лише деяких користувачів.",
|
||||||
"confirmations.mute.confirm": "Заглушити",
|
"confirmations.mute.confirm": "Заглушити",
|
||||||
"confirmations.mute.message": "Ви впевнені, що хочете заглушити {name}?",
|
"confirmations.mute.message": "Ви впевнені, що хочете заглушити {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "Заняття",
|
"emoji_button.activity": "Заняття",
|
||||||
"emoji_button.flags": "Прапори",
|
"emoji_button.flags": "Прапори",
|
||||||
"emoji_button.food": "Їжа та напої",
|
"emoji_button.food": "Їжа та напої",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "静音",
|
"confirmations.mute.confirm": "静音",
|
||||||
"confirmations.mute.message": "想好了,真的要静音 {name}?",
|
"confirmations.mute.message": "想好了,真的要静音 {name}?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "活动",
|
"emoji_button.activity": "活动",
|
||||||
"emoji_button.flags": "旗帜",
|
"emoji_button.flags": "旗帜",
|
||||||
"emoji_button.food": "食物和饮料",
|
"emoji_button.food": "食物和饮料",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
|
||||||
"confirmations.mute.confirm": "靜音",
|
"confirmations.mute.confirm": "靜音",
|
||||||
"confirmations.mute.message": "你確定要將{name}靜音嗎?",
|
"confirmations.mute.message": "你確定要將{name}靜音嗎?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "活動",
|
"emoji_button.activity": "活動",
|
||||||
"emoji_button.flags": "旗幟",
|
"emoji_button.flags": "旗幟",
|
||||||
"emoji_button.food": "飲飲食食",
|
"emoji_button.food": "飲飲食食",
|
||||||
|
|
|
@ -55,6 +55,8 @@
|
||||||
"confirmations.domain_block.message": "你真的真的確定要封鎖整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
|
"confirmations.domain_block.message": "你真的真的確定要封鎖整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
|
||||||
"confirmations.mute.confirm": "消音",
|
"confirmations.mute.confirm": "消音",
|
||||||
"confirmations.mute.message": "你確定要消音 {name} ?",
|
"confirmations.mute.message": "你確定要消音 {name} ?",
|
||||||
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"emoji_button.activity": "活動",
|
"emoji_button.activity": "活動",
|
||||||
"emoji_button.flags": "旗幟",
|
"emoji_button.flags": "旗幟",
|
||||||
"emoji_button.food": "食物與飲料",
|
"emoji_button.food": "食物與飲料",
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
|
import * as OfflinePluginRuntime from 'offline-plugin/runtime';
|
||||||
|
import * as WebPushSubscription from './web_push_subscription';
|
||||||
|
import Mastodon from 'mastodon/containers/mastodon';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
import ready from './ready';
|
import ready from './ready';
|
||||||
|
|
||||||
const perf = require('./performance');
|
const perf = require('./performance');
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
perf.start('main()');
|
perf.start('main()');
|
||||||
const Mastodon = require('mastodon/containers/mastodon').default;
|
|
||||||
const React = require('react');
|
|
||||||
const ReactDOM = require('react-dom');
|
|
||||||
|
|
||||||
if (window.history && history.replaceState) {
|
if (window.history && history.replaceState) {
|
||||||
const { pathname, search, hash } = window.location;
|
const { pathname, search, hash } = window.location;
|
||||||
|
@ -23,9 +25,6 @@ function main() {
|
||||||
ReactDOM.render(<Mastodon {...props} />, mountNode);
|
ReactDOM.render(<Mastodon {...props} />, mountNode);
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
// avoid offline in dev mode because it's harder to debug
|
// avoid offline in dev mode because it's harder to debug
|
||||||
const OfflinePluginRuntime = require('offline-plugin/runtime');
|
|
||||||
const WebPushSubscription = require('./web_push_subscription');
|
|
||||||
|
|
||||||
OfflinePluginRuntime.install();
|
OfflinePluginRuntime.install();
|
||||||
WebPushSubscription.register();
|
WebPushSubscription.register();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,10 @@
|
||||||
import './web_push_notifications';
|
import './web_push_notifications';
|
||||||
|
|
||||||
|
// Cause a new version of a registered Service Worker to replace an existing one
|
||||||
|
// that is already installed, and replace the currently active worker on open pages.
|
||||||
|
self.addEventListener('install', function(event) {
|
||||||
|
event.waitUntil(self.skipWaiting());
|
||||||
|
});
|
||||||
|
self.addEventListener('activate', function(event) {
|
||||||
|
event.waitUntil(self.clients.claim());
|
||||||
|
});
|
||||||
|
|
|
@ -50,6 +50,24 @@ const makeRequest = (notification, action) =>
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const openUrl = url =>
|
||||||
|
self.clients.matchAll({ type: 'window' }).then(clientList => {
|
||||||
|
if (clientList.length !== 0 && 'navigate' in clientList[0]) { // Chrome 42-48 does not support navigate
|
||||||
|
const webClients = clientList
|
||||||
|
.filter(client => /\/web\//.test(client.url))
|
||||||
|
.sort(client => client !== 'visible');
|
||||||
|
|
||||||
|
const visibleClient = clientList.find(client => client.visibilityState === 'visible');
|
||||||
|
const focusedClient = clientList.find(client => client.focused);
|
||||||
|
|
||||||
|
const client = webClients[0] || visibleClient || focusedClient || clientList[0];
|
||||||
|
|
||||||
|
return client.navigate(url).then(client => client.focus());
|
||||||
|
} else {
|
||||||
|
return self.clients.openWindow(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const removeActionFromNotification = (notification, action) => {
|
const removeActionFromNotification = (notification, action) => {
|
||||||
const actions = notification.actions.filter(act => act.action !== action.action);
|
const actions = notification.actions.filter(act => act.action !== action.action);
|
||||||
|
|
||||||
|
@ -75,7 +93,7 @@ const handleNotificationClick = (event) => {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
event.notification.close();
|
event.notification.close();
|
||||||
resolve(self.clients.openWindow(event.notification.data.url));
|
resolve(openUrl(event.notification.data.url));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import TimelineContainer from '../mastodon/containers/timeline_container';
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import loadPolyfills from '../mastodon/load_polyfills';
|
import loadPolyfills from '../mastodon/load_polyfills';
|
||||||
import ready from '../mastodon/ready';
|
|
||||||
|
|
||||||
require.context('../images/', true);
|
require.context('../images/', true);
|
||||||
|
|
||||||
function loaded() {
|
function loaded() {
|
||||||
|
const TimelineContainer = require('../mastodon/containers/timeline_container').default;
|
||||||
|
const React = require('react');
|
||||||
|
const ReactDOM = require('react-dom');
|
||||||
const mountNode = document.getElementById('mastodon-timeline');
|
const mountNode = document.getElementById('mastodon-timeline');
|
||||||
|
|
||||||
if (mountNode !== null) {
|
if (mountNode !== null) {
|
||||||
|
@ -16,6 +15,7 @@ function loaded() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
|
const ready = require('../mastodon/ready').default;
|
||||||
ready(loaded);
|
ready(loaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
40
app/javascript/packs/admin.js
Normal file
40
app/javascript/packs/admin.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { delegate } from 'rails-ujs';
|
||||||
|
|
||||||
|
function handleDeleteStatus(event) {
|
||||||
|
const [data] = event.detail;
|
||||||
|
const element = document.querySelector(`[data-id="${data.id}"]`);
|
||||||
|
if (element) {
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[].forEach.call(document.querySelectorAll('.trash-button'), (content) => {
|
||||||
|
content.addEventListener('ajax:success', handleDeleteStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
|
||||||
|
|
||||||
|
delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
||||||
|
[].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
|
||||||
|
content.checked = target.checked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
delegate(document, batchCheckboxClassName, 'change', () => {
|
||||||
|
const checkAllElement = document.querySelector('#batch_checkbox_all');
|
||||||
|
if (checkAllElement) {
|
||||||
|
checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
delegate(document, '.media-spoiler-show-button', 'click', () => {
|
||||||
|
[].forEach.call(document.querySelectorAll('.activity-stream .media-spoiler-wrapper'), (content) => {
|
||||||
|
content.classList.add('media-spoiler-wrapper__visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
delegate(document, '.media-spoiler-hide-button', 'click', () => {
|
||||||
|
[].forEach.call(document.querySelectorAll('.activity-stream .media-spoiler-wrapper'), (content) => {
|
||||||
|
content.classList.remove('media-spoiler-wrapper__visible');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
import main from '../mastodon/main';
|
|
||||||
import loadPolyfills from '../mastodon/load_polyfills';
|
import loadPolyfills from '../mastodon/load_polyfills';
|
||||||
|
|
||||||
loadPolyfills().then(main).catch(e => {
|
loadPolyfills().then(() => {
|
||||||
|
require('../mastodon/main').default();
|
||||||
|
}).catch(e => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { length } from 'stringz';
|
|
||||||
import IntlRelativeFormat from 'intl-relativeformat';
|
|
||||||
import { delegate } from 'rails-ujs';
|
|
||||||
import emojify from '../mastodon/emoji';
|
|
||||||
import { getLocale } from '../mastodon/locales';
|
|
||||||
import loadPolyfills from '../mastodon/load_polyfills';
|
import loadPolyfills from '../mastodon/load_polyfills';
|
||||||
import { processBio } from '../glitch/util/bio_metadata';
|
import { processBio } from '../glitch/util/bio_metadata';
|
||||||
import ready from '../mastodon/ready';
|
|
||||||
|
|
||||||
const { localeData } = getLocale();
|
function main() {
|
||||||
localeData.forEach(IntlRelativeFormat.__addLocaleData);
|
const { length } = require('stringz');
|
||||||
|
const IntlRelativeFormat = require('intl-relativeformat').default;
|
||||||
|
const { delegate } = require('rails-ujs');
|
||||||
|
const emojify = require('../mastodon/emoji').default;
|
||||||
|
const { getLocale } = require('../mastodon/locales');
|
||||||
|
const ready = require('../mastodon/ready').default;
|
||||||
|
|
||||||
function loaded() {
|
const { localeData } = getLocale();
|
||||||
|
localeData.forEach(IntlRelativeFormat.__addLocaleData);
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
const locale = document.documentElement.lang;
|
const locale = document.documentElement.lang;
|
||||||
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
|
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
@ -36,10 +38,7 @@ function loaded() {
|
||||||
const datetime = new Date(content.getAttribute('datetime'));
|
const datetime = new Date(content.getAttribute('datetime'));
|
||||||
content.textContent = relativeFormat.format(datetime);;
|
content.textContent = relativeFormat.format(datetime);;
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
function main() {
|
|
||||||
ready(loaded);
|
|
||||||
|
|
||||||
delegate(document, '.video-player video', 'click', ({ target }) => {
|
delegate(document, '.video-player video', 'click', ({ target }) => {
|
||||||
if (target.paused) {
|
if (target.paused) {
|
||||||
|
|
|
@ -253,7 +253,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-status {
|
.report-status,
|
||||||
|
.account-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
@ -263,7 +264,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-status__actions {
|
.report-status__actions,
|
||||||
|
.account-status__actions {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -275,3 +277,42 @@
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.batch-form-box {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
#form_status_batch_action {
|
||||||
|
margin-right: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-spoiler-toggle-buttons {
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-checkbox,
|
||||||
|
.batch-checkbox-all {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $classic-highlight-color;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
50
app/lib/ostatus/activity/base.rb
Normal file
50
app/lib/ostatus/activity/base.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Ostatus::Activity::Base
|
||||||
|
def initialize(xml, account = nil)
|
||||||
|
@xml = xml
|
||||||
|
@account = account
|
||||||
|
end
|
||||||
|
|
||||||
|
def status?
|
||||||
|
[:activity, :note, :comment].include?(type)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verb
|
||||||
|
raw = @xml.at_xpath('./activity:verb', activity: TagManager::AS_XMLNS).content
|
||||||
|
TagManager::VERBS.key(raw)
|
||||||
|
rescue
|
||||||
|
:post
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
raw = @xml.at_xpath('./activity:object-type', activity: TagManager::AS_XMLNS).content
|
||||||
|
TagManager::TYPES.key(raw)
|
||||||
|
rescue
|
||||||
|
:activity
|
||||||
|
end
|
||||||
|
|
||||||
|
def id
|
||||||
|
@xml.at_xpath('./xmlns:id', xmlns: TagManager::XMLNS).content
|
||||||
|
end
|
||||||
|
|
||||||
|
def url
|
||||||
|
link = @xml.at_xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS)
|
||||||
|
link.nil? ? nil : link['href']
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_status(uri)
|
||||||
|
if TagManager.instance.local_id?(uri)
|
||||||
|
local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Status')
|
||||||
|
return Status.find_by(id: local_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
Status.find_by(uri: uri)
|
||||||
|
end
|
||||||
|
|
||||||
|
def redis
|
||||||
|
Redis.current
|
||||||
|
end
|
||||||
|
end
|
149
app/lib/ostatus/activity/creation.rb
Normal file
149
app/lib/ostatus/activity/creation.rb
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Ostatus::Activity::Creation < Ostatus::Activity::Base
|
||||||
|
def perform
|
||||||
|
if redis.exists("delete_upon_arrival:#{@account.id}:#{id}")
|
||||||
|
Rails.logger.debug "Delete for status #{id} was queued, ignoring"
|
||||||
|
return [nil, false]
|
||||||
|
end
|
||||||
|
|
||||||
|
return [nil, false] if @account.suspended?
|
||||||
|
|
||||||
|
Rails.logger.debug "Creating remote status #{id}"
|
||||||
|
|
||||||
|
# Return early if status already exists in db
|
||||||
|
status = find_status(id)
|
||||||
|
|
||||||
|
return [status, false] unless status.nil?
|
||||||
|
|
||||||
|
status = Status.create!(
|
||||||
|
uri: id,
|
||||||
|
url: url,
|
||||||
|
account: @account,
|
||||||
|
reblog: reblog,
|
||||||
|
text: content,
|
||||||
|
spoiler_text: content_warning,
|
||||||
|
created_at: published,
|
||||||
|
reply: thread?,
|
||||||
|
language: content_language,
|
||||||
|
visibility: visibility_scope,
|
||||||
|
conversation: find_or_create_conversation,
|
||||||
|
thread: thread? ? find_status(thread.first) : nil
|
||||||
|
)
|
||||||
|
|
||||||
|
save_mentions(status)
|
||||||
|
save_hashtags(status)
|
||||||
|
save_media(status)
|
||||||
|
|
||||||
|
if thread? && status.thread.nil?
|
||||||
|
Rails.logger.debug "Trying to attach #{status.id} (#{id}) to #{thread.first}"
|
||||||
|
ThreadResolveWorker.perform_async(status.id, thread.second)
|
||||||
|
end
|
||||||
|
|
||||||
|
Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution"
|
||||||
|
|
||||||
|
LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
|
||||||
|
DistributionWorker.perform_async(status.id)
|
||||||
|
|
||||||
|
[status, true]
|
||||||
|
end
|
||||||
|
|
||||||
|
def content
|
||||||
|
@xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_language
|
||||||
|
@xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS)['xml:lang']&.presence || 'en'
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_warning
|
||||||
|
@xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || ''
|
||||||
|
end
|
||||||
|
|
||||||
|
def visibility_scope
|
||||||
|
@xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content&.to_sym || :public
|
||||||
|
end
|
||||||
|
|
||||||
|
def published
|
||||||
|
@xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content
|
||||||
|
end
|
||||||
|
|
||||||
|
def thread?
|
||||||
|
!@xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS).nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def thread
|
||||||
|
thr = @xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS)
|
||||||
|
[thr['ref'], thr['href']]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_or_create_conversation
|
||||||
|
uri = @xml.at_xpath('./ostatus:conversation', ostatus: TagManager::OS_XMLNS)&.attribute('ref')&.content
|
||||||
|
return if uri.nil?
|
||||||
|
|
||||||
|
if TagManager.instance.local_id?(uri)
|
||||||
|
local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')
|
||||||
|
return Conversation.find_by(id: local_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
Conversation.find_by(uri: uri) || Conversation.create!(uri: uri)
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_mentions(parent)
|
||||||
|
processed_account_ids = []
|
||||||
|
|
||||||
|
@xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link|
|
||||||
|
next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type']
|
||||||
|
|
||||||
|
mentioned_account = account_from_href(link['href'])
|
||||||
|
|
||||||
|
next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id)
|
||||||
|
|
||||||
|
mentioned_account.mentions.where(status: parent).first_or_create(status: parent)
|
||||||
|
|
||||||
|
# So we can skip duplicate mentions
|
||||||
|
processed_account_ids << mentioned_account.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_hashtags(parent)
|
||||||
|
tags = @xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select(&:present?)
|
||||||
|
ProcessHashtagsService.new.call(parent, tags)
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_media(parent)
|
||||||
|
do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
|
||||||
|
|
||||||
|
@xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: TagManager::XMLNS).each do |link|
|
||||||
|
next unless link['href']
|
||||||
|
|
||||||
|
media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href'])
|
||||||
|
parsed_url = Addressable::URI.parse(link['href']).normalize
|
||||||
|
|
||||||
|
next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty?
|
||||||
|
|
||||||
|
media.save
|
||||||
|
|
||||||
|
next if do_not_download
|
||||||
|
|
||||||
|
begin
|
||||||
|
media.file_remote_url = link['href']
|
||||||
|
media.save!
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
next
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_from_href(href)
|
||||||
|
url = Addressable::URI.parse(href).normalize
|
||||||
|
|
||||||
|
if TagManager.instance.web_domain?(url.host)
|
||||||
|
Account.find_local(url.path.gsub('/users/', ''))
|
||||||
|
else
|
||||||
|
Account.where(uri: href).or(Account.where(url: href)).first || FetchRemoteAccountService.new.call(href)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
app/lib/ostatus/activity/deletion.rb
Normal file
14
app/lib/ostatus/activity/deletion.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Ostatus::Activity::Deletion < Ostatus::Activity::Base
|
||||||
|
def perform
|
||||||
|
Rails.logger.debug "Deleting remote status #{id}"
|
||||||
|
status = Status.find_by(uri: id, account: @account)
|
||||||
|
|
||||||
|
if status.nil?
|
||||||
|
redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id)
|
||||||
|
else
|
||||||
|
RemoveStatusService.new.call(status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
20
app/lib/ostatus/activity/general.rb
Normal file
20
app/lib/ostatus/activity/general.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Ostatus::Activity::General < Ostatus::Activity::Base
|
||||||
|
def specialize
|
||||||
|
special_class&.new(@xml, @account)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def special_class
|
||||||
|
case verb
|
||||||
|
when :post
|
||||||
|
Ostatus::Activity::Post
|
||||||
|
when :share
|
||||||
|
Ostatus::Activity::Share
|
||||||
|
when :delete
|
||||||
|
Ostatus::Activity::Deletion
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
23
app/lib/ostatus/activity/post.rb
Normal file
23
app/lib/ostatus/activity/post.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Ostatus::Activity::Post < Ostatus::Activity::Creation
|
||||||
|
def perform
|
||||||
|
status, just_created = super
|
||||||
|
|
||||||
|
if just_created
|
||||||
|
status.mentions.includes(:account).each do |mention|
|
||||||
|
mentioned_account = mention.account
|
||||||
|
next unless mentioned_account.local?
|
||||||
|
NotifyService.new.call(mentioned_account, mention)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
status
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def reblog
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
7
app/lib/ostatus/activity/remote.rb
Normal file
7
app/lib/ostatus/activity/remote.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Ostatus::Activity::Remote < Ostatus::Activity::Base
|
||||||
|
def perform
|
||||||
|
find_status(id) || FetchRemoteStatusService.new.call(url)
|
||||||
|
end
|
||||||
|
end
|
26
app/lib/ostatus/activity/share.rb
Normal file
26
app/lib/ostatus/activity/share.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Ostatus::Activity::Share < Ostatus::Activity::Creation
|
||||||
|
def perform
|
||||||
|
return if reblog.nil?
|
||||||
|
|
||||||
|
status, just_created = super
|
||||||
|
NotifyService.new.call(reblog.account, status) if reblog.account.local? && just_created
|
||||||
|
status
|
||||||
|
end
|
||||||
|
|
||||||
|
def object
|
||||||
|
@xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def reblog
|
||||||
|
return @reblog if defined? @reblog
|
||||||
|
|
||||||
|
original_status = Ostatus::Activity::Remote.new(object).perform
|
||||||
|
return if original_status.nil?
|
||||||
|
|
||||||
|
@reblog = original_status.reblog? ? original_status.reblog : original_status
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AtomSerializer
|
class Ostatus::AtomSerializer
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
include ActionView::Helpers::SanitizeHelper
|
include ActionView::Helpers::SanitizeHelper
|
||||||
|
|
|
@ -19,6 +19,7 @@ class UserSettingsDecorator
|
||||||
user.settings['interactions'] = merged_interactions
|
user.settings['interactions'] = merged_interactions
|
||||||
user.settings['default_privacy'] = default_privacy_preference
|
user.settings['default_privacy'] = default_privacy_preference
|
||||||
user.settings['default_sensitive'] = default_sensitive_preference
|
user.settings['default_sensitive'] = default_sensitive_preference
|
||||||
|
user.settings['unfollow_modal'] = unfollow_modal_preference
|
||||||
user.settings['boost_modal'] = boost_modal_preference
|
user.settings['boost_modal'] = boost_modal_preference
|
||||||
user.settings['delete_modal'] = delete_modal_preference
|
user.settings['delete_modal'] = delete_modal_preference
|
||||||
user.settings['auto_play_gif'] = auto_play_gif_preference
|
user.settings['auto_play_gif'] = auto_play_gif_preference
|
||||||
|
@ -42,6 +43,10 @@ class UserSettingsDecorator
|
||||||
boolean_cast_setting 'setting_default_sensitive'
|
boolean_cast_setting 'setting_default_sensitive'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unfollow_modal_preference
|
||||||
|
boolean_cast_setting 'setting_unfollow_modal'
|
||||||
|
end
|
||||||
|
|
||||||
def boost_modal_preference
|
def boost_modal_preference
|
||||||
boolean_cast_setting 'setting_boost_modal'
|
boolean_cast_setting 'setting_boost_modal'
|
||||||
end
|
end
|
||||||
|
|
39
app/models/form/status_batch.rb
Normal file
39
app/models/form/status_batch.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Form::StatusBatch
|
||||||
|
include ActiveModel::Model
|
||||||
|
|
||||||
|
attr_accessor :status_ids, :action
|
||||||
|
|
||||||
|
ACTION_TYPE = %w(nsfw_on nsfw_off delete).freeze
|
||||||
|
|
||||||
|
def save
|
||||||
|
case action
|
||||||
|
when 'nsfw_on', 'nsfw_off'
|
||||||
|
change_sensitive(action == 'nsfw_on')
|
||||||
|
when 'delete'
|
||||||
|
delete_statuses
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def change_sensitive(sensitive)
|
||||||
|
media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id)
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
Status.where(id: media_attached_status_ids).find_each do |status|
|
||||||
|
status.update!(sensitive: sensitive)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
true
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_statuses
|
||||||
|
Status.where(id: status_ids).find_each do |status|
|
||||||
|
RemovalWorker.perform_async(status.id)
|
||||||
|
end
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
|
@ -83,6 +83,10 @@ class User < ApplicationRecord
|
||||||
settings.default_sensitive
|
settings.default_sensitive
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setting_unfollow_modal
|
||||||
|
settings.unfollow_modal
|
||||||
|
end
|
||||||
|
|
||||||
def setting_boost_modal
|
def setting_boost_modal
|
||||||
settings.boost_modal
|
settings.boost_modal
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
#
|
#
|
||||||
|
|
||||||
|
require 'webpush'
|
||||||
|
require_relative '../../models/setting'
|
||||||
|
|
||||||
class Web::PushSubscription < ApplicationRecord
|
class Web::PushSubscription < ApplicationRecord
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
include StreamEntriesHelper
|
include StreamEntriesHelper
|
||||||
|
@ -37,7 +40,6 @@ class Web::PushSubscription < ApplicationRecord
|
||||||
nsfw = notification.target_status.nil? || notification.target_status.spoiler_text.empty? ? nil : notification.target_status.spoiler_text
|
nsfw = notification.target_status.nil? || notification.target_status.spoiler_text.empty? ? nil : notification.target_status.spoiler_text
|
||||||
|
|
||||||
# TODO: Make sure that the payload does not exceed 4KB - Webpush::PayloadTooLarge
|
# TODO: Make sure that the payload does not exceed 4KB - Webpush::PayloadTooLarge
|
||||||
# TODO: Queue the requests - Webpush::TooManyRequests
|
|
||||||
Webpush.payload_send(
|
Webpush.payload_send(
|
||||||
message: JSON.generate(
|
message: JSON.generate(
|
||||||
title: title,
|
title: title,
|
||||||
|
@ -59,7 +61,7 @@ class Web::PushSubscription < ApplicationRecord
|
||||||
p256dh: key_p256dh,
|
p256dh: key_p256dh,
|
||||||
auth: key_auth,
|
auth: key_auth,
|
||||||
vapid: {
|
vapid: {
|
||||||
# subject: "mailto:#{Setting.site_contact_email}",
|
subject: "mailto:#{Setting.site_contact_email}",
|
||||||
private_key: Rails.configuration.x.vapid_private_key,
|
private_key: Rails.configuration.x.vapid_private_key,
|
||||||
public_key: Rails.configuration.x.vapid_public_key,
|
public_key: Rails.configuration.x.vapid_public_key,
|
||||||
},
|
},
|
||||||
|
@ -166,7 +168,7 @@ class Web::PushSubscription < ApplicationRecord
|
||||||
p256dh: key_p256dh,
|
p256dh: key_p256dh,
|
||||||
auth: key_auth,
|
auth: key_auth,
|
||||||
vapid: {
|
vapid: {
|
||||||
# subject: "mailto:#{Setting.site_contact_email}",
|
subject: "mailto:#{Setting.site_contact_email}",
|
||||||
private_key: Rails.configuration.x.vapid_private_key,
|
private_key: Rails.configuration.x.vapid_private_key,
|
||||||
public_key: Rails.configuration.x.vapid_public_key,
|
public_key: Rails.configuration.x.vapid_public_key,
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
|
|
||||||
if object.current_account
|
if object.current_account
|
||||||
store[:me] = object.current_account.id
|
store[:me] = object.current_account.id
|
||||||
|
store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal
|
||||||
store[:boost_modal] = object.current_account.user.setting_boost_modal
|
store[:boost_modal] = object.current_account.user.setting_boost_modal
|
||||||
store[:delete_modal] = object.current_account.user.setting_delete_modal
|
store[:delete_modal] = object.current_account.user.setting_delete_modal
|
||||||
store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif
|
store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif
|
||||||
|
|
|
@ -10,6 +10,6 @@ class AuthorizeFollowService < BaseService
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_xml(follow_request)
|
def build_xml(follow_request)
|
||||||
AtomSerializer.render(AtomSerializer.new.authorize_follow_request_salmon(follow_request))
|
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,6 @@ class BlockService < BaseService
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_xml(block)
|
def build_xml(block)
|
||||||
AtomSerializer.render(AtomSerializer.new.block_salmon(block))
|
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.block_salmon(block))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
module StreamEntryRenderer
|
module StreamEntryRenderer
|
||||||
def stream_entry_to_xml(stream_entry)
|
def stream_entry_to_xml(stream_entry)
|
||||||
AtomSerializer.render(AtomSerializer.new.entry(stream_entry, true))
|
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.entry(stream_entry, true))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,6 +28,6 @@ class FavouriteService < BaseService
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_xml(favourite)
|
def build_xml(favourite)
|
||||||
AtomSerializer.render(AtomSerializer.new.favourite_salmon(favourite))
|
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.favourite_salmon(favourite))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,10 +57,10 @@ class FollowService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_follow_request_xml(follow_request)
|
def build_follow_request_xml(follow_request)
|
||||||
AtomSerializer.render(AtomSerializer.new.follow_request_salmon(follow_request))
|
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.follow_request_salmon(follow_request))
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_follow_xml(follow)
|
def build_follow_xml(follow)
|
||||||
AtomSerializer.render(AtomSerializer.new.follow_salmon(follow))
|
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.follow_salmon(follow))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -65,7 +65,11 @@ class NotifyService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_push_notifications
|
def send_push_notifications
|
||||||
WebPushNotificationWorker.perform_async(@recipient.id, @notification.id)
|
sessions_with_subscriptions_ids = @recipient.user.session_activations.where.not(web_push_subscription: nil).pluck(:id)
|
||||||
|
|
||||||
|
WebPushNotificationWorker.push_bulk(sessions_with_subscriptions_ids) do |session_activation_id|
|
||||||
|
[session_activation_id, @notification.id]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_email
|
def send_email
|
||||||
|
|
|
@ -16,274 +16,14 @@ class ProcessFeedService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_entries(xml, account)
|
def process_entries(xml, account)
|
||||||
xml.xpath('//xmlns:entry', xmlns: TagManager::XMLNS).reverse_each.map { |entry| ProcessEntry.new.call(entry, account) }.compact
|
xml.xpath('//xmlns:entry', xmlns: TagManager::XMLNS).reverse_each.map { |entry| process_entry(entry, account) }.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
class ProcessEntry
|
def process_entry(xml, account)
|
||||||
def call(xml, account)
|
activity = Ostatus::Activity::General.new(xml, account)
|
||||||
@account = account
|
activity.specialize&.perform if activity.status?
|
||||||
@xml = xml
|
|
||||||
|
|
||||||
return if skip_unsupported_type?
|
|
||||||
|
|
||||||
case verb
|
|
||||||
when :post, :share
|
|
||||||
return create_status
|
|
||||||
when :delete
|
|
||||||
return delete_status
|
|
||||||
end
|
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
Rails.logger.debug "Nothing was saved for #{id} because: #{e}"
|
Rails.logger.debug "Nothing was saved for #{id} because: #{e}"
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def create_status
|
|
||||||
if redis.exists("delete_upon_arrival:#{@account.id}:#{id}")
|
|
||||||
Rails.logger.debug "Delete for status #{id} was queued, ignoring"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
status, just_created = nil
|
|
||||||
|
|
||||||
Rails.logger.debug "Creating remote status #{id}"
|
|
||||||
|
|
||||||
if verb == :share
|
|
||||||
original_status = shared_status_from_xml(@xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS))
|
|
||||||
return nil if original_status.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
|
||||||
status, just_created = status_from_xml(@xml)
|
|
||||||
|
|
||||||
return if status.nil?
|
|
||||||
return status unless just_created
|
|
||||||
|
|
||||||
if verb == :share
|
|
||||||
status.reblog = original_status.reblog? ? original_status.reblog : original_status
|
|
||||||
end
|
|
||||||
|
|
||||||
status.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
if thread?(@xml) && status.thread.nil?
|
|
||||||
Rails.logger.debug "Trying to attach #{status.id} (#{id(@xml)}) to #{thread(@xml).first}"
|
|
||||||
ThreadResolveWorker.perform_async(status.id, thread(@xml).second)
|
|
||||||
end
|
|
||||||
|
|
||||||
notify_about_mentions!(status) unless status.reblog?
|
|
||||||
notify_about_reblog!(status) if status.reblog? && status.reblog.account.local?
|
|
||||||
|
|
||||||
Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution"
|
|
||||||
|
|
||||||
LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
|
|
||||||
DistributionWorker.perform_async(status.id)
|
|
||||||
|
|
||||||
status
|
|
||||||
end
|
|
||||||
|
|
||||||
def notify_about_mentions!(status)
|
|
||||||
status.mentions.includes(:account).each do |mention|
|
|
||||||
mentioned_account = mention.account
|
|
||||||
next unless mentioned_account.local?
|
|
||||||
NotifyService.new.call(mentioned_account, mention)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def notify_about_reblog!(status)
|
|
||||||
NotifyService.new.call(status.reblog.account, status)
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete_status
|
|
||||||
Rails.logger.debug "Deleting remote status #{id}"
|
|
||||||
status = Status.find_by(uri: id, account: @account)
|
|
||||||
|
|
||||||
if status.nil?
|
|
||||||
redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id)
|
|
||||||
else
|
|
||||||
RemoveStatusService.new.call(status)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def skip_unsupported_type?
|
|
||||||
!([:post, :share, :delete].include?(verb) && [:activity, :note, :comment].include?(type))
|
|
||||||
end
|
|
||||||
|
|
||||||
def shared_status_from_xml(entry)
|
|
||||||
status = find_status(id(entry))
|
|
||||||
|
|
||||||
return status unless status.nil?
|
|
||||||
|
|
||||||
FetchRemoteStatusService.new.call(url(entry))
|
|
||||||
end
|
|
||||||
|
|
||||||
def status_from_xml(entry)
|
|
||||||
# Return early if status already exists in db
|
|
||||||
status = find_status(id(entry))
|
|
||||||
|
|
||||||
return [status, false] unless status.nil?
|
|
||||||
|
|
||||||
account = @account
|
|
||||||
|
|
||||||
return [nil, false] if account.suspended?
|
|
||||||
|
|
||||||
status = Status.create!(
|
|
||||||
uri: id(entry),
|
|
||||||
url: url(entry),
|
|
||||||
account: account,
|
|
||||||
text: content(entry),
|
|
||||||
spoiler_text: content_warning(entry),
|
|
||||||
created_at: published(entry),
|
|
||||||
reply: thread?(entry),
|
|
||||||
language: content_language(entry),
|
|
||||||
visibility: visibility_scope(entry),
|
|
||||||
conversation: find_or_create_conversation(entry),
|
|
||||||
thread: thread?(entry) ? find_status(thread(entry).first) : nil
|
|
||||||
)
|
|
||||||
|
|
||||||
mentions_from_xml(status, entry)
|
|
||||||
hashtags_from_xml(status, entry)
|
|
||||||
media_from_xml(status, entry)
|
|
||||||
|
|
||||||
[status, true]
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_or_create_conversation(xml)
|
|
||||||
uri = xml.at_xpath('./ostatus:conversation', ostatus: TagManager::OS_XMLNS)&.attribute('ref')&.content
|
|
||||||
return if uri.nil?
|
|
||||||
|
|
||||||
if TagManager.instance.local_id?(uri)
|
|
||||||
local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')
|
|
||||||
return Conversation.find_by(id: local_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
Conversation.find_by(uri: uri) || Conversation.create!(uri: uri)
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_status(uri)
|
|
||||||
if TagManager.instance.local_id?(uri)
|
|
||||||
local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Status')
|
|
||||||
return Status.find_by(id: local_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
Status.find_by(uri: uri)
|
|
||||||
end
|
|
||||||
|
|
||||||
def mentions_from_xml(parent, xml)
|
|
||||||
processed_account_ids = []
|
|
||||||
|
|
||||||
xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link|
|
|
||||||
next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type']
|
|
||||||
|
|
||||||
mentioned_account = account_from_href(link['href'])
|
|
||||||
|
|
||||||
next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id)
|
|
||||||
|
|
||||||
mentioned_account.mentions.where(status: parent).first_or_create(status: parent)
|
|
||||||
|
|
||||||
# So we can skip duplicate mentions
|
|
||||||
processed_account_ids << mentioned_account.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_from_href(href)
|
|
||||||
url = Addressable::URI.parse(href).normalize
|
|
||||||
|
|
||||||
if TagManager.instance.web_domain?(url.host)
|
|
||||||
Account.find_local(url.path.gsub('/users/', ''))
|
|
||||||
else
|
|
||||||
Account.where(uri: href).or(Account.where(url: href)).first || FetchRemoteAccountService.new.call(href)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def hashtags_from_xml(parent, xml)
|
|
||||||
tags = xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select(&:present?)
|
|
||||||
ProcessHashtagsService.new.call(parent, tags)
|
|
||||||
end
|
|
||||||
|
|
||||||
def media_from_xml(parent, xml)
|
|
||||||
do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
|
|
||||||
|
|
||||||
xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: TagManager::XMLNS).each do |link|
|
|
||||||
next unless link['href']
|
|
||||||
|
|
||||||
media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href'])
|
|
||||||
parsed_url = Addressable::URI.parse(link['href']).normalize
|
|
||||||
|
|
||||||
next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty?
|
|
||||||
|
|
||||||
media.save
|
|
||||||
|
|
||||||
next if do_not_download
|
|
||||||
|
|
||||||
begin
|
|
||||||
media.file_remote_url = link['href']
|
|
||||||
media.save!
|
|
||||||
rescue ActiveRecord::RecordInvalid
|
|
||||||
next
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def id(xml = @xml)
|
|
||||||
xml.at_xpath('./xmlns:id', xmlns: TagManager::XMLNS).content
|
|
||||||
end
|
|
||||||
|
|
||||||
def verb(xml = @xml)
|
|
||||||
raw = xml.at_xpath('./activity:verb', activity: TagManager::AS_XMLNS).content
|
|
||||||
TagManager::VERBS.key(raw)
|
|
||||||
rescue
|
|
||||||
:post
|
|
||||||
end
|
|
||||||
|
|
||||||
def type(xml = @xml)
|
|
||||||
raw = xml.at_xpath('./activity:object-type', activity: TagManager::AS_XMLNS).content
|
|
||||||
TagManager::TYPES.key(raw)
|
|
||||||
rescue
|
|
||||||
:activity
|
|
||||||
end
|
|
||||||
|
|
||||||
def url(xml = @xml)
|
|
||||||
link = xml.at_xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS)
|
|
||||||
link.nil? ? nil : link['href']
|
|
||||||
end
|
|
||||||
|
|
||||||
def content(xml = @xml)
|
|
||||||
xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content
|
|
||||||
end
|
|
||||||
|
|
||||||
def content_language(xml = @xml)
|
|
||||||
xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS)['xml:lang']&.presence || 'en'
|
|
||||||
end
|
|
||||||
|
|
||||||
def content_warning(xml = @xml)
|
|
||||||
xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || ''
|
|
||||||
end
|
|
||||||
|
|
||||||
def visibility_scope(xml = @xml)
|
|
||||||
xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content&.to_sym || :public
|
|
||||||
end
|
|
||||||
|
|
||||||
def published(xml = @xml)
|
|
||||||
xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content
|
|
||||||
end
|
|
||||||
|
|
||||||
def thread?(xml = @xml)
|
|
||||||
!xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS).nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
def thread(xml = @xml)
|
|
||||||
thr = xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS)
|
|
||||||
[thr['ref'], thr['href']]
|
|
||||||
end
|
|
||||||
|
|
||||||
def account?(xml = @xml)
|
|
||||||
!xml.at_xpath('./xmlns:author', xmlns: TagManager::XMLNS).nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
def redis
|
|
||||||
Redis.current
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,6 @@ class RejectFollowService < BaseService
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_xml(follow_request)
|
def build_xml(follow_request)
|
||||||
AtomSerializer.render(AtomSerializer.new.reject_follow_request_salmon(follow_request))
|
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.reject_follow_request_salmon(follow_request))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,6 +11,6 @@ class UnblockService < BaseService
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_xml(block)
|
def build_xml(block)
|
||||||
AtomSerializer.render(AtomSerializer.new.unblock_salmon(block))
|
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.unblock_salmon(block))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,6 +13,6 @@ class UnfavouriteService < BaseService
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_xml(favourite)
|
def build_xml(favourite)
|
||||||
AtomSerializer.render(AtomSerializer.new.unfavourite_salmon(favourite))
|
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.unfavourite_salmon(favourite))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,6 @@ class UnfollowService < BaseService
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_xml(follow)
|
def build_xml(follow)
|
||||||
AtomSerializer.render(AtomSerializer.new.unfollow_salmon(follow))
|
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.unfollow_salmon(follow))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,13 +18,13 @@
|
||||||
.landing-page
|
.landing-page
|
||||||
.header-wrapper
|
.header-wrapper
|
||||||
.mascot-container
|
.mascot-container
|
||||||
= image_tag asset_pack_path('elephant-fren.png'), class: 'mascot'
|
= image_tag asset_pack_path('elephant-fren.png'), alt: '', role: 'presentation', class: 'mascot'
|
||||||
|
|
||||||
.header
|
.header
|
||||||
.container.links
|
.container.links
|
||||||
.brand
|
.brand
|
||||||
= link_to root_url do
|
= link_to root_url do
|
||||||
= image_tag asset_pack_path('logo.svg')
|
= image_tag asset_pack_path('logo.svg'), alt: '', role: 'presentation'
|
||||||
Mastodon
|
Mastodon
|
||||||
|
|
||||||
%ul.nav
|
%ul.nav
|
||||||
|
@ -38,9 +38,9 @@
|
||||||
|
|
||||||
.container.hero
|
.container.hero
|
||||||
.floats
|
.floats
|
||||||
= image_tag asset_pack_path('cloud2.png'), class: 'float-1'
|
= image_tag asset_pack_path('cloud2.png'), alt: '', role: 'presentation', class: 'float-1'
|
||||||
= image_tag asset_pack_path('cloud3.png'), class: 'float-2'
|
= image_tag asset_pack_path('cloud3.png'), alt: '', role: 'presentation', class: 'float-2'
|
||||||
= image_tag asset_pack_path('cloud4.png'), class: 'float-3'
|
= image_tag asset_pack_path('cloud4.png'), alt: '', role: 'presentation', class: 'float-3'
|
||||||
.heading
|
.heading
|
||||||
%h1
|
%h1
|
||||||
= @instance_presenter.site_title
|
= @instance_presenter.site_title
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
%p= t('about.closed_registrations')
|
%p= t('about.closed_registrations')
|
||||||
- else
|
- else
|
||||||
= @instance_presenter.closed_registrations_message.html_safe
|
= @instance_presenter.closed_registrations_message.html_safe
|
||||||
= link_to t('about.find_another_instance'), 'https://joinmastodon.org', class: 'button button-alternative button--block'
|
= link_to t('about.find_another_instance'), 'https://joinmastodon.org/', class: 'button button-alternative button--block'
|
||||||
|
|
||||||
.learn-more-cta
|
.learn-more-cta
|
||||||
.container
|
.container
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
.about-mastodon
|
.about-mastodon
|
||||||
%h3= t 'about.what_is_mastodon'
|
%h3= t 'about.what_is_mastodon'
|
||||||
%p= t 'about.about_mastodon_html'
|
%p= t 'about.about_mastodon_html'
|
||||||
%a.button.button-secondary{ href: 'https://joinmastodon.org' }= t 'about.learn_more'
|
%a.button.button-secondary{ href: 'https://joinmastodon.org/' }= t 'about.learn_more'
|
||||||
= render 'features'
|
= render 'features'
|
||||||
.footer-links
|
.footer-links
|
||||||
.container
|
.container
|
||||||
|
|
|
@ -53,11 +53,11 @@
|
||||||
%td= @account.followers_count
|
%td= @account.followers_count
|
||||||
%tr
|
%tr
|
||||||
%th= t('admin.accounts.statuses')
|
%th= t('admin.accounts.statuses')
|
||||||
%td= @account.statuses_count
|
%td= link_to @account.statuses_count, admin_account_statuses_path(@account.id)
|
||||||
%tr
|
%tr
|
||||||
%th= t('admin.accounts.media_attachments')
|
%th= t('admin.accounts.media_attachments')
|
||||||
%td
|
%td
|
||||||
= @account.media_attachments.count
|
= link_to @account.media_attachments.count, admin_account_statuses_path(@account.id, { media: true })
|
||||||
= surround '(', ')' do
|
= surround '(', ')' do
|
||||||
= number_to_human_size @account.media_attachments.sum('file_file_size')
|
= number_to_human_size @account.media_attachments.sum('file_file_size')
|
||||||
%tr
|
%tr
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
- content_for :header_tags do
|
||||||
|
= javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
|
||||||
|
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('admin.reports.report', id: @report.id)
|
= t('admin.reports.report', id: @report.id)
|
||||||
|
|
||||||
|
@ -19,15 +22,26 @@
|
||||||
- unless @report.statuses.empty?
|
- unless @report.statuses.empty?
|
||||||
%hr/
|
%hr/
|
||||||
|
|
||||||
|
= form_for(@form, url: admin_report_reported_statuses_path(@report.id)) do |f|
|
||||||
|
.batch-form-box
|
||||||
|
.batch-checkbox-all
|
||||||
|
= check_box_tag :batch_checkbox_all, nil, false
|
||||||
|
= f.select :action, Form::StatusBatch::ACTION_TYPE.map{|action| [t("admin.statuses.batch.#{action}"), action]}
|
||||||
|
= f.submit t('admin.statuses.execute'), data: { confirm: t('admin.reports.are_you_sure') }, class: 'button'
|
||||||
|
.media-spoiler-toggle-buttons
|
||||||
|
.media-spoiler-show-button.button= t('admin.statuses.media.show')
|
||||||
|
.media-spoiler-hide-button.button= t('admin.statuses.media.hide')
|
||||||
- @report.statuses.each do |status|
|
- @report.statuses.each do |status|
|
||||||
.report-status
|
.report-status{ data: { id: status.id } }
|
||||||
|
.batch-checkbox
|
||||||
|
= f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
|
||||||
.activity-stream.activity-stream-headless
|
.activity-stream.activity-stream-headless
|
||||||
.entry= render 'stream_entries/simple_status', status: status
|
.entry= render 'stream_entries/simple_status', status: status
|
||||||
.report-status__actions
|
.report-status__actions
|
||||||
- unless status.media_attachments.empty?
|
- unless status.media_attachments.empty?
|
||||||
= link_to admin_report_reported_status_path(@report, status, status: { sensitive: !status.sensitive }), method: :patch, class: 'icon-button nsfw-button', title: t("admin.reports.nsfw.#{!status.sensitive}") do
|
= link_to admin_report_reported_status_path(@report, status, status: { sensitive: !status.sensitive }), method: :put, class: 'icon-button nsfw-button', title: t("admin.reports.nsfw.#{!status.sensitive}") do
|
||||||
= fa_icon status.sensitive? ? 'eye' : 'eye-slash'
|
= fa_icon status.sensitive? ? 'eye' : 'eye-slash'
|
||||||
= link_to admin_report_reported_status_path(@report, status), method: :delete, class: 'icon-button trash-button', title: t('admin.reports.delete'), data: { confirm: t('admin.reports.are_you_sure') } do
|
= link_to admin_report_reported_status_path(@report, status), method: :delete, class: 'icon-button trash-button', title: t('admin.reports.delete'), data: { confirm: t('admin.reports.are_you_sure') }, remote: true do
|
||||||
= fa_icon 'trash'
|
= fa_icon 'trash'
|
||||||
|
|
||||||
%hr/
|
%hr/
|
||||||
|
|
47
app/views/admin/statuses/index.html.haml
Normal file
47
app/views/admin/statuses/index.html.haml
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
- content_for :header_tags do
|
||||||
|
= javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
|
||||||
|
|
||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.statuses.title')
|
||||||
|
|
||||||
|
.back-link
|
||||||
|
= link_to admin_account_path(@account.id) do
|
||||||
|
%i.fa.fa-chevron-left.fa-fw
|
||||||
|
= t('admin.statuses.back_to_account')
|
||||||
|
|
||||||
|
.filters
|
||||||
|
.filter-subset
|
||||||
|
%strong= t('admin.statuses.media.title')
|
||||||
|
%ul
|
||||||
|
%li= link_to t('admin.statuses.no_media'), admin_account_statuses_path(@account.id, current_params.merge(media: nil)), class: !params[:media] && 'selected'
|
||||||
|
%li= link_to t('admin.statuses.with_media'), admin_account_statuses_path(@account.id, current_params.merge(media: true)), class: params[:media] && 'selected'
|
||||||
|
|
||||||
|
- if @statuses.empty?
|
||||||
|
.accounts-grid
|
||||||
|
= render 'accounts/nothing_here'
|
||||||
|
- else
|
||||||
|
= form_for(@form, url: admin_account_statuses_path(@account.id)) do |f|
|
||||||
|
= hidden_field_tag :page, params[:page]
|
||||||
|
= hidden_field_tag :media, params[:media]
|
||||||
|
.batch-form-box
|
||||||
|
.batch-checkbox-all
|
||||||
|
= check_box_tag :batch_checkbox_all, nil, false
|
||||||
|
= f.select :action, Form::StatusBatch::ACTION_TYPE.map{|action| [t("admin.statuses.batch.#{action}"), action]}
|
||||||
|
= f.submit t('admin.statuses.execute'), data: { confirm: t('admin.reports.are_you_sure') }, class: 'button'
|
||||||
|
.media-spoiler-toggle-buttons
|
||||||
|
.media-spoiler-show-button.button= t('admin.statuses.media.show')
|
||||||
|
.media-spoiler-hide-button.button= t('admin.statuses.media.hide')
|
||||||
|
- @statuses.each do |status|
|
||||||
|
.account-status{ data: { id: status.id } }
|
||||||
|
.batch-checkbox
|
||||||
|
= f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
|
||||||
|
.activity-stream.activity-stream-headless
|
||||||
|
.entry= render 'stream_entries/simple_status', status: status
|
||||||
|
.account-status__actions
|
||||||
|
- unless status.media_attachments.empty?
|
||||||
|
= link_to admin_account_status_path(@account.id, status, current_params.merge(status: { sensitive: !status.sensitive })), method: :patch, class: 'icon-button nsfw-button', title: t("admin.reports.nsfw.#{!status.sensitive}") do
|
||||||
|
= fa_icon status.sensitive? ? 'eye' : 'eye-slash'
|
||||||
|
= link_to admin_account_status_path(@account.id, status), method: :delete, class: 'icon-button trash-button', title: t('admin.reports.delete'), data: { confirm: t('admin.reports.are_you_sure') }, remote: true do
|
||||||
|
= fa_icon 'trash'
|
||||||
|
|
||||||
|
= paginate @statuses
|
|
@ -44,6 +44,7 @@
|
||||||
= f.input :setting_noindex, as: :boolean, wrapper: :with_label
|
= f.input :setting_noindex, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
|
= f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label
|
||||||
= f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
|
= f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
|
||||||
= f.input :setting_delete_modal, as: :boolean, wrapper: :with_label
|
= f.input :setting_delete_modal, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Pubsubhubbub::DistributionWorker
|
||||||
def distribute_public!(stream_entries)
|
def distribute_public!(stream_entries)
|
||||||
return if stream_entries.empty?
|
return if stream_entries.empty?
|
||||||
|
|
||||||
@payload = AtomSerializer.render(AtomSerializer.new.feed(@account, stream_entries))
|
@payload = Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.feed(@account, stream_entries))
|
||||||
|
|
||||||
Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription|
|
Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription|
|
||||||
[subscription.id, @payload]
|
[subscription.id, @payload]
|
||||||
|
@ -32,7 +32,7 @@ class Pubsubhubbub::DistributionWorker
|
||||||
def distribute_hidden!(stream_entries)
|
def distribute_hidden!(stream_entries)
|
||||||
return if stream_entries.empty?
|
return if stream_entries.empty?
|
||||||
|
|
||||||
@payload = AtomSerializer.render(AtomSerializer.new.feed(@account, stream_entries))
|
@payload = Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.feed(@account, stream_entries))
|
||||||
@domains = @account.followers.domains
|
@domains = @account.followers.domains
|
||||||
|
|
||||||
Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions.reject { |s| !allowed_to_receive?(s.callback_url, s.domain) }) do |subscription|
|
Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions.reject { |s| !allowed_to_receive?(s.callback_url, s.domain) }) do |subscription|
|
||||||
|
|
|
@ -5,22 +5,18 @@ class WebPushNotificationWorker
|
||||||
|
|
||||||
sidekiq_options backtrace: true
|
sidekiq_options backtrace: true
|
||||||
|
|
||||||
def perform(recipient_id, notification_id)
|
def perform(session_activation_id, notification_id)
|
||||||
recipient = Account.find(recipient_id)
|
session_activation = SessionActivation.find(session_activation_id)
|
||||||
notification = Notification.find(notification_id)
|
notification = Notification.find(notification_id)
|
||||||
|
|
||||||
sessions_with_subscriptions = recipient.user.session_activations.where.not(web_push_subscription: nil)
|
|
||||||
|
|
||||||
sessions_with_subscriptions.each do |session|
|
|
||||||
begin
|
begin
|
||||||
session.web_push_subscription.push(notification)
|
session_activation.web_push_subscription.push(notification)
|
||||||
rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription
|
rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription => e
|
||||||
# Subscription expiration is not currently implemented in any browser
|
# Subscription expiration is not currently implemented in any browser
|
||||||
session.web_push_subscription.destroy!
|
session_activation.web_push_subscription.destroy!
|
||||||
session.update!(web_push_subscription: nil)
|
session_activation.update!(web_push_subscription: nil)
|
||||||
rescue Webpush::PayloadTooLarge => e
|
|
||||||
Rails.logger.error(e)
|
raise e
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -185,6 +185,21 @@ en:
|
||||||
desc_html: Display public timeline on landing page
|
desc_html: Display public timeline on landing page
|
||||||
title: Timeline preview
|
title: Timeline preview
|
||||||
title: Site Settings
|
title: Site Settings
|
||||||
|
statuses:
|
||||||
|
back_to_account: Back to account page
|
||||||
|
batch:
|
||||||
|
delete: Delete
|
||||||
|
nsfw_off: NSFW OFF
|
||||||
|
nsfw_on: NSFW ON
|
||||||
|
execute: Execute
|
||||||
|
failed_to_execute: Failed to execute
|
||||||
|
media:
|
||||||
|
hide: Hide media
|
||||||
|
show: Show media
|
||||||
|
title: Media
|
||||||
|
no_media: No media
|
||||||
|
with_media: With media
|
||||||
|
title: Account statuses
|
||||||
subscriptions:
|
subscriptions:
|
||||||
callback_url: Callback URL
|
callback_url: Callback URL
|
||||||
confirmed: Confirmed
|
confirmed: Confirmed
|
||||||
|
|
|
@ -1,15 +1,28 @@
|
||||||
---
|
---
|
||||||
ja:
|
ja:
|
||||||
about:
|
about:
|
||||||
about_mastodon: Mastodon は<em>自由でオープンソース</em>なソーシャルネットワークです。商用プラットフォームの代替となる<em>分散型</em>を採用し、あなたのやりとりが一つの会社によって独占されるのを防ぎます。信頼できるインスタンスを選択してください — どのインスタンスを選んでも、誰とでもやりとりすることができます。 だれでも自分の Mastodon インスタンスを作ることができ、シームレスに<em>ソーシャルネットワーク</em>に参加できます。
|
about_mastodon_html: Mastodon は、オープンなウェブプロトコルを採用した、自由でオープンソースなソーシャルネットワークです。電子メールのような分散型の仕組みを採っています。
|
||||||
about_this: このインスタンスについて
|
about_this: このインスタンスについて
|
||||||
business_email: 'ビジネスメールアドレス:'
|
business_email: 'ビジネスメールアドレス:'
|
||||||
closed_registrations: 現在このインスタンスでの新規登録は受け付けていません。
|
closed_registrations: 現在このインスタンスでの新規登録は受け付けていません。しかし、他のインスタンスにアカウントを作成しても全く同じネットワークに参加することができます。
|
||||||
contact: 連絡先
|
contact: 連絡先
|
||||||
description_headline: "%{domain} とは?"
|
description_headline: "%{domain} とは?"
|
||||||
domain_count_after: 個のインスタンス
|
domain_count_after: 個のインスタンス
|
||||||
domain_count_before: 接続中
|
domain_count_before: 接続中
|
||||||
|
features:
|
||||||
|
humane_approach_body: 他の SNS の失敗から学び、Mastodon はソーシャルメディアが誤った使い方をされることの無いように倫理的な設計を目指しています。
|
||||||
|
humane_approach_title: より思いやりのある設計
|
||||||
|
not_a_product_body: Mastodon は営利的な SNS ではありません。広告や、データの収集・解析は無く、またユーザーの囲い込みもありません。
|
||||||
|
not_a_product_title: あなたは人間であり、商品ではありません
|
||||||
|
real_conversation_body: 好きなように書ける500文字までの投稿や、文章やメディアの内容に警告をつけられる機能で、思い通りに自分自身を表現することができます。
|
||||||
|
real_conversation_title: 本当のコミュニケーションのために
|
||||||
|
within_reach_body: デベロッパーフレンドリーな API により実現された、iOS や Android、その他様々なプラットフォームのためのアプリでどこでも友人とやりとりできます。
|
||||||
|
within_reach_title: いつでも身近に
|
||||||
|
find_another_instance: 他のインスタンスを探す
|
||||||
|
generic_description: "%{domain} は、Mastodon インスタンスの一つです。"
|
||||||
get_started: 参加する
|
get_started: 参加する
|
||||||
|
hosted_on: Mastodon hosted on %{domain}
|
||||||
|
learn_more: もっと詳しく
|
||||||
links: リンク
|
links: リンク
|
||||||
other_instances: 他のインスタンス
|
other_instances: 他のインスタンス
|
||||||
source_code: ソースコード
|
source_code: ソースコード
|
||||||
|
@ -19,6 +32,7 @@ ja:
|
||||||
user_count_after: 人
|
user_count_after: 人
|
||||||
user_count_before: ユーザー数
|
user_count_before: ユーザー数
|
||||||
version: バージョン
|
version: バージョン
|
||||||
|
what_is_mastodon: Mastodon とは?
|
||||||
accounts:
|
accounts:
|
||||||
follow: フォロー
|
follow: フォロー
|
||||||
followers: フォロワー
|
followers: フォロワー
|
||||||
|
@ -171,6 +185,21 @@ ja:
|
||||||
desc_html: ランディングページに公開タイムラインを表示します
|
desc_html: ランディングページに公開タイムラインを表示します
|
||||||
title: タイムラインプレビュー
|
title: タイムラインプレビュー
|
||||||
title: サイト設定
|
title: サイト設定
|
||||||
|
statuses:
|
||||||
|
back_to_account: アカウントページに戻る
|
||||||
|
batch:
|
||||||
|
delete: 削除
|
||||||
|
nsfw_off: NSFW オフ
|
||||||
|
nsfw_on: NSFW オン
|
||||||
|
execute: 実行
|
||||||
|
failed_to_execute: 実行に失敗しました
|
||||||
|
media:
|
||||||
|
hide: メディアを隠す
|
||||||
|
show: メディアを表示
|
||||||
|
title: メディア
|
||||||
|
no_media: メディアなし
|
||||||
|
with_media: メディアあり
|
||||||
|
title: トゥート一覧
|
||||||
subscriptions:
|
subscriptions:
|
||||||
callback_url: コールバックURL
|
callback_url: コールバックURL
|
||||||
confirmed: 確認済み
|
confirmed: 確認済み
|
||||||
|
@ -190,9 +219,10 @@ ja:
|
||||||
applications:
|
applications:
|
||||||
invalid_url: URLが無効です
|
invalid_url: URLが無効です
|
||||||
auth:
|
auth:
|
||||||
|
agreement_html: 登録すると <a href="%{rules_path}">利用規約</a> と <a href="%{terms_path}">プライバシーポリシー</a> に同意したことになります。
|
||||||
change_password: セキュリティ
|
change_password: セキュリティ
|
||||||
delete_account: アカウントの削除
|
delete_account: アカウントの削除
|
||||||
delete_account_html: アカウントを削除したい場合、<a href="%{path}">こちら</a>から手続きが行えます。削除前には確認画面があります。
|
delete_account_html: アカウントを削除したい場合、<a href="%{path}">こちら</a> から手続きが行えます。削除する前に、確認画面があります。
|
||||||
didnt_get_confirmation: 確認メールを受信できませんか?
|
didnt_get_confirmation: 確認メールを受信できませんか?
|
||||||
forgot_password: パスワードをお忘れですか?
|
forgot_password: パスワードをお忘れですか?
|
||||||
login: ログイン
|
login: ログイン
|
||||||
|
|
|
@ -42,6 +42,7 @@ en:
|
||||||
setting_default_sensitive: Always mark media as sensitive
|
setting_default_sensitive: Always mark media as sensitive
|
||||||
setting_delete_modal: Show confirmation dialog before deleting a toot
|
setting_delete_modal: Show confirmation dialog before deleting a toot
|
||||||
setting_system_font_ui: Use system's default font
|
setting_system_font_ui: Use system's default font
|
||||||
|
setting_unfollow_modal: Show confirmation dialog before unfollowing someone
|
||||||
setting_noindex: Opt-out of search engine indexing
|
setting_noindex: Opt-out of search engine indexing
|
||||||
severity: Severity
|
severity: Severity
|
||||||
type: Import type
|
type: Import type
|
||||||
|
|
|
@ -8,6 +8,8 @@ ja:
|
||||||
header: 2MBまでのPNGやGIF、JPGが利用可能です。 700x335pxまで縮小されます。
|
header: 2MBまでのPNGやGIF、JPGが利用可能です。 700x335pxまで縮小されます。
|
||||||
locked: フォロワーを手動で承認する必要があります。
|
locked: フォロワーを手動で承認する必要があります。
|
||||||
note: あと<span class="note-counter">%{count}</span>文字入力できます。
|
note: あと<span class="note-counter">%{count}</span>文字入力できます。
|
||||||
|
setting_noindex: 公開プロフィールおよび各投稿ページに影響します
|
||||||
|
|
||||||
imports:
|
imports:
|
||||||
data: 他の Mastodon インスタンスからエクスポートしたCSVファイルを選択して下さい
|
data: 他の Mastodon インスタンスからエクスポートしたCSVファイルを選択して下さい
|
||||||
sessions:
|
sessions:
|
||||||
|
@ -37,6 +39,7 @@ ja:
|
||||||
setting_default_sensitive: メディアを常に閲覧注意としてマークする
|
setting_default_sensitive: メディアを常に閲覧注意としてマークする
|
||||||
setting_delete_modal: トゥートを削除する前に確認ダイアログを表示する
|
setting_delete_modal: トゥートを削除する前に確認ダイアログを表示する
|
||||||
setting_system_font_ui: システムのデフォルトフォントを使う
|
setting_system_font_ui: システムのデフォルトフォントを使う
|
||||||
|
setting_noindex: 検索エンジンによるインデックスを拒否する
|
||||||
severity: 重大性
|
severity: 重大性
|
||||||
type: インポートする項目
|
type: インポートする項目
|
||||||
username: ユーザー名
|
username: ユーザー名
|
||||||
|
|
|
@ -89,7 +89,7 @@ Rails.application.routes.draw do
|
||||||
resources :instances, only: [:index]
|
resources :instances, only: [:index]
|
||||||
|
|
||||||
resources :reports, only: [:index, :show, :update] do
|
resources :reports, only: [:index, :show, :update] do
|
||||||
resources :reported_statuses, only: [:update, :destroy]
|
resources :reported_statuses, only: [:create, :update, :destroy]
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :accounts, only: [:index, :show] do
|
resources :accounts, only: [:index, :show] do
|
||||||
|
@ -103,6 +103,7 @@ Rails.application.routes.draw do
|
||||||
resource :silence, only: [:create, :destroy]
|
resource :silence, only: [:create, :destroy]
|
||||||
resource :suspension, only: [:create, :destroy]
|
resource :suspension, only: [:create, :destroy]
|
||||||
resource :confirmation, only: [:create]
|
resource :confirmation, only: [:create]
|
||||||
|
resources :statuses, only: [:index, :create, :update, :destroy]
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :users, only: [] do
|
resources :users, only: [] do
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
"es6-symbol": "^3.1.1",
|
"es6-symbol": "^3.1.1",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"express": "^4.15.2",
|
"express": "^4.15.2",
|
||||||
"extract-text-webpack-plugin": "^3.0.0",
|
"extract-text-webpack-plugin": "^2.1.2",
|
||||||
"file-loader": "^0.11.2",
|
"file-loader": "^0.11.2",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"glob": "^7.1.1",
|
"glob": "^7.1.1",
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
"tiny-queue": "^0.2.1",
|
"tiny-queue": "^0.2.1",
|
||||||
"uuid": "^3.1.0",
|
"uuid": "^3.1.0",
|
||||||
"uws": "^8.14.0",
|
"uws": "^8.14.0",
|
||||||
"webpack": "^3.2.0",
|
"webpack": "^3.0.0",
|
||||||
"webpack-bundle-analyzer": "^2.8.2",
|
"webpack-bundle-analyzer": "^2.8.2",
|
||||||
"webpack-manifest-plugin": "^1.1.2",
|
"webpack-manifest-plugin": "^1.1.2",
|
||||||
"webpack-merge": "^4.1.0",
|
"webpack-merge": "^4.1.0",
|
||||||
|
|
|
@ -11,6 +11,42 @@ describe Admin::ReportedStatusesController do
|
||||||
sign_in user, scope: :user
|
sign_in user, scope: :user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'POST #create' do
|
||||||
|
subject do
|
||||||
|
-> { post :create, params: { report_id: report, form_status_batch: { action: action, status_ids: status_ids } } }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:action) { 'nsfw_on' }
|
||||||
|
let(:status_ids) { [status.id] }
|
||||||
|
let(:status) { Fabricate(:status, sensitive: !sensitive) }
|
||||||
|
let(:sensitive) { true }
|
||||||
|
let!(:media_attachment) { Fabricate(:media_attachment, status: status) }
|
||||||
|
|
||||||
|
context 'updates sensitive column to true' do
|
||||||
|
it 'updates sensitive column' do
|
||||||
|
is_expected.to change {
|
||||||
|
status.reload.sensitive
|
||||||
|
}.from(false).to(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'updates sensitive column to false' do
|
||||||
|
let(:action) { 'nsfw_off' }
|
||||||
|
let(:sensitive) { false }
|
||||||
|
|
||||||
|
it 'updates sensitive column' do
|
||||||
|
is_expected.to change {
|
||||||
|
status.reload.sensitive
|
||||||
|
}.from(true).to(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to report page' do
|
||||||
|
subject.call
|
||||||
|
expect(response).to redirect_to(admin_report_path(report))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'PATCH #update' do
|
describe 'PATCH #update' do
|
||||||
subject do
|
subject do
|
||||||
-> { patch :update, params: { report_id: report, id: status, status: { sensitive: sensitive } } }
|
-> { patch :update, params: { report_id: report, id: status, status: { sensitive: sensitive } } }
|
||||||
|
@ -48,7 +84,7 @@ describe Admin::ReportedStatusesController do
|
||||||
allow(RemovalWorker).to receive(:perform_async)
|
allow(RemovalWorker).to receive(:perform_async)
|
||||||
|
|
||||||
delete :destroy, params: { report_id: report, id: status }
|
delete :destroy, params: { report_id: report, id: status }
|
||||||
expect(response).to redirect_to(admin_report_path(report))
|
expect(response).to have_http_status(:success)
|
||||||
expect(RemovalWorker).
|
expect(RemovalWorker).
|
||||||
to have_received(:perform_async).with(status.id)
|
to have_received(:perform_async).with(status.id)
|
||||||
end
|
end
|
||||||
|
|
107
spec/controllers/admin/statuses_controller_spec.rb
Normal file
107
spec/controllers/admin/statuses_controller_spec.rb
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Admin::StatusesController do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:user) { Fabricate(:user, admin: true) }
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
let!(:status) { Fabricate(:status, account: account) }
|
||||||
|
let(:media_attached_status) { Fabricate(:status, account: account, sensitive: !sensitive) }
|
||||||
|
let!(:media_attachment) { Fabricate(:media_attachment, account: account, status: media_attached_status) }
|
||||||
|
let(:sensitive) { true }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in user, scope: :user
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
|
it 'returns http success with no media' do
|
||||||
|
get :index, params: { account_id: account.id }
|
||||||
|
|
||||||
|
statuses = assigns(:statuses).to_a
|
||||||
|
expect(statuses.size).to eq 2
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success with media' do
|
||||||
|
get :index, params: { account_id: account.id , media: true }
|
||||||
|
|
||||||
|
statuses = assigns(:statuses).to_a
|
||||||
|
expect(statuses.size).to eq 1
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #create' do
|
||||||
|
subject do
|
||||||
|
-> { post :create, params: { account_id: account.id, form_status_batch: { action: action, status_ids: status_ids } } }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:action) { 'nsfw_on' }
|
||||||
|
let(:status_ids) { [media_attached_status.id] }
|
||||||
|
|
||||||
|
context 'updates sensitive column to true' do
|
||||||
|
it 'updates sensitive column' do
|
||||||
|
is_expected.to change {
|
||||||
|
media_attached_status.reload.sensitive
|
||||||
|
}.from(false).to(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'updates sensitive column to false' do
|
||||||
|
let(:action) { 'nsfw_off' }
|
||||||
|
let(:sensitive) { false }
|
||||||
|
|
||||||
|
it 'updates sensitive column' do
|
||||||
|
is_expected.to change {
|
||||||
|
media_attached_status.reload.sensitive
|
||||||
|
}.from(true).to(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to account statuses page' do
|
||||||
|
subject.call
|
||||||
|
expect(response).to redirect_to(admin_account_statuses_path(account.id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH #update' do
|
||||||
|
subject do
|
||||||
|
-> { patch :update, params: { account_id: account.id, id: media_attached_status, status: { sensitive: sensitive } } }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'updates sensitive column to true' do
|
||||||
|
it 'updates sensitive column' do
|
||||||
|
is_expected.to change {
|
||||||
|
media_attached_status.reload.sensitive
|
||||||
|
}.from(false).to(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'updates sensitive column to false' do
|
||||||
|
let(:sensitive) { false }
|
||||||
|
|
||||||
|
it 'updates sensitive column' do
|
||||||
|
is_expected.to change {
|
||||||
|
media_attached_status.reload.sensitive
|
||||||
|
}.from(true).to(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirects to account statuses page' do
|
||||||
|
subject.call
|
||||||
|
expect(response).to redirect_to(admin_account_statuses_path(account.id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE #destroy' do
|
||||||
|
it 'removes a status' do
|
||||||
|
allow(RemovalWorker).to receive(:perform_async)
|
||||||
|
|
||||||
|
delete :destroy, params: { account_id: account.id, id: status }
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(RemovalWorker).
|
||||||
|
to have_received(:perform_async).with(status.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
File diff suppressed because it is too large
Load diff
|
@ -35,6 +35,13 @@ describe UserSettingsDecorator do
|
||||||
expect(user.settings['default_sensitive']).to eq true
|
expect(user.settings['default_sensitive']).to eq true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'updates the user settings value for unfollow modal' do
|
||||||
|
values = { 'setting_unfollow_modal' => '0' }
|
||||||
|
|
||||||
|
settings.update(values)
|
||||||
|
expect(user.settings['unfollow_modal']).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
it 'updates the user settings value for boost modal' do
|
it 'updates the user settings value for boost modal' do
|
||||||
values = { 'setting_boost_modal' => '1' }
|
values = { 'setting_boost_modal' => '1' }
|
||||||
|
|
||||||
|
|
52
spec/models/form/status_batch_spec.rb
Normal file
52
spec/models/form/status_batch_spec.rb
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Form::StatusBatch do
|
||||||
|
let(:form) { Form::StatusBatch.new(action: action, status_ids: status_ids) }
|
||||||
|
let(:status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
describe 'with nsfw action' do
|
||||||
|
let(:status_ids) { [status.id, nonsensitive_status.id, sensitive_status.id] }
|
||||||
|
let(:nonsensitive_status) { Fabricate(:status, sensitive: false) }
|
||||||
|
let(:sensitive_status) { Fabricate(:status, sensitive: true) }
|
||||||
|
let!(:shown_media_attachment) { Fabricate(:media_attachment, status: nonsensitive_status) }
|
||||||
|
let!(:hidden_media_attachment) { Fabricate(:media_attachment, status: sensitive_status) }
|
||||||
|
|
||||||
|
context 'nsfw_on' do
|
||||||
|
let(:action) { 'nsfw_on' }
|
||||||
|
|
||||||
|
it { expect(form.save).to be true }
|
||||||
|
it { expect { form.save }.to change { nonsensitive_status.reload.sensitive }.from(false).to(true) }
|
||||||
|
it { expect { form.save }.not_to change { sensitive_status.reload.sensitive } }
|
||||||
|
it { expect { form.save }.not_to change { status.reload.sensitive } }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'nsfw_off' do
|
||||||
|
let(:action) { 'nsfw_off' }
|
||||||
|
|
||||||
|
it { expect(form.save).to be true }
|
||||||
|
it { expect { form.save }.to change { sensitive_status.reload.sensitive }.from(true).to(false) }
|
||||||
|
it { expect { form.save }.not_to change { nonsensitive_status.reload.sensitive } }
|
||||||
|
it { expect { form.save }.not_to change { status.reload.sensitive } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'with delete action' do
|
||||||
|
let(:status_ids) { [status.id] }
|
||||||
|
let(:action) { 'delete' }
|
||||||
|
let!(:another_status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(RemovalWorker).to receive(:perform_async)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'call RemovalWorker' do
|
||||||
|
form.save
|
||||||
|
expect(RemovalWorker).to have_received(:perform_async).with(status.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'do not call RemovalWorker' do
|
||||||
|
form.save
|
||||||
|
expect(RemovalWorker).not_to have_received(:perform_async).with(another_status.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -219,6 +219,14 @@ RSpec.describe User, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#setting_unfollow_modal' do
|
||||||
|
it 'returns unfollow modal setting' do
|
||||||
|
user = Fabricate(:user)
|
||||||
|
user.settings[:unfollow_modal] = true
|
||||||
|
expect(user.setting_unfollow_modal).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#setting_delete_modal' do
|
describe '#setting_delete_modal' do
|
||||||
it 'returns delete modal setting' do
|
it 'returns delete modal setting' do
|
||||||
user = Fabricate(:user)
|
user = Fabricate(:user)
|
||||||
|
|
|
@ -167,6 +167,46 @@ XML
|
||||||
expect(created_statuses.first.reblog.text).to eq 'Overwatch rocks'
|
expect(created_statuses.first.reblog.text).to eq 'Overwatch rocks'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'ignores reblogs if it failed to retreive reblogged statuses' do
|
||||||
|
stub_request(:head, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 404)
|
||||||
|
|
||||||
|
actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com')
|
||||||
|
|
||||||
|
body = <<XML
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
|
||||||
|
<id>tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status</id>
|
||||||
|
<published>2017-04-27T13:49:25Z</published>
|
||||||
|
<updated>2017-04-27T13:49:25Z</updated>
|
||||||
|
<author>
|
||||||
|
<id>https://overwatch.com/users/tracer</id>
|
||||||
|
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||||
|
<uri>https://overwatch.com/users/tracer</uri>
|
||||||
|
<name>tracer</name>
|
||||||
|
</author>
|
||||||
|
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
|
||||||
|
<activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
|
||||||
|
<content type="html">Overwatch rocks</content>
|
||||||
|
<activity:object>
|
||||||
|
<id>tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status</id>
|
||||||
|
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
|
||||||
|
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||||
|
<author>
|
||||||
|
<id>https://overwatch.com/users/tracer</id>
|
||||||
|
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||||
|
<uri>https://overwatch.com/users/tracer</uri>
|
||||||
|
<name>tracer</name>
|
||||||
|
</author>
|
||||||
|
<content type="html">Overwatch rocks</content>
|
||||||
|
<link rel="alternate" type="text/html" href="https://overwatch.com/users/tracer/updates/1" />
|
||||||
|
</activity:object>
|
||||||
|
XML
|
||||||
|
|
||||||
|
created_statuses = subject.call(body, actor)
|
||||||
|
|
||||||
|
expect(created_statuses).to eq []
|
||||||
|
end
|
||||||
|
|
||||||
it 'ignores statuses with an out-of-order delete' do
|
it 'ignores statuses with an out-of-order delete' do
|
||||||
sender = Fabricate(:account, username: 'tracer', domain: 'overwatch.com')
|
sender = Fabricate(:account, username: 'tracer', domain: 'overwatch.com')
|
||||||
|
|
||||||
|
|
51
yarn.lock
51
yarn.lock
|
@ -415,7 +415,7 @@ async@^1.5.2:
|
||||||
version "1.5.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||||
|
|
||||||
async@^2.1.2, async@^2.1.4, async@^2.1.5, async@^2.4.1:
|
async@^2.1.2, async@^2.1.4, async@^2.1.5:
|
||||||
version "2.5.0"
|
version "2.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
|
resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -1657,7 +1657,7 @@ cheerio@^0.22.0:
|
||||||
lodash.reject "^4.4.0"
|
lodash.reject "^4.4.0"
|
||||||
lodash.some "^4.4.0"
|
lodash.some "^4.4.0"
|
||||||
|
|
||||||
chokidar@^1.4.3, chokidar@^1.6.0:
|
chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.7.0:
|
||||||
version "1.7.0"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2868,12 +2868,12 @@ extglob@^0.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-extglob "^1.0.0"
|
is-extglob "^1.0.0"
|
||||||
|
|
||||||
extract-text-webpack-plugin@^3.0.0:
|
extract-text-webpack-plugin@^2.1.2:
|
||||||
version "3.0.0"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612"
|
resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-2.1.2.tgz#756ef4efa8155c3681833fbc34da53b941746d6c"
|
||||||
dependencies:
|
dependencies:
|
||||||
async "^2.4.1"
|
async "^2.1.2"
|
||||||
loader-utils "^1.1.0"
|
loader-utils "^1.0.2"
|
||||||
schema-utils "^0.3.0"
|
schema-utils "^0.3.0"
|
||||||
webpack-sources "^1.0.1"
|
webpack-sources "^1.0.1"
|
||||||
|
|
||||||
|
@ -7328,6 +7328,14 @@ watchpack@^1.3.1:
|
||||||
chokidar "^1.4.3"
|
chokidar "^1.4.3"
|
||||||
graceful-fs "^4.1.2"
|
graceful-fs "^4.1.2"
|
||||||
|
|
||||||
|
watchpack@^1.4.0:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
|
||||||
|
dependencies:
|
||||||
|
async "^2.1.2"
|
||||||
|
chokidar "^1.7.0"
|
||||||
|
graceful-fs "^4.1.2"
|
||||||
|
|
||||||
wbuf@^1.1.0, wbuf@^1.7.2:
|
wbuf@^1.1.0, wbuf@^1.7.2:
|
||||||
version "1.7.2"
|
version "1.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe"
|
resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe"
|
||||||
|
@ -7425,7 +7433,7 @@ webpack-sources@^1.0.1:
|
||||||
source-list-map "^2.0.0"
|
source-list-map "^2.0.0"
|
||||||
source-map "~0.5.3"
|
source-map "~0.5.3"
|
||||||
|
|
||||||
"webpack@^2.5.1 || ^3.0.0", webpack@^3.2.0:
|
"webpack@^2.5.1 || ^3.0.0":
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.2.0.tgz#8b0cae0e1a9fd76bfbf0eab61a8c2ada848c312f"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.2.0.tgz#8b0cae0e1a9fd76bfbf0eab61a8c2ada848c312f"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -7452,6 +7460,33 @@ webpack-sources@^1.0.1:
|
||||||
webpack-sources "^1.0.1"
|
webpack-sources "^1.0.1"
|
||||||
yargs "^6.0.0"
|
yargs "^6.0.0"
|
||||||
|
|
||||||
|
webpack@^3.0.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.3.0.tgz#ce2f9e076566aba91f74887133a883fd7da187bc"
|
||||||
|
dependencies:
|
||||||
|
acorn "^5.0.0"
|
||||||
|
acorn-dynamic-import "^2.0.0"
|
||||||
|
ajv "^5.1.5"
|
||||||
|
ajv-keywords "^2.0.0"
|
||||||
|
async "^2.1.2"
|
||||||
|
enhanced-resolve "^3.3.0"
|
||||||
|
escope "^3.6.0"
|
||||||
|
interpret "^1.0.0"
|
||||||
|
json-loader "^0.5.4"
|
||||||
|
json5 "^0.5.1"
|
||||||
|
loader-runner "^2.3.0"
|
||||||
|
loader-utils "^1.1.0"
|
||||||
|
memory-fs "~0.4.1"
|
||||||
|
mkdirp "~0.5.0"
|
||||||
|
node-libs-browser "^2.0.0"
|
||||||
|
source-map "^0.5.3"
|
||||||
|
supports-color "^3.1.0"
|
||||||
|
tapable "~0.2.5"
|
||||||
|
uglifyjs-webpack-plugin "^0.4.6"
|
||||||
|
watchpack "^1.4.0"
|
||||||
|
webpack-sources "^1.0.1"
|
||||||
|
yargs "^6.0.0"
|
||||||
|
|
||||||
websocket-driver@>=0.5.1:
|
websocket-driver@>=0.5.1:
|
||||||
version "0.6.5"
|
version "0.6.5"
|
||||||
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36"
|
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36"
|
||||||
|
|
Loading…
Reference in a new issue