Merge remote-tracking branch 'tootsuite/master'
This commit is contained in:
commit
d083f7741a
|
@ -13,6 +13,7 @@ module Admin
|
|||
closed_registrations_message
|
||||
open_deletion
|
||||
timeline_preview
|
||||
show_staff_badge
|
||||
bootstrap_timeline_accounts
|
||||
thumbnail
|
||||
).freeze
|
||||
|
@ -21,6 +22,7 @@ module Admin
|
|||
open_registrations
|
||||
open_deletion
|
||||
timeline_preview
|
||||
show_staff_badge
|
||||
).freeze
|
||||
|
||||
UPLOAD_SETTINGS = %w(
|
||||
|
|
|
@ -7,6 +7,7 @@ import Motion from '../../ui/util/optional_motion';
|
|||
import spring from 'react-motion/lib/spring';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { autoPlayGif, me } from '../../../initial_state';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
|
@ -102,6 +103,10 @@ export default class Header extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
if (account.get('moved')) {
|
||||
actionBtn = '';
|
||||
}
|
||||
|
||||
if (account.get('locked')) {
|
||||
lockedIcon = <i className='fa fa-lock' />;
|
||||
}
|
||||
|
@ -110,7 +115,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||
|
||||
return (
|
||||
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
|
||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${account.get('header')})` }}>
|
||||
<div>
|
||||
<Avatar account={account} />
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import InnerHeader from '../../account/components/header';
|
|||
import ActionBar from '../../account/components/action_bar';
|
||||
import MissingIndicator from '../../../components/missing_indicator';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import MovedNote from './moved_note';
|
||||
|
||||
export default class Header extends ImmutablePureComponent {
|
||||
|
||||
|
@ -68,6 +69,8 @@ export default class Header extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='account-timeline__header'>
|
||||
{account.get('moved') && <MovedNote from={account} to={account.get('moved')} />}
|
||||
|
||||
<InnerHeader
|
||||
account={account}
|
||||
onFollow={this.handleFollow}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import AvatarOverlay from '../../../components/avatar_overlay';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
|
||||
export default class MovedNote extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
from: ImmutablePropTypes.map.isRequired,
|
||||
to: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
handleAccountClick = e => {
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/accounts/${this.props.to.get('id')}`);
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { from, to } = this.props;
|
||||
const displayNameHtml = { __html: from.get('display_name_html') };
|
||||
|
||||
return (
|
||||
<div className='account__moved-note'>
|
||||
<div className='account__moved-note__message'>
|
||||
<div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div>
|
||||
<FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <strong dangerouslySetInnerHTML={displayNameHtml} /> }} />
|
||||
</div>
|
||||
|
||||
<a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
||||
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
|
||||
<DisplayName account={to} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -59,6 +59,11 @@ const normalizeAccount = (state, account) => {
|
|||
account.display_name_html = emojify(escapeTextContentForBrowser(displayName));
|
||||
account.note_emojified = emojify(account.note);
|
||||
|
||||
if (account.moved) {
|
||||
state = normalizeAccount(state, account.moved);
|
||||
account.moved = account.moved.id;
|
||||
}
|
||||
|
||||
return state.set(account.id, fromJS(account));
|
||||
};
|
||||
|
||||
|
|
|
@ -4,14 +4,18 @@ import { List as ImmutableList } from 'immutable';
|
|||
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
|
||||
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
|
||||
const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null);
|
||||
const getAccountMoved = (state, id) => state.getIn(['accounts', state.getIn(['accounts', id, 'moved'])]);
|
||||
|
||||
export const makeGetAccount = () => {
|
||||
return createSelector([getAccountBase, getAccountCounters, getAccountRelationship], (base, counters, relationship) => {
|
||||
return createSelector([getAccountBase, getAccountCounters, getAccountRelationship, getAccountMoved], (base, counters, relationship, moved) => {
|
||||
if (base === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return base.merge(counters).set('relationship', relationship);
|
||||
return base.merge(counters).withMutations(map => {
|
||||
map.set('relationship', relationship);
|
||||
map.set('moved', moved);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -917,6 +917,18 @@
|
|||
background-position: center;
|
||||
position: relative;
|
||||
|
||||
&.inactive {
|
||||
opacity: 0.5;
|
||||
|
||||
.account__header__avatar {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.account__header__username {
|
||||
color: $ui-primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
& > div {
|
||||
background: rgba(lighten($ui-base-color, 4%), 0.9);
|
||||
padding: 20px 10px;
|
||||
|
@ -4375,3 +4387,40 @@ noscript {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account__moved-note {
|
||||
padding: 14px 10px;
|
||||
padding-bottom: 16px;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border-top: 1px solid lighten($ui-base-color, 8%);
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
&__message {
|
||||
position: relative;
|
||||
margin-left: 58px;
|
||||
color: $ui-base-lighter-color;
|
||||
padding: 8px 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 4px;
|
||||
font-size: 14px;
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon-wrapper {
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.detailed-status__display-avatar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detailed-status__display-name {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,3 +34,66 @@
|
|||
.memoriam-strip {
|
||||
background: rgba($base-shadow-color, 0.7);
|
||||
}
|
||||
|
||||
.moved-strip {
|
||||
padding: 14px;
|
||||
border-radius: 4px;
|
||||
background: rgba(darken($ui-base-color, 7%), 0.8);
|
||||
color: $ui-secondary-color;
|
||||
font-weight: 400;
|
||||
margin-bottom: 20px;
|
||||
|
||||
strong,
|
||||
a {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
|
||||
&.mention {
|
||||
text-decoration: none;
|
||||
|
||||
span {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.fa {
|
||||
margin-right: 5px;
|
||||
color: $ui-primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__card {
|
||||
.detailed-status__display-avatar {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.detailed-status__display-name {
|
||||
margin-bottom: 0;
|
||||
text-decoration: none;
|
||||
|
||||
span {
|
||||
color: $ui-highlight-color;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
|||
{
|
||||
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
|
||||
'sensitive' => 'as:sensitive',
|
||||
'movedTo' => 'as:movedTo',
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'ostatus' => 'http://ostatus.org#',
|
||||
'atomUri' => 'ostatus:atomUri',
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
# followers_url :string default(""), not null
|
||||
# protocol :integer default("ostatus"), not null
|
||||
# memorial :boolean default(FALSE), not null
|
||||
# moved_to_account_id :integer
|
||||
#
|
||||
|
||||
class Account < ApplicationRecord
|
||||
|
@ -102,6 +103,9 @@ class Account < ApplicationRecord
|
|||
has_many :list_accounts, inverse_of: :account, dependent: :destroy
|
||||
has_many :lists, through: :list_accounts
|
||||
|
||||
# Account migrations
|
||||
belongs_to :moved_to_account, class_name: 'Account'
|
||||
|
||||
scope :remote, -> { where.not(domain: nil) }
|
||||
scope :local, -> { where(domain: nil) }
|
||||
scope :without_followers, -> { where(followers_count: 0) }
|
||||
|
@ -135,6 +139,10 @@ class Account < ApplicationRecord
|
|||
domain.nil?
|
||||
end
|
||||
|
||||
def moved?
|
||||
moved_to_account_id.present?
|
||||
end
|
||||
|
||||
def acct
|
||||
local? ? username : "#{username}@#{domain}"
|
||||
end
|
||||
|
|
|
@ -24,6 +24,8 @@ class Form::AdminSettings
|
|||
:open_deletion=,
|
||||
:timeline_preview,
|
||||
:timeline_preview=,
|
||||
:show_staff_badge,
|
||||
:show_staff_badge=,
|
||||
:bootstrap_timeline_accounts,
|
||||
:bootstrap_timeline_accounts=,
|
||||
to: Setting
|
||||
|
|
|
@ -10,6 +10,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
|
|||
|
||||
has_one :public_key, serializer: ActivityPub::PublicKeySerializer
|
||||
|
||||
attribute :moved_to, if: :moved?
|
||||
|
||||
class EndpointsSerializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
|
@ -25,6 +27,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
|
|||
has_one :icon, serializer: ActivityPub::ImageSerializer, if: :avatar_exists?
|
||||
has_one :image, serializer: ActivityPub::ImageSerializer, if: :header_exists?
|
||||
|
||||
delegate :moved?, to: :object
|
||||
|
||||
def id
|
||||
account_url(object)
|
||||
end
|
||||
|
@ -92,4 +96,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
|
|||
def manually_approves_followers
|
||||
object.locked
|
||||
end
|
||||
|
||||
def moved_to
|
||||
ActivityPub::TagManager.instance.uri_for(object.moved_to_account)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,10 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
:note, :url, :avatar, :avatar_static, :header, :header_static,
|
||||
:followers_count, :following_count, :statuses_count
|
||||
|
||||
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved?
|
||||
|
||||
delegate :moved?, to: :object
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
|
|
|
@ -74,6 +74,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
@account.statuses_count = outbox_total_items if outbox_total_items.present?
|
||||
@account.following_count = following_total_items if following_total_items.present?
|
||||
@account.followers_count = followers_total_items if followers_total_items.present?
|
||||
@account.moved_to_account = moved_account if @json['movedTo'].present?
|
||||
end
|
||||
|
||||
def after_protocol_change!
|
||||
|
@ -137,6 +138,12 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
@collections[type] = nil
|
||||
end
|
||||
|
||||
def moved_account
|
||||
account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
|
||||
account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true)
|
||||
account
|
||||
end
|
||||
|
||||
def skip_download?
|
||||
@account.suspended? || domain_block&.reject_media?
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- processed_bio = FrontmatterHandler.instance.process_bio Formatter.instance.simplified_format account
|
||||
.card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" }
|
||||
.card__illustration
|
||||
- unless account.memorial?
|
||||
- unless account.memorial? || account.moved?
|
||||
- if user_signed_in? && current_account.id != account.id && !current_account.requested?(account)
|
||||
.controls
|
||||
- if current_account.following?(account)
|
||||
|
@ -27,7 +27,7 @@
|
|||
%small
|
||||
%span @#{account.local_username_and_domain}
|
||||
= fa_icon('lock') if account.locked?
|
||||
|
||||
- if Setting.show_staff_badge
|
||||
- if account.user_admin?
|
||||
.roles
|
||||
.account-role.admin
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
- moved_to_account = account.moved_to_account
|
||||
|
||||
.moved-strip
|
||||
.moved-strip__message
|
||||
= fa_icon 'suitcase'
|
||||
= t('accounts.moved_html', name: content_tag(:strong, display_name(account), class: :emojify), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), TagManager.instance.url_for(moved_to_account), class: 'mention'))
|
||||
|
||||
.moved-strip__card
|
||||
= link_to TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do
|
||||
.detailed-status__display-avatar
|
||||
.account__avatar-overlay
|
||||
.account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" }
|
||||
.account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" }
|
||||
|
||||
%span.display-name
|
||||
%strong.emojify= display_name(moved_to_account)
|
||||
%span @#{moved_to_account.acct}
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
- if @account.memorial?
|
||||
.memoriam-strip= t('in_memoriam_html')
|
||||
- elsif @account.moved?
|
||||
= render partial: 'moved_strip', locals: { account: @account }
|
||||
- elsif show_landing_strip?
|
||||
= render partial: 'shared/landing_strip', locals: { account: @account }
|
||||
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
.fields-group
|
||||
= f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
|
||||
|
||||
.fields-group
|
||||
= f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html')
|
||||
|
||||
.fields-group
|
||||
= f.input :open_registrations, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.open.title'), hint: t('admin.settings.registrations.open.desc_html')
|
||||
|
||||
|
|
|
@ -517,8 +517,6 @@ de:
|
|||
pinned: Angehefteter Beitrag
|
||||
reblogged: teilte
|
||||
sensitive_content: Heikle Inhalte
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%d.%m.%Y %H:%M"
|
||||
|
|
|
@ -39,6 +39,7 @@ en:
|
|||
followers: Followers
|
||||
following: Following
|
||||
media: Media
|
||||
moved_html: "%{name} has moved to %{new_profile_link}:"
|
||||
nothing_here: There is nothing here!
|
||||
people_followed_by: People whom %{name} follows
|
||||
people_who_follow: People who follow %{name}
|
||||
|
@ -245,6 +246,9 @@ en:
|
|||
open:
|
||||
desc_html: Allow anyone to create an account
|
||||
title: Open registration
|
||||
show_staff_badge:
|
||||
desc_html: Show a staff badge on a user page
|
||||
title: Show staff badge
|
||||
site_description:
|
||||
desc_html: Introductory paragraph on the frontpage and in meta tags. You can use HTML tags, in particular <code><a></code> and <code><em></code>.
|
||||
title: Instance description
|
||||
|
|
|
@ -423,8 +423,6 @@ eo:
|
|||
sensitive_content: Tikla enhavo
|
||||
terms:
|
||||
title: "%{instance} Reguloj de servo kaj Politikaj pri privatecoj"
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
|
|
@ -587,8 +587,6 @@ fa:
|
|||
|
||||
<p>این نوشته اقتباسی است از <a href="https://github.com/discourse/discourse">سیاست رازداری Discourse</a>.</p>
|
||||
title: شرایط استفاده و سیاست رازداری %{instance}
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%d %b %Y, %H:%M"
|
||||
|
|
|
@ -587,8 +587,6 @@ fr:
|
|||
|
||||
<p>Originellement adapté à partir de la politique de confidentialité de <a href="https://github.com/discourse/discourse">Discourse</a>.</p>
|
||||
title: "%{instance} Conditions d’utilisations et politique de confidentialité"
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%d %b %Y, %H:%M"
|
||||
|
|
|
@ -48,6 +48,7 @@ ja:
|
|||
reserved_username: このユーザー名は予約されています。
|
||||
roles:
|
||||
admin: Admin
|
||||
moderator: Mod
|
||||
unfollow: フォロー解除
|
||||
admin:
|
||||
account_moderation_notes:
|
||||
|
@ -607,8 +608,6 @@ ja:
|
|||
|
||||
<p>オリジナルの出典 <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
|
||||
title: "%{instance} 利用規約・プライバシーポリシー"
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%Y年%m月%d日 %H:%M"
|
||||
|
|
|
@ -518,8 +518,6 @@ ko:
|
|||
sensitive_content: 민감한 컨텐츠
|
||||
terms:
|
||||
title: "%{instance} 이용약관과 개인정보 취급 방침"
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%Y년 %m월 %d일 %H:%M"
|
||||
|
|
|
@ -587,8 +587,6 @@ nl:
|
|||
|
||||
<p>Originally adapted from the <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p>
|
||||
title: "%{instance} Terms of Service and Privacy Policy"
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%d %B %Y om %H:%M"
|
||||
|
|
|
@ -678,8 +678,6 @@ oc:
|
|||
|
||||
<p>Prima adaptacion de la <a href="https://github.com/discourse/discourse">politica de confidencialitat de Discourse</a>.</p>
|
||||
title: Condicions d’utilizacion e politica de confidencialitat de %{instance}
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: Lo %d %b de %Y a %Ho%M
|
||||
|
|
|
@ -39,6 +39,7 @@ pl:
|
|||
followers: Śledzących
|
||||
following: Śledzi
|
||||
media: Zawartość multimedialna
|
||||
moved_html: "%{name} korzysta teraz z konta %{new_profile_link}:"
|
||||
nothing_here: Niczego tu nie ma!
|
||||
people_followed_by: Konta śledzone przez %{name}
|
||||
people_who_follow: Osoby, które śledzą konto %{name}
|
||||
|
@ -244,6 +245,9 @@ pl:
|
|||
open:
|
||||
desc_html: Pozwól każdemu na założenie konta
|
||||
title: Otwarta rejestracja
|
||||
show_staff_badge:
|
||||
desc_html: Pokazuj odznakę uprawnień na stronie profilu użytkownika
|
||||
title: Pokazuj odznakę administracji
|
||||
site_description:
|
||||
desc_html: Akapit wprowadzający, widoczny na stronie głównej i znacznikach meta. Możesz korzystać z tagów HTML, w szczególności <code><a></code> i <code><em></code>.
|
||||
title: Opis instancji
|
||||
|
@ -611,8 +615,6 @@ pl:
|
|||
|
||||
<p>Tekst bazuje na <a href="https://github.com/discourse/discourse">polityce prywatności Discourse</a>.</p>
|
||||
title: Zasady korzystania i polityka prywatności %{instance}
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
|
|
@ -607,8 +607,6 @@ pt-BR:
|
|||
|
||||
<p>Originalmente adaptado da <a href="https://github.com/discourse/discourse">política de privacidade do Discourse</a>.</p>
|
||||
title: "%{instance} Termos de Serviço e Política de Privacidade"
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
|
|
@ -521,8 +521,6 @@ ru:
|
|||
sensitive_content: Чувствительный контент
|
||||
terms:
|
||||
title: Условия обслуживания и политика конфиденциальности %{instance}
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
|
|
@ -587,8 +587,6 @@ sv:
|
|||
|
||||
<p>Ursprungligen anpassad från <a href="https://github.com/discourse/discourse">Discourse integritetspolicy</a>.</p>
|
||||
title: "%{instance} Användarvillkor och Sekretesspolicy"
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%b %d, %Y, %H:%M"
|
||||
|
|
|
@ -602,8 +602,6 @@ zh-CN:
|
|||
|
||||
<p>原文出自 <a href="https://github.com/discourse/discourse">Discourse 隐私权政策</a>。</p>
|
||||
title: "%{instance} 使用条款和隐私权政策"
|
||||
themes:
|
||||
default: Mastodon
|
||||
time:
|
||||
formats:
|
||||
default: "%Y年%-m月%d日 %H:%M"
|
||||
|
|
|
@ -17,6 +17,7 @@ defaults: &defaults
|
|||
closed_registrations_message: ''
|
||||
open_deletion: true
|
||||
timeline_preview: false
|
||||
show_staff_badge: true
|
||||
default_sensitive: false
|
||||
unfollow_modal: false
|
||||
boost_modal: false
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class AddMovedToAccountIdToAccounts < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
add_column :accounts, :moved_to_account_id, :bigint, null: true, default: nil
|
||||
add_foreign_key :accounts, :accounts, column: :moved_to_account_id, on_delete: :nullify
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20171116161857) do
|
||||
ActiveRecord::Schema.define(version: 20171118012443) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -72,6 +72,7 @@ ActiveRecord::Schema.define(version: 20171116161857) do
|
|||
t.string "followers_url", default: "", null: false
|
||||
t.integer "protocol", default: 0, null: false
|
||||
t.boolean "memorial", default: false, null: false
|
||||
t.bigint "moved_to_account_id"
|
||||
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
|
||||
t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower"
|
||||
t.index ["uri"], name: "index_accounts_on_uri"
|
||||
|
@ -497,6 +498,7 @@ ActiveRecord::Schema.define(version: 20171116161857) do
|
|||
add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
|
||||
add_foreign_key "account_moderation_notes", "accounts"
|
||||
add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
|
||||
add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify
|
||||
add_foreign_key "blocks", "accounts", column: "target_account_id", name: "fk_9571bfabc1", on_delete: :cascade
|
||||
add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
|
||||
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Settings::Extend do
|
||||
class User
|
||||
include Settings::Extend
|
||||
end
|
||||
|
||||
describe '#settings' do
|
||||
it 'sets @settings as an instance of Settings::ScopedSettings' do
|
||||
user = Fabricate(:user)
|
||||
expect(user.settings).to be_kind_of Settings::ScopedSettings
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,554 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe AccountInteractions do
|
||||
let(:account) { Fabricate(:account, username: 'account') }
|
||||
let(:account_id) { account.id }
|
||||
let(:account_ids) { [account_id] }
|
||||
let(:target_account) { Fabricate(:account, username: 'target') }
|
||||
let(:target_account_id) { target_account.id }
|
||||
let(:target_account_ids) { [target_account_id] }
|
||||
|
||||
describe '.following_map' do
|
||||
subject { Account.following_map(target_account_ids, account_id) }
|
||||
|
||||
context 'account with Follow' do
|
||||
it 'returns { target_account_id => true }' do
|
||||
Fabricate(:follow, account: account, target_account: target_account)
|
||||
is_expected.to eq(target_account_id => true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'account without Follow' do
|
||||
it 'returns {}' do
|
||||
is_expected.to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.followed_by_map' do
|
||||
subject { Account.followed_by_map(target_account_ids, account_id) }
|
||||
|
||||
context 'account with Follow' do
|
||||
it 'returns { target_account_id => true }' do
|
||||
Fabricate(:follow, account: target_account, target_account: account)
|
||||
is_expected.to eq(target_account_id => true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'account without Follow' do
|
||||
it 'returns {}' do
|
||||
is_expected.to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.blocking_map' do
|
||||
subject { Account.blocking_map(target_account_ids, account_id) }
|
||||
|
||||
context 'account with Block' do
|
||||
it 'returns { target_account_id => true }' do
|
||||
Fabricate(:block, account: account, target_account: target_account)
|
||||
is_expected.to eq(target_account_id => true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'account without Block' do
|
||||
it 'returns {}' do
|
||||
is_expected.to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.muting_map' do
|
||||
subject { Account.muting_map(target_account_ids, account_id) }
|
||||
|
||||
context 'account with Mute' do
|
||||
before do
|
||||
Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide)
|
||||
end
|
||||
|
||||
context 'if Mute#hide_notifications?' do
|
||||
let(:hide) { true }
|
||||
|
||||
it 'returns { target_account_id => { notifications: true } }' do
|
||||
is_expected.to eq(target_account_id => { notifications: true })
|
||||
end
|
||||
end
|
||||
|
||||
context 'unless Mute#hide_notifications?' do
|
||||
let(:hide) { false }
|
||||
|
||||
it 'returns { target_account_id => { notifications: false } }' do
|
||||
is_expected.to eq(target_account_id => { notifications: false })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'account without Mute' do
|
||||
it 'returns {}' do
|
||||
is_expected.to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#follow!' do
|
||||
it 'creates and returns Follow' do
|
||||
expect do
|
||||
expect(account.follow!(target_account)).to be_kind_of Follow
|
||||
end.to change { account.following.count }.by 1
|
||||
end
|
||||
end
|
||||
|
||||
describe '#block' do
|
||||
it 'creates and returns Block' do
|
||||
expect do
|
||||
expect(account.block!(target_account)).to be_kind_of Block
|
||||
end.to change { account.block_relationships.count }.by 1
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mute!' do
|
||||
context 'Mute does not exist yet' do
|
||||
context 'arg :notifications is nil' do
|
||||
let(:arg_notifications) { nil }
|
||||
|
||||
it 'creates Mute, and returns nil' do
|
||||
expect do
|
||||
expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
|
||||
end.to change { account.mute_relationships.count }.by 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'arg :notifications is false' do
|
||||
let(:arg_notifications) { false }
|
||||
|
||||
it 'creates Mute, and returns nil' do
|
||||
expect do
|
||||
expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
|
||||
end.to change { account.mute_relationships.count }.by 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'arg :notifications is true' do
|
||||
let(:arg_notifications) { true }
|
||||
|
||||
it 'creates Mute, and returns nil' do
|
||||
expect do
|
||||
expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
|
||||
end.to change { account.mute_relationships.count }.by 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Mute already exists' do
|
||||
before do
|
||||
account.mute_relationships << mute
|
||||
end
|
||||
|
||||
let(:mute) do
|
||||
Fabricate(:mute,
|
||||
account: account,
|
||||
target_account: target_account,
|
||||
hide_notifications: hide_notifications)
|
||||
end
|
||||
|
||||
context 'mute.hide_notifications is true' do
|
||||
let(:hide_notifications) { true }
|
||||
|
||||
context 'arg :notifications is nil' do
|
||||
let(:arg_notifications) { nil }
|
||||
|
||||
it 'returns nil without updating mute.hide_notifications' do
|
||||
expect do
|
||||
expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
|
||||
mute = account.mute_relationships.find_by(target_account: target_account)
|
||||
expect(mute.hide_notifications?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'arg :notifications is false' do
|
||||
let(:arg_notifications) { false }
|
||||
|
||||
it 'returns true, and updates mute.hide_notifications false' do
|
||||
expect do
|
||||
expect(account.mute!(target_account, notifications: arg_notifications)).to be true
|
||||
mute = account.mute_relationships.find_by(target_account: target_account)
|
||||
expect(mute.hide_notifications?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'arg :notifications is true' do
|
||||
let(:arg_notifications) { true }
|
||||
|
||||
it 'returns nil without updating mute.hide_notifications' do
|
||||
expect do
|
||||
expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
|
||||
mute = account.mute_relationships.find_by(target_account: target_account)
|
||||
expect(mute.hide_notifications?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'mute.hide_notifications is false' do
|
||||
let(:hide_notifications) { false }
|
||||
|
||||
context 'arg :notifications is nil' do
|
||||
let(:arg_notifications) { nil }
|
||||
|
||||
it 'returns true, and updates mute.hide_notifications true' do
|
||||
expect do
|
||||
expect(account.mute!(target_account, notifications: arg_notifications)).to be true
|
||||
mute = account.mute_relationships.find_by(target_account: target_account)
|
||||
expect(mute.hide_notifications?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'arg :notifications is false' do
|
||||
let(:arg_notifications) { false }
|
||||
|
||||
it 'returns nil without updating mute.hide_notifications' do
|
||||
expect do
|
||||
expect(account.mute!(target_account, notifications: arg_notifications)).to be nil
|
||||
mute = account.mute_relationships.find_by(target_account: target_account)
|
||||
expect(mute.hide_notifications?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'arg :notifications is true' do
|
||||
let(:arg_notifications) { true }
|
||||
|
||||
it 'returns true, and updates mute.hide_notifications true' do
|
||||
expect do
|
||||
expect(account.mute!(target_account, notifications: arg_notifications)).to be true
|
||||
mute = account.mute_relationships.find_by(target_account: target_account)
|
||||
expect(mute.hide_notifications?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mute_conversation!' do
|
||||
let(:conversation) { Fabricate(:conversation) }
|
||||
|
||||
subject { account.mute_conversation!(conversation) }
|
||||
|
||||
it 'creates and returns ConversationMute' do
|
||||
expect do
|
||||
is_expected.to be_kind_of ConversationMute
|
||||
end.to change { account.conversation_mutes.count }.by 1
|
||||
end
|
||||
end
|
||||
|
||||
describe '#block_domain!' do
|
||||
let(:domain_block) { Fabricate(:domain_block) }
|
||||
|
||||
subject { account.block_domain!(domain_block) }
|
||||
|
||||
it 'creates and returns AccountDomainBlock' do
|
||||
expect do
|
||||
is_expected.to be_kind_of AccountDomainBlock
|
||||
end.to change { account.domain_blocks.count }.by 1
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unfollow!' do
|
||||
subject { account.unfollow!(target_account) }
|
||||
|
||||
context 'following target_account' do
|
||||
it 'returns destroyed Follow' do
|
||||
account.active_relationships.create(target_account: target_account)
|
||||
is_expected.to be_kind_of Follow
|
||||
expect(subject).to be_destroyed
|
||||
end
|
||||
end
|
||||
|
||||
context 'not following target_account' do
|
||||
it 'returns nil' do
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unblock!' do
|
||||
subject { account.unblock!(target_account) }
|
||||
|
||||
context 'blocking target_account' do
|
||||
it 'returns destroyed Block' do
|
||||
account.block_relationships.create(target_account: target_account)
|
||||
is_expected.to be_kind_of Block
|
||||
expect(subject).to be_destroyed
|
||||
end
|
||||
end
|
||||
|
||||
context 'not blocking target_account' do
|
||||
it 'returns nil' do
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unmute!' do
|
||||
subject { account.unmute!(target_account) }
|
||||
|
||||
context 'muting target_account' do
|
||||
it 'returns destroyed Mute' do
|
||||
account.mute_relationships.create(target_account: target_account)
|
||||
is_expected.to be_kind_of Mute
|
||||
expect(subject).to be_destroyed
|
||||
end
|
||||
end
|
||||
|
||||
context 'not muting target_account' do
|
||||
it 'returns nil' do
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unmute_conversation!' do
|
||||
let(:conversation) { Fabricate(:conversation) }
|
||||
|
||||
subject { account.unmute_conversation!(conversation) }
|
||||
|
||||
context 'muting the conversation' do
|
||||
it 'returns destroyed ConversationMute' do
|
||||
account.conversation_mutes.create(conversation: conversation)
|
||||
is_expected.to be_kind_of ConversationMute
|
||||
expect(subject).to be_destroyed
|
||||
end
|
||||
end
|
||||
|
||||
context 'not muting the conversation' do
|
||||
it 'returns nil' do
|
||||
is_expected.to be nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unblock_domain!' do
|
||||
let(:domain) { 'example.com' }
|
||||
|
||||
subject { account.unblock_domain!(domain) }
|
||||
|
||||
context 'blocking the domain' do
|
||||
it 'returns destroyed AccountDomainBlock' do
|
||||
account_domain_block = Fabricate(:account_domain_block, domain: domain)
|
||||
account.domain_blocks << account_domain_block
|
||||
is_expected.to be_kind_of AccountDomainBlock
|
||||
expect(subject).to be_destroyed
|
||||
end
|
||||
end
|
||||
|
||||
context 'unblocking the domain' do
|
||||
it 'returns nil' do
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#following?' do
|
||||
subject { account.following?(target_account) }
|
||||
|
||||
context 'following target_account' do
|
||||
it 'returns true' do
|
||||
account.active_relationships.create(target_account: target_account)
|
||||
is_expected.to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'not following target_account' do
|
||||
it 'returns false' do
|
||||
is_expected.to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#blocking?' do
|
||||
subject { account.blocking?(target_account) }
|
||||
|
||||
context 'blocking target_account' do
|
||||
it 'returns true' do
|
||||
account.block_relationships.create(target_account: target_account)
|
||||
is_expected.to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'not blocking target_account' do
|
||||
it 'returns false' do
|
||||
is_expected.to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#domain_blocking?' do
|
||||
let(:domain) { 'example.com' }
|
||||
|
||||
subject { account.domain_blocking?(domain) }
|
||||
|
||||
context 'blocking the domain' do
|
||||
it' returns true' do
|
||||
account_domain_block = Fabricate(:account_domain_block, domain: domain)
|
||||
account.domain_blocks << account_domain_block
|
||||
is_expected.to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'not blocking the domain' do
|
||||
it 'returns false' do
|
||||
is_expected.to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#muting?' do
|
||||
subject { account.muting?(target_account) }
|
||||
|
||||
context 'muting target_account' do
|
||||
it 'returns true' do
|
||||
mute = Fabricate(:mute, account: account, target_account: target_account)
|
||||
account.mute_relationships << mute
|
||||
is_expected.to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'not muting target_account' do
|
||||
it 'returns false' do
|
||||
is_expected.to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#muting_conversation?' do
|
||||
let(:conversation) { Fabricate(:conversation) }
|
||||
|
||||
subject { account.muting_conversation?(conversation) }
|
||||
|
||||
context 'muting the conversation' do
|
||||
it 'returns true' do
|
||||
account.conversation_mutes.create(conversation: conversation)
|
||||
is_expected.to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'not muting the conversation' do
|
||||
it 'returns false' do
|
||||
is_expected.to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#muting_notifications?' do
|
||||
before do
|
||||
mute = Fabricate(:mute, target_account: target_account, account: account, hide_notifications: hide)
|
||||
account.mute_relationships << mute
|
||||
end
|
||||
|
||||
subject { account.muting_notifications?(target_account) }
|
||||
|
||||
context 'muting notifications of target_account' do
|
||||
let(:hide) { true }
|
||||
|
||||
it 'returns true' do
|
||||
is_expected.to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'not muting notifications of target_account' do
|
||||
let(:hide) { false }
|
||||
|
||||
it 'returns false' do
|
||||
is_expected.to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#requested?' do
|
||||
subject { account.requested?(target_account) }
|
||||
|
||||
context 'requested by target_account' do
|
||||
it 'returns true' do
|
||||
Fabricate(:follow_request, account: account, target_account: target_account)
|
||||
is_expected.to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'not requested by target_account' do
|
||||
it 'returns false' do
|
||||
is_expected.to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#favourited?' do
|
||||
let(:status) { Fabricate(:status, account: account, favourites: favourites) }
|
||||
|
||||
subject { account.favourited?(status) }
|
||||
|
||||
context 'favorited' do
|
||||
let(:favourites) { [Fabricate(:favourite, account: account)] }
|
||||
|
||||
it 'returns true' do
|
||||
is_expected.to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'not favorited' do
|
||||
let(:favourites) { [] }
|
||||
|
||||
it 'returns false' do
|
||||
is_expected.to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reblogged?' do
|
||||
let(:status) { Fabricate(:status, account: account, reblogs: reblogs) }
|
||||
|
||||
subject { account.reblogged?(status) }
|
||||
|
||||
context 'reblogged' do
|
||||
let(:reblogs) { [Fabricate(:status, account: account)] }
|
||||
|
||||
it 'returns true' do
|
||||
is_expected.to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'not reblogged' do
|
||||
let(:reblogs) { [] }
|
||||
|
||||
it 'returns false' do
|
||||
is_expected.to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#pinned?' do
|
||||
let(:status) { Fabricate(:status, account: account) }
|
||||
|
||||
subject { account.pinned?(status) }
|
||||
|
||||
context 'pinned' do
|
||||
it 'returns true' do
|
||||
Fabricate(:status_pin, account: account, status: status)
|
||||
is_expected.to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'not pinned' do
|
||||
it 'returns false' do
|
||||
is_expected.to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'muting an account' do
|
||||
let(:me) { Fabricate(:account, username: 'Me') }
|
||||
let(:you) { Fabricate(:account, username: 'You') }
|
||||
|
|
|
@ -83,8 +83,31 @@ RSpec.describe Status, type: :model do
|
|||
end
|
||||
|
||||
describe '#title' do
|
||||
it 'is a shorter version of the content' do
|
||||
expect(subject.title).to be_a String
|
||||
# rubocop:disable Style/InterpolationCheck
|
||||
|
||||
let(:account) { subject.account }
|
||||
|
||||
context 'if destroyed?' do
|
||||
it 'returns "#{account.acct} deleted status"' do
|
||||
subject.destroy!
|
||||
expect(subject.title).to eq "#{account.acct} deleted status"
|
||||
end
|
||||
end
|
||||
|
||||
context 'unless destroyed?' do
|
||||
context 'if reblog?' do
|
||||
it 'returns "#{account.acct} shared a status by #{reblog.account.acct}"' do
|
||||
reblog = subject.reblog = other
|
||||
expect(subject.title).to eq "#{account.acct} shared a status by #{reblog.account.acct}"
|
||||
end
|
||||
end
|
||||
|
||||
context 'unless reblog?' do
|
||||
it 'returns "New status by #{account.acct}"' do
|
||||
subject.reblog = nil
|
||||
expect(subject.title).to eq "New status by #{account.acct}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue