diff options
38 files changed, 852 insertions, 42 deletions
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index e81290228..d9199b3d5 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -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( diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index f0d2d481f..b2399ae9b 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -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} /> diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index 8cf7b92ca..5e251c0e5 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -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} diff --git a/app/javascript/mastodon/features/account_timeline/components/moved_note.js b/app/javascript/mastodon/features/account_timeline/components/moved_note.js new file mode 100644 index 000000000..1c0e081cc --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/moved_note.js @@ -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> + ); + } + +} diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js index 8a4d69f26..5891d0fdd 100644 --- a/app/javascript/mastodon/reducers/accounts.js +++ b/app/javascript/mastodon/reducers/accounts.js @@ -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)); }; diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index d26d1b727..e47ec5183 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -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); + }); }); }; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 0ded6f159..b70769e18 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -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; + } +} diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss index 0bf9daafd..2f7c98d54 100644 --- a/app/javascript/styles/mastodon/landing_strip.scss +++ b/app/javascript/styles/mastodon/landing_strip.scss @@ -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; + } + } + } +} diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 790d2025c..90d589d90 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -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', diff --git a/app/models/account.rb b/app/models/account.rb index a4b8e1c0b..19186b697 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -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 diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 2b148c82b..6e9a2cd4b 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -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 diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 896d67115..622bdde0c 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -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 diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 65fdb0308..bab944c5a 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -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 diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f93baf4b5..5ee7d89ee 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -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 diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index 94ec5ae5b..b0062752c 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -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,15 +27,15 @@ %small %span @#{account.local_username_and_domain} = fa_icon('lock') if account.locked? - - - if account.user_admin? - .roles - .account-role.admin - = t 'accounts.roles.admin' - - elsif account.user_moderator? - .roles - .account-role.moderator - = t 'accounts.roles.moderator' + - if Setting.show_staff_badge + - if account.user_admin? + .roles + .account-role.admin + = t 'accounts.roles.admin' + - elsif account.user_moderator? + .roles + .account-role.moderator + = t 'accounts.roles.moderator' .bio .account__header__content.p-note.emojify!=processed_bio[:text] - if processed_bio[:metadata].length > 0 diff --git a/app/views/accounts/_moved_strip.html.haml b/app/views/accounts/_moved_strip.html.haml new file mode 100644 index 000000000..6a14a5dd3 --- /dev/null +++ b/app/views/accounts/_moved_strip.html.haml @@ -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} diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index fd8ad5530..accad5f78 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -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 } diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 468166035..b07718315 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -19,6 +19,9 @@ = 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') .fields-group diff --git a/config/locales/de.yml b/config/locales/de.yml index 169af50ff..db96f7de7 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -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" diff --git a/config/locales/en.yml b/config/locales/en.yml index cebf704ce..3b2aa4897 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 0bf195d1b..847299ac7 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -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" diff --git a/config/locales/fa.yml b/config/locales/fa.yml index 24a2ddd51..1e3bd0e8b 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -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" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 55588d111..2fd875b2c 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 82b642b5b..d84bda656 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -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" diff --git a/config/locales/ko.yml b/config/locales/ko.yml index abf5f0ea4..fd6351486 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -518,8 +518,6 @@ ko: sensitive_content: 민감한 컨텐츠 terms: title: "%{instance} 이용약관과 개인정보 취급 방침" - themes: - default: Mastodon time: formats: default: "%Y년 %m월 %d일 %H:%M" diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 501ec013d..cda771ce2 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -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" diff --git a/config/locales/oc.yml b/config/locales/oc.yml index 914cc7e9d..e5d036303 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -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 diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 49dace354..047d3df9b 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -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" diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index f5c61c01c..de2b9c778 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -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" diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 7c9caec14..5eb7f256a 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -521,8 +521,6 @@ ru: sensitive_content: Чувствительный контент terms: title: Условия обслуживания и политика конфиденциальности %{instance} - themes: - default: Mastodon time: formats: default: "%b %d, %Y, %H:%M" diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 260b44666..ebb6d6595 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -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" diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index bba70f2e4..cbf642615 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -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" diff --git a/config/settings.yml b/config/settings.yml index 6983484d0..670a992bb 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -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 diff --git a/db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb b/db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb new file mode 100644 index 000000000..0c8a894cc --- /dev/null +++ b/db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 10e35cd7d..39d81a810 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/spec/lib/settings/extend_spec.rb b/spec/lib/settings/extend_spec.rb new file mode 100644 index 000000000..83ced4230 --- /dev/null +++ b/spec/lib/settings/extend_spec.rb @@ -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 diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb index 1e238e27c..918508ee6 100644 --- a/spec/models/concerns/account_interactions_spec.rb +++ b/spec/models/concerns/account_interactions_spec.rb @@ -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') } diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 89ad3adcf..c6701018e 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -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 |