Merge branch 'tootsuite-master'
This commit is contained in:
commit
df74e26baf
@ -12,9 +12,11 @@ EXPOSE 3000 4000
|
||||
WORKDIR /mastodon
|
||||
|
||||
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 add -t build-dependencies \
|
||||
build-base \
|
||||
icu-dev \
|
||||
libidn-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
@ -26,7 +28,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
|
||||
ffmpeg \
|
||||
file \
|
||||
git \
|
||||
icu-dev \
|
||||
icu-libs \
|
||||
imagemagick@edge \
|
||||
libidn \
|
||||
libpq \
|
||||
@ -37,7 +39,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
|
||||
protobuf \
|
||||
su-exec \
|
||||
tini \
|
||||
&& npm install -g npm@3 && npm install -g yarn \
|
||||
yarn@edge \
|
||||
&& update-ca-certificates \
|
||||
&& rm -rf /tmp/* /var/cache/apk/*
|
||||
|
||||
|
3
Vagrantfile
vendored
3
Vagrantfile
vendored
@ -35,9 +35,10 @@ sudo apt-get install \
|
||||
postgresql-contrib \
|
||||
protobuf-compiler \
|
||||
yarn \
|
||||
libicu-dev \
|
||||
libidn11-dev \
|
||||
libprotobuf-dev \
|
||||
libreadline-dev \
|
||||
libicu-dev \
|
||||
-y
|
||||
|
||||
# Install rvm
|
||||
|
@ -13,7 +13,7 @@ class AccountsController < ApplicationController
|
||||
|
||||
format.atom do
|
||||
@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
|
||||
|
||||
format.json do
|
||||
|
@ -5,7 +5,14 @@ module Admin
|
||||
include Authorization
|
||||
|
||||
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
|
||||
@status.update(status_params)
|
||||
@ -15,7 +22,7 @@ module Admin
|
||||
def destroy
|
||||
authorize @status, :destroy?
|
||||
RemovalWorker.perform_async(@status.id)
|
||||
redirect_to admin_report_path(@report)
|
||||
render json: @status
|
||||
end
|
||||
|
||||
private
|
||||
@ -24,6 +31,10 @@ module Admin
|
||||
params.require(:status).permit(:sensitive)
|
||||
end
|
||||
|
||||
def form_status_batch_params
|
||||
params.require(:form_status_batch).permit(:action, status_ids: [])
|
||||
end
|
||||
|
||||
def set_report
|
||||
@report = Report.find(params[:report_id])
|
||||
end
|
||||
|
@ -8,7 +8,9 @@ module Admin
|
||||
@reports = filtered_reports.page(params[:page])
|
||||
end
|
||||
|
||||
def show; end
|
||||
def show
|
||||
@form = Form::StatusBatch.new
|
||||
end
|
||||
|
||||
def update
|
||||
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(
|
||||
:setting_default_privacy,
|
||||
:setting_default_sensitive,
|
||||
:setting_unfollow_modal,
|
||||
:setting_boost_modal,
|
||||
:setting_delete_modal,
|
||||
:setting_auto_play_gif,
|
||||
|
@ -19,7 +19,7 @@ class StreamEntriesController < ApplicationController
|
||||
end
|
||||
|
||||
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
|
||||
|
@ -186,6 +186,12 @@ export default class MediaGallery extends React.PureComponent {
|
||||
visible: !this.props.sensitive,
|
||||
};
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.sensitive !== this.props.sensitive) {
|
||||
this.setState({ visible: !nextProps.sensitive });
|
||||
}
|
||||
}
|
||||
|
||||
handleOpen = () => {
|
||||
this.setState({ visible: !this.state.visible });
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { makeGetAccount } from '../selectors';
|
||||
import Account from '../components/account';
|
||||
import {
|
||||
@ -9,6 +11,11 @@ import {
|
||||
muteAccount,
|
||||
unmuteAccount,
|
||||
} from '../actions/accounts';
|
||||
import { openModal } from '../actions/modal';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
@ -16,15 +23,25 @@ const makeMapStateToProps = () => {
|
||||
const mapStateToProps = (state, props) => ({
|
||||
account: getAccount(state, props.id),
|
||||
me: state.getIn(['meta', 'me']),
|
||||
unfollowModal: state.getIn(['meta', 'unfollow_modal']),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onFollow (account) {
|
||||
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')));
|
||||
}
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
}
|
||||
@ -45,6 +62,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
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';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
|
||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||
@ -28,15 +29,25 @@ const makeMapStateToProps = () => {
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, Number(accountId)),
|
||||
me: state.getIn(['meta', 'me']),
|
||||
unfollowModal: state.getIn(['meta', 'unfollow_modal']),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onFollow (account) {
|
||||
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')));
|
||||
}
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
}
|
||||
@ -85,6 +96,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onUnblockDomain (domain, accountId) {
|
||||
dispatch(unblockDomain(domain, accountId));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
|
||||
|
@ -87,7 +87,7 @@ export default class ModalRoot extends React.PureComponent {
|
||||
>
|
||||
{interpolatedStyles =>
|
||||
<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 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})` }}>
|
||||
|
@ -10,7 +10,17 @@ const makeGetStatusIds = () => createSelector([
|
||||
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
|
||||
(state) => state.get('statuses'),
|
||||
(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);
|
||||
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);
|
||||
}
|
||||
|
||||
if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {
|
||||
try {
|
||||
if (showStatus) {
|
||||
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
|
||||
}
|
||||
if (showStatus && regex && statusForId.get('account') !== me) {
|
||||
const searchIndex = statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'search_index']) : statusForId.get('search_index');
|
||||
showStatus = !regex.test(searchIndex);
|
||||
}
|
||||
|
||||
return showStatus;
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
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.mute.confirm": "أكتم",
|
||||
"confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"emoji_button.activity": "الأنشطة",
|
||||
"emoji_button.flags": "الأعلام",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"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.mute.confirm": "Silenciar",
|
||||
"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.flags": "Flags",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"emoji_button.food": "Food & Drink",
|
||||
|
@ -228,6 +228,19 @@
|
||||
],
|
||||
"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": [
|
||||
{
|
||||
@ -268,6 +281,10 @@
|
||||
},
|
||||
{
|
||||
"descriptors": [
|
||||
{
|
||||
"defaultMessage": "Unfollow",
|
||||
"id": "confirmations.unfollow.confirm"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Block",
|
||||
"id": "confirmations.block.confirm"
|
||||
@ -280,6 +297,10 @@
|
||||
"defaultMessage": "Hide entire domain",
|
||||
"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}?",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"emoji_button.food": "Food & Drink",
|
||||
|
@ -55,6 +55,8 @@
|
||||
"confirmations.domain_block.message": "آیا جدی جدی میخواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقتها مسدودکردن یا بیصداکردن چند حساب کاربری خاص کافی است و توصیه میشود.",
|
||||
"confirmations.mute.confirm": "بیصدا کن",
|
||||
"confirmations.mute.message": "آیا واقعاً میخواهید {name} را بیصدا کنید؟",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"emoji_button.activity": "فعالیت",
|
||||
"emoji_button.flags": "پرچمها",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"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.mute.confirm": "Masquer",
|
||||
"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.flags": "Drapeaux",
|
||||
"emoji_button.food": "Boire et manger",
|
||||
|
@ -55,6 +55,8 @@
|
||||
"confirmations.domain_block.message": "באמת באמת לחסום את כל קהילת {domain}? ברב המקרים השתקות נבחרות של מספר משתמשים מסויימים צריכה להספיק.",
|
||||
"confirmations.mute.confirm": "להשתיק",
|
||||
"confirmations.mute.message": "להשתיק את {name}?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"emoji_button.activity": "פעילות",
|
||||
"emoji_button.flags": "דגלים",
|
||||
"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.mute.confirm": "Utišaj",
|
||||
"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.flags": "Zastave",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"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.mute.confirm": "Bisukan",
|
||||
"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.flags": "Bendera",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"emoji_button.food": "Food & Drink",
|
||||
|
@ -55,6 +55,8 @@
|
||||
"confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。",
|
||||
"confirmations.mute.confirm": "ミュート",
|
||||
"confirmations.mute.message": "本当に{name}をミュートしますか?",
|
||||
"confirmations.unfollow.confirm": "フォロー解除",
|
||||
"confirmations.unfollow.message": "本当に{name}のフォローを解除しますか?",
|
||||
"emoji_button.activity": "活動",
|
||||
"emoji_button.flags": "国旗",
|
||||
"emoji_button.food": "食べ物",
|
||||
@ -149,7 +151,7 @@
|
||||
"report.target": "問題のユーザー",
|
||||
"search.placeholder": "検索",
|
||||
"search_results.total": "{count, number}件の結果",
|
||||
"standalone.public_title": "A look inside...",
|
||||
"standalone.public_title": "連合タイムライン",
|
||||
"status.cannot_reblog": "この投稿はブーストできません",
|
||||
"status.delete": "削除",
|
||||
"status.favourite": "お気に入り",
|
||||
|
@ -55,6 +55,8 @@
|
||||
"confirmations.domain_block.message": "정말로 {domain} 전체를 숨기시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다.",
|
||||
"confirmations.mute.confirm": "뮤트",
|
||||
"confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"emoji_button.activity": "활동",
|
||||
"emoji_button.flags": "국기",
|
||||
"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.mute.confirm": "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.flags": "Vlaggen",
|
||||
"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.mute.confirm": "Demp",
|
||||
"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.flags": "Flagg",
|
||||
"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.mute.confirm": "Metre en silenci",
|
||||
"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.flags": "Drapèus",
|
||||
"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.mute.confirm": "Wycisz",
|
||||
"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.flags": "Flagi",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"emoji_button.food": "Food & Drink",
|
||||
|
@ -55,6 +55,8 @@
|
||||
"confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.",
|
||||
"confirmations.mute.confirm": "Заглушить",
|
||||
"confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"emoji_button.activity": "Занятия",
|
||||
"emoji_button.flags": "Флаги",
|
||||
"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.mute.confirm": "Mute",
|
||||
"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.flags": "Flags",
|
||||
"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.mute.confirm": "Sessize al",
|
||||
"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.flags": "Bayraklar",
|
||||
"emoji_button.food": "Yiyecek ve İçecek",
|
||||
|
@ -55,6 +55,8 @@
|
||||
"confirmations.domain_block.message": "Ви точно, точно впевнені, що хочете заблокувати весь домен {domain}? У більшості випадків для нормальної роботи краще заблокувати/заглушити лише деяких користувачів.",
|
||||
"confirmations.mute.confirm": "Заглушити",
|
||||
"confirmations.mute.message": "Ви впевнені, що хочете заглушити {name}?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"emoji_button.activity": "Заняття",
|
||||
"emoji_button.flags": "Прапори",
|
||||
"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.mute.confirm": "静音",
|
||||
"confirmations.mute.message": "想好了,真的要静音 {name}?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"emoji_button.activity": "活动",
|
||||
"emoji_button.flags": "旗帜",
|
||||
"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.mute.confirm": "靜音",
|
||||
"confirmations.mute.message": "你確定要將{name}靜音嗎?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"emoji_button.activity": "活動",
|
||||
"emoji_button.flags": "旗幟",
|
||||
"emoji_button.food": "飲飲食食",
|
||||
|
@ -55,6 +55,8 @@
|
||||
"confirmations.domain_block.message": "你真的真的確定要封鎖整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
|
||||
"confirmations.mute.confirm": "消音",
|
||||
"confirmations.mute.message": "你確定要消音 {name} ?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"emoji_button.activity": "活動",
|
||||
"emoji_button.flags": "旗幟",
|
||||
"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';
|
||||
|
||||
const perf = require('./performance');
|
||||
|
||||
function 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) {
|
||||
const { pathname, search, hash } = window.location;
|
||||
@ -23,9 +25,6 @@ function main() {
|
||||
ReactDOM.render(<Mastodon {...props} />, mountNode);
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// 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();
|
||||
WebPushSubscription.register();
|
||||
}
|
||||
|
@ -1 +1,10 @@
|
||||
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',
|
||||
});
|
||||
|
||||
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 actions = notification.actions.filter(act => act.action !== action.action);
|
||||
|
||||
@ -75,7 +93,7 @@ const handleNotificationClick = (event) => {
|
||||
}
|
||||
} else {
|
||||
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 ready from '../mastodon/ready';
|
||||
|
||||
require.context('../images/', true);
|
||||
|
||||
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');
|
||||
|
||||
if (mountNode !== null) {
|
||||
@ -16,6 +15,7 @@ function loaded() {
|
||||
}
|
||||
|
||||
function main() {
|
||||
const ready = require('../mastodon/ready').default;
|
||||
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';
|
||||
|
||||
loadPolyfills().then(main).catch(e => {
|
||||
loadPolyfills().then(() => {
|
||||
require('../mastodon/main').default();
|
||||
}).catch(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 { processBio } from '../glitch/util/bio_metadata';
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
function main() {
|
||||
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;
|
||||
|
||||
const { localeData } = getLocale();
|
||||
localeData.forEach(IntlRelativeFormat.__addLocaleData);
|
||||
|
||||
function loaded() {
|
||||
ready(() => {
|
||||
const locale = document.documentElement.lang;
|
||||
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
@ -36,10 +38,7 @@ function loaded() {
|
||||
const datetime = new Date(content.getAttribute('datetime'));
|
||||
content.textContent = relativeFormat.format(datetime);;
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
ready(loaded);
|
||||
});
|
||||
|
||||
delegate(document, '.video-player video', 'click', ({ target }) => {
|
||||
if (target.paused) {
|
||||
|
@ -253,7 +253,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.report-status {
|
||||
.report-status,
|
||||
.account-status {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@ -263,7 +264,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.report-status__actions {
|
||||
.report-status__actions,
|
||||
.account-status__actions {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -275,3 +277,42 @@
|
||||
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
|
||||
|
||||
class AtomSerializer
|
||||
class Ostatus::AtomSerializer
|
||||
include RoutingHelper
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
@ -19,6 +19,7 @@ class UserSettingsDecorator
|
||||
user.settings['interactions'] = merged_interactions
|
||||
user.settings['default_privacy'] = default_privacy_preference
|
||||
user.settings['default_sensitive'] = default_sensitive_preference
|
||||
user.settings['unfollow_modal'] = unfollow_modal_preference
|
||||
user.settings['boost_modal'] = boost_modal_preference
|
||||
user.settings['delete_modal'] = delete_modal_preference
|
||||
user.settings['auto_play_gif'] = auto_play_gif_preference
|
||||
@ -42,6 +43,10 @@ class UserSettingsDecorator
|
||||
boolean_cast_setting 'setting_default_sensitive'
|
||||
end
|
||||
|
||||
def unfollow_modal_preference
|
||||
boolean_cast_setting 'setting_unfollow_modal'
|
||||
end
|
||||
|
||||
def boost_modal_preference
|
||||
boolean_cast_setting 'setting_boost_modal'
|
||||
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
|
||||
end
|
||||
|
||||
def setting_unfollow_modal
|
||||
settings.unfollow_modal
|
||||
end
|
||||
|
||||
def setting_boost_modal
|
||||
settings.boost_modal
|
||||
end
|
||||
|
@ -12,6 +12,9 @@
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
require 'webpush'
|
||||
require_relative '../../models/setting'
|
||||
|
||||
class Web::PushSubscription < ApplicationRecord
|
||||
include RoutingHelper
|
||||
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
|
||||
|
||||
# TODO: Make sure that the payload does not exceed 4KB - Webpush::PayloadTooLarge
|
||||
# TODO: Queue the requests - Webpush::TooManyRequests
|
||||
Webpush.payload_send(
|
||||
message: JSON.generate(
|
||||
title: title,
|
||||
@ -59,7 +61,7 @@ class Web::PushSubscription < ApplicationRecord
|
||||
p256dh: key_p256dh,
|
||||
auth: key_auth,
|
||||
vapid: {
|
||||
# subject: "mailto:#{Setting.site_contact_email}",
|
||||
subject: "mailto:#{Setting.site_contact_email}",
|
||||
private_key: Rails.configuration.x.vapid_private_key,
|
||||
public_key: Rails.configuration.x.vapid_public_key,
|
||||
},
|
||||
@ -166,7 +168,7 @@ class Web::PushSubscription < ApplicationRecord
|
||||
p256dh: key_p256dh,
|
||||
auth: key_auth,
|
||||
vapid: {
|
||||
# subject: "mailto:#{Setting.site_contact_email}",
|
||||
subject: "mailto:#{Setting.site_contact_email}",
|
||||
private_key: Rails.configuration.x.vapid_private_key,
|
||||
public_key: Rails.configuration.x.vapid_public_key,
|
||||
},
|
||||
|
@ -15,6 +15,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||
|
||||
if object.current_account
|
||||
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[:delete_modal] = object.current_account.user.setting_delete_modal
|
||||
store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif
|
||||
|
@ -10,6 +10,6 @@ class AuthorizeFollowService < BaseService
|
||||
private
|
||||
|
||||
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
|
||||
|
@ -18,6 +18,6 @@ class BlockService < BaseService
|
||||
private
|
||||
|
||||
def build_xml(block)
|
||||
AtomSerializer.render(AtomSerializer.new.block_salmon(block))
|
||||
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.block_salmon(block))
|
||||
end
|
||||
end
|
||||
|
@ -2,6 +2,6 @@
|
||||
|
||||
module StreamEntryRenderer
|
||||
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
|
||||
|
@ -28,6 +28,6 @@ class FavouriteService < BaseService
|
||||
private
|
||||
|
||||
def build_xml(favourite)
|
||||
AtomSerializer.render(AtomSerializer.new.favourite_salmon(favourite))
|
||||
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.favourite_salmon(favourite))
|
||||
end
|
||||
end
|
||||
|
@ -57,10 +57,10 @@ class FollowService < BaseService
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def build_follow_xml(follow)
|
||||
AtomSerializer.render(AtomSerializer.new.follow_salmon(follow))
|
||||
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.follow_salmon(follow))
|
||||
end
|
||||
end
|
||||
|
@ -65,7 +65,11 @@ class NotifyService < BaseService
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def send_email
|
||||
|
@ -16,274 +16,14 @@ class ProcessFeedService < BaseService
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
class ProcessEntry
|
||||
def call(xml, account)
|
||||
@account = account
|
||||
@xml = xml
|
||||
|
||||
return if skip_unsupported_type?
|
||||
|
||||
case verb
|
||||
when :post, :share
|
||||
return create_status
|
||||
when :delete
|
||||
return delete_status
|
||||
end
|
||||
def process_entry(xml, account)
|
||||
activity = Ostatus::Activity::General.new(xml, account)
|
||||
activity.specialize&.perform if activity.status?
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.debug "Nothing was saved for #{id} because: #{e}"
|
||||
nil
|
||||
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
|
||||
|
@ -10,6 +10,6 @@ class RejectFollowService < BaseService
|
||||
private
|
||||
|
||||
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
|
||||
|
@ -11,6 +11,6 @@ class UnblockService < BaseService
|
||||
private
|
||||
|
||||
def build_xml(block)
|
||||
AtomSerializer.render(AtomSerializer.new.unblock_salmon(block))
|
||||
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.unblock_salmon(block))
|
||||
end
|
||||
end
|
||||
|
@ -13,6 +13,6 @@ class UnfavouriteService < BaseService
|
||||
private
|
||||
|
||||
def build_xml(favourite)
|
||||
AtomSerializer.render(AtomSerializer.new.unfavourite_salmon(favourite))
|
||||
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.unfavourite_salmon(favourite))
|
||||
end
|
||||
end
|
||||
|
@ -14,6 +14,6 @@ class UnfollowService < BaseService
|
||||
private
|
||||
|
||||
def build_xml(follow)
|
||||
AtomSerializer.render(AtomSerializer.new.unfollow_salmon(follow))
|
||||
Ostatus::AtomSerializer.render(Ostatus::AtomSerializer.new.unfollow_salmon(follow))
|
||||
end
|
||||
end
|
||||
|
@ -18,13 +18,13 @@
|
||||
.landing-page
|
||||
.header-wrapper
|
||||
.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
|
||||
.container.links
|
||||
.brand
|
||||
= link_to root_url do
|
||||
= image_tag asset_pack_path('logo.svg')
|
||||
= image_tag asset_pack_path('logo.svg'), alt: '', role: 'presentation'
|
||||
Mastodon
|
||||
|
||||
%ul.nav
|
||||
@ -38,9 +38,9 @@
|
||||
|
||||
.container.hero
|
||||
.floats
|
||||
= image_tag asset_pack_path('cloud2.png'), class: 'float-1'
|
||||
= image_tag asset_pack_path('cloud3.png'), class: 'float-2'
|
||||
= image_tag asset_pack_path('cloud4.png'), class: 'float-3'
|
||||
= image_tag asset_pack_path('cloud2.png'), alt: '', role: 'presentation', class: 'float-1'
|
||||
= image_tag asset_pack_path('cloud3.png'), alt: '', role: 'presentation', class: 'float-2'
|
||||
= image_tag asset_pack_path('cloud4.png'), alt: '', role: 'presentation', class: 'float-3'
|
||||
.heading
|
||||
%h1
|
||||
= @instance_presenter.site_title
|
||||
@ -54,7 +54,7 @@
|
||||
%p= t('about.closed_registrations')
|
||||
- else
|
||||
= @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
|
||||
.container
|
||||
@ -69,7 +69,7 @@
|
||||
.about-mastodon
|
||||
%h3= t 'about.what_is_mastodon'
|
||||
%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'
|
||||
.footer-links
|
||||
.container
|
||||
|
@ -53,11 +53,11 @@
|
||||
%td= @account.followers_count
|
||||
%tr
|
||||
%th= t('admin.accounts.statuses')
|
||||
%td= @account.statuses_count
|
||||
%td= link_to @account.statuses_count, admin_account_statuses_path(@account.id)
|
||||
%tr
|
||||
%th= t('admin.accounts.media_attachments')
|
||||
%td
|
||||
= @account.media_attachments.count
|
||||
= link_to @account.media_attachments.count, admin_account_statuses_path(@account.id, { media: true })
|
||||
= surround '(', ')' do
|
||||
= number_to_human_size @account.media_attachments.sum('file_file_size')
|
||||
%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
|
||||
= t('admin.reports.report', id: @report.id)
|
||||
|
||||
@ -19,15 +22,26 @@
|
||||
- unless @report.statuses.empty?
|
||||
%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-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
|
||||
.entry= render 'stream_entries/simple_status', status: status
|
||||
.report-status__actions
|
||||
- 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'
|
||||
= 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'
|
||||
|
||||
%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
|
||||
|
||||
.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_delete_modal, as: :boolean, wrapper: :with_label
|
||||
|
||||
|
@ -22,7 +22,7 @@ class Pubsubhubbub::DistributionWorker
|
||||
def distribute_public!(stream_entries)
|
||||
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|
|
||||
[subscription.id, @payload]
|
||||
@ -32,7 +32,7 @@ class Pubsubhubbub::DistributionWorker
|
||||
def distribute_hidden!(stream_entries)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def perform(recipient_id, notification_id)
|
||||
recipient = Account.find(recipient_id)
|
||||
def perform(session_activation_id, notification_id)
|
||||
session_activation = SessionActivation.find(session_activation_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
|
||||
session.web_push_subscription.push(notification)
|
||||
rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription
|
||||
session_activation.web_push_subscription.push(notification)
|
||||
rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription => e
|
||||
# Subscription expiration is not currently implemented in any browser
|
||||
session.web_push_subscription.destroy!
|
||||
session.update!(web_push_subscription: nil)
|
||||
rescue Webpush::PayloadTooLarge => e
|
||||
Rails.logger.error(e)
|
||||
end
|
||||
session_activation.web_push_subscription.destroy!
|
||||
session_activation.update!(web_push_subscription: nil)
|
||||
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -185,6 +185,21 @@ en:
|
||||
desc_html: Display public timeline on landing page
|
||||
title: Timeline preview
|
||||
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:
|
||||
callback_url: Callback URL
|
||||
confirmed: Confirmed
|
||||
|
@ -1,15 +1,28 @@
|
||||
---
|
||||
ja:
|
||||
about:
|
||||
about_mastodon: Mastodon は<em>自由でオープンソース</em>なソーシャルネットワークです。商用プラットフォームの代替となる<em>分散型</em>を採用し、あなたのやりとりが一つの会社によって独占されるのを防ぎます。信頼できるインスタンスを選択してください — どのインスタンスを選んでも、誰とでもやりとりすることができます。 だれでも自分の Mastodon インスタンスを作ることができ、シームレスに<em>ソーシャルネットワーク</em>に参加できます。
|
||||
about_mastodon_html: Mastodon は、オープンなウェブプロトコルを採用した、自由でオープンソースなソーシャルネットワークです。電子メールのような分散型の仕組みを採っています。
|
||||
about_this: このインスタンスについて
|
||||
business_email: 'ビジネスメールアドレス:'
|
||||
closed_registrations: 現在このインスタンスでの新規登録は受け付けていません。
|
||||
closed_registrations: 現在このインスタンスでの新規登録は受け付けていません。しかし、他のインスタンスにアカウントを作成しても全く同じネットワークに参加することができます。
|
||||
contact: 連絡先
|
||||
description_headline: "%{domain} とは?"
|
||||
domain_count_after: 個のインスタンス
|
||||
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: 参加する
|
||||
hosted_on: Mastodon hosted on %{domain}
|
||||
learn_more: もっと詳しく
|
||||
links: リンク
|
||||
other_instances: 他のインスタンス
|
||||
source_code: ソースコード
|
||||
@ -19,6 +32,7 @@ ja:
|
||||
user_count_after: 人
|
||||
user_count_before: ユーザー数
|
||||
version: バージョン
|
||||
what_is_mastodon: Mastodon とは?
|
||||
accounts:
|
||||
follow: フォロー
|
||||
followers: フォロワー
|
||||
@ -171,6 +185,21 @@ ja:
|
||||
desc_html: ランディングページに公開タイムラインを表示します
|
||||
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:
|
||||
callback_url: コールバックURL
|
||||
confirmed: 確認済み
|
||||
@ -190,9 +219,10 @@ ja:
|
||||
applications:
|
||||
invalid_url: URLが無効です
|
||||
auth:
|
||||
agreement_html: 登録すると <a href="%{rules_path}">利用規約</a> と <a href="%{terms_path}">プライバシーポリシー</a> に同意したことになります。
|
||||
change_password: セキュリティ
|
||||
delete_account: アカウントの削除
|
||||
delete_account_html: アカウントを削除したい場合、<a href="%{path}">こちら</a>から手続きが行えます。削除前には確認画面があります。
|
||||
delete_account_html: アカウントを削除したい場合、<a href="%{path}">こちら</a> から手続きが行えます。削除する前に、確認画面があります。
|
||||
didnt_get_confirmation: 確認メールを受信できませんか?
|
||||
forgot_password: パスワードをお忘れですか?
|
||||
login: ログイン
|
||||
|
@ -42,6 +42,7 @@ en:
|
||||
setting_default_sensitive: Always mark media as sensitive
|
||||
setting_delete_modal: Show confirmation dialog before deleting a toot
|
||||
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
|
||||
severity: Severity
|
||||
type: Import type
|
||||
|
@ -8,6 +8,8 @@ ja:
|
||||
header: 2MBまでのPNGやGIF、JPGが利用可能です。 700x335pxまで縮小されます。
|
||||
locked: フォロワーを手動で承認する必要があります。
|
||||
note: あと<span class="note-counter">%{count}</span>文字入力できます。
|
||||
setting_noindex: 公開プロフィールおよび各投稿ページに影響します
|
||||
|
||||
imports:
|
||||
data: 他の Mastodon インスタンスからエクスポートしたCSVファイルを選択して下さい
|
||||
sessions:
|
||||
@ -37,6 +39,7 @@ ja:
|
||||
setting_default_sensitive: メディアを常に閲覧注意としてマークする
|
||||
setting_delete_modal: トゥートを削除する前に確認ダイアログを表示する
|
||||
setting_system_font_ui: システムのデフォルトフォントを使う
|
||||
setting_noindex: 検索エンジンによるインデックスを拒否する
|
||||
severity: 重大性
|
||||
type: インポートする項目
|
||||
username: ユーザー名
|
||||
|
@ -89,7 +89,7 @@ Rails.application.routes.draw do
|
||||
resources :instances, only: [:index]
|
||||
|
||||
resources :reports, only: [:index, :show, :update] do
|
||||
resources :reported_statuses, only: [:update, :destroy]
|
||||
resources :reported_statuses, only: [:create, :update, :destroy]
|
||||
end
|
||||
|
||||
resources :accounts, only: [:index, :show] do
|
||||
@ -103,6 +103,7 @@ Rails.application.routes.draw do
|
||||
resource :silence, only: [:create, :destroy]
|
||||
resource :suspension, only: [:create, :destroy]
|
||||
resource :confirmation, only: [:create]
|
||||
resources :statuses, only: [:index, :create, :update, :destroy]
|
||||
end
|
||||
|
||||
resources :users, only: [] do
|
||||
|
@ -50,7 +50,7 @@
|
||||
"es6-symbol": "^3.1.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"express": "^4.15.2",
|
||||
"extract-text-webpack-plugin": "^3.0.0",
|
||||
"extract-text-webpack-plugin": "^2.1.2",
|
||||
"file-loader": "^0.11.2",
|
||||
"font-awesome": "^4.7.0",
|
||||
"glob": "^7.1.1",
|
||||
@ -112,7 +112,7 @@
|
||||
"tiny-queue": "^0.2.1",
|
||||
"uuid": "^3.1.0",
|
||||
"uws": "^8.14.0",
|
||||
"webpack": "^3.2.0",
|
||||
"webpack": "^3.0.0",
|
||||
"webpack-bundle-analyzer": "^2.8.2",
|
||||
"webpack-manifest-plugin": "^1.1.2",
|
||||
"webpack-merge": "^4.1.0",
|
||||
|
@ -11,6 +11,42 @@ describe Admin::ReportedStatusesController do
|
||||
sign_in user, scope: :user
|
||||
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
|
||||
subject do
|
||||
-> { 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)
|
||||
|
||||
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).
|
||||
to have_received(:perform_async).with(status.id)
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
it 'returns delete modal setting' do
|
||||
user = Fabricate(:user)
|
||||
|
@ -167,6 +167,46 @@ XML
|
||||
expect(created_statuses.first.reblog.text).to eq 'Overwatch rocks'
|
||||
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
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
|
||||
dependencies:
|
||||
@ -1657,7 +1657,7 @@ cheerio@^0.22.0:
|
||||
lodash.reject "^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"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
|
||||
dependencies:
|
||||
@ -2868,12 +2868,12 @@ extglob@^0.3.1:
|
||||
dependencies:
|
||||
is-extglob "^1.0.0"
|
||||
|
||||
extract-text-webpack-plugin@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612"
|
||||
extract-text-webpack-plugin@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-2.1.2.tgz#756ef4efa8155c3681833fbc34da53b941746d6c"
|
||||
dependencies:
|
||||
async "^2.4.1"
|
||||
loader-utils "^1.1.0"
|
||||
async "^2.1.2"
|
||||
loader-utils "^1.0.2"
|
||||
schema-utils "^0.3.0"
|
||||
webpack-sources "^1.0.1"
|
||||
|
||||
@ -7328,6 +7328,14 @@ watchpack@^1.3.1:
|
||||
chokidar "^1.4.3"
|
||||
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:
|
||||
version "1.7.2"
|
||||
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-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"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.2.0.tgz#8b0cae0e1a9fd76bfbf0eab61a8c2ada848c312f"
|
||||
dependencies:
|
||||
@ -7452,6 +7460,33 @@ webpack-sources@^1.0.1:
|
||||
webpack-sources "^1.0.1"
|
||||
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:
|
||||
version "0.6.5"
|
||||
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36"
|
||||
|
Loading…
Reference in New Issue
Block a user