diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/controllers/settings/profiles_controller.rb | 6 | ||||
-rw-r--r-- | app/javascript/mastodon/actions/importer/normalizer.js | 8 | ||||
-rw-r--r-- | app/javascript/mastodon/features/account/components/header.js | 14 | ||||
-rw-r--r-- | app/javascript/styles/mastodon/accounts.scss | 54 | ||||
-rw-r--r-- | app/javascript/styles/mastodon/components.scss | 37 | ||||
-rw-r--r-- | app/javascript/styles/mastodon/forms.scss | 12 | ||||
-rw-r--r-- | app/lib/activitypub/adapter.rb | 3 | ||||
-rw-r--r-- | app/lib/formatter.rb | 18 | ||||
-rw-r--r-- | app/models/account.rb | 36 | ||||
-rw-r--r-- | app/serializers/activitypub/actor_serializer.rb | 17 | ||||
-rw-r--r-- | app/serializers/rest/account_serializer.rb | 10 | ||||
-rw-r--r-- | app/services/activitypub/process_account_service.rb | 6 | ||||
-rw-r--r-- | app/views/accounts/_header.html.haml | 8 | ||||
-rw-r--r-- | app/views/settings/profiles/show.html.haml | 10 |
14 files changed, 234 insertions, 5 deletions
diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index dadc3d911..2b8330f2e 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -8,7 +8,9 @@ class Settings::ProfilesController < Settings::BaseController obfuscate_filename [:account, :avatar] obfuscate_filename [:account, :header] - def show; end + def show + @account.build_fields + end def update if UpdateAccountService.new.call(@account, account_params) @@ -22,7 +24,7 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - params.require(:account).permit(:display_name, :note, :avatar, :header, :locked) + params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, fields_attributes: [:name, :value]) end def set_account diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 1b09f319f..5f1274fab 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -10,6 +10,14 @@ export function normalizeAccount(account) { account.display_name_html = emojify(escapeTextContentForBrowser(displayName)); account.note_emojified = emojify(account.note); + if (account.fields) { + account.fields = account.fields.map(pair => ({ + ...pair, + name_emojified: emojify(escapeTextContentForBrowser(pair.name)), + value_emojified: emojify(pair.value), + })); + } + if (account.moved) { account.moved = account.moved.id; } diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index bb7b3b632..bbf886dca 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -130,6 +130,7 @@ export default class Header extends ImmutablePureComponent { const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; + const fields = account.get('fields'); return ( <div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${account.get('header')})` }}> @@ -140,6 +141,19 @@ export default class Header extends ImmutablePureComponent { <span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span> <div className='account__header__content' dangerouslySetInnerHTML={content} /> + {fields.size > 0 && ( + <table className='account__header__fields'> + <tbody> + {fields.map((pair, i) => ( + <tr key={i}> + <th dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} /> + <td dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> + </tr> + ))} + </tbody> + </table> + )} + {info} {mutingInfo} {actionBtn} diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index dd82ab375..0b49da1ad 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -563,3 +563,57 @@ border-color: rgba(lighten($error-red, 12%), 0.5); } } + +.account__header__fields { + border-collapse: collapse; + padding: 0; + margin: 15px -15px -15px; + border: 0 none; + border-top: 1px solid lighten($ui-base-color, 4%); + border-bottom: 1px solid lighten($ui-base-color, 4%); + + th, + td { + padding: 15px; + padding-left: 15px; + border: 0 none; + border-bottom: 1px solid lighten($ui-base-color, 4%); + vertical-align: middle; + } + + th { + padding-left: 15px; + font-weight: 500; + text-align: center; + width: 94px; + color: $ui-secondary-color; + background: rgba(darken($ui-base-color, 8%), 0.5); + } + + td { + color: $ui-primary-color; + text-align: center; + width: 100%; + padding-left: 0; + } + + a { + color: $ui-highlight-color; + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + + tr { + &:last-child { + th, + td { + border-bottom: 0; + } + } + } +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 94e3089f8..2d74bc717 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -5176,3 +5176,40 @@ noscript { background: lighten($ui-highlight-color, 7%); } } + +.account__header .account__header__fields { + font-size: 14px; + line-height: 20px; + overflow: hidden; + border-collapse: collapse; + margin: 20px -10px -20px; + border-bottom: 0; + + tr { + border-top: 1px solid lighten($ui-base-color, 8%); + text-align: center; + } + + th, + td { + padding: 14px 20px; + vertical-align: middle; + max-height: 40px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + th { + color: $ui-primary-color; + background: darken($ui-base-color, 4%); + max-width: 120px; + font-weight: 500; + } + + td { + flex: auto; + color: $primary-text-color; + background: $ui-base-color; + } +} diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index d74c5a4fd..945579a9c 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -15,6 +15,18 @@ code { overflow: hidden; } + .row { + display: flex; + margin: 0 -5px; + + .input { + box-sizing: border-box; + flex: 1 1 auto; + width: 50%; + padding: 0 5px; + } + } + span.hint { display: block; color: $ui-primary-color; diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index f19b04ae6..e880499f1 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -19,6 +19,9 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base 'Emoji' => 'toot:Emoji', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' }, 'featured' => 'toot:featured', + 'schema' => 'http://schema.org#', + 'PropertyValue' => 'schema:PropertyValue', + 'value' => 'schema:value', }, ], }.freeze diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index f7e7a3c23..4124f1660 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -71,6 +71,11 @@ class Formatter html.html_safe # rubocop:disable Rails/OutputSafety end + def format_field(account, str) + return reformat(str).html_safe unless account.local? # rubocop:disable Rails/OutputSafety + encode_and_link_urls(str, me: true).html_safe # rubocop:disable Rails/OutputSafety + end + def linkify(text) html = encode_and_link_urls(text) html = simple_format(html, {}, sanitize: false) @@ -85,12 +90,17 @@ class Formatter HTMLEntities.new.encode(html) end - def encode_and_link_urls(html, accounts = nil) + def encode_and_link_urls(html, accounts = nil, options = {}) entities = Extractor.extract_entities_with_indices(html, extract_url_without_protocol: false) + if accounts.is_a?(Hash) + options = accounts + accounts = nil + end + rewrite(html.dup, entities) do |entity| if entity[:url] - link_to_url(entity) + link_to_url(entity, options) elsif entity[:hashtag] link_to_hashtag(entity) elsif entity[:screen_name] @@ -177,10 +187,12 @@ class Formatter result.flatten.join end - def link_to_url(entity) + def link_to_url(entity, options = {}) url = Addressable::URI.parse(entity[:url]) html_attrs = { target: '_blank', rel: 'nofollow noopener' } + html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me] + Twitter::Autolink.send(:link_to_text, entity, link_html(entity[:url]), url, html_attrs) rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError encode(entity[:url]) diff --git a/app/models/account.rb b/app/models/account.rb index dd8bad585..49ee50dfb 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -44,6 +44,7 @@ # memorial :boolean default(FALSE), not null # moved_to_account_id :integer # featured_collection_url :string +# fields :jsonb # class Account < ApplicationRecord @@ -192,6 +193,30 @@ class Account < ApplicationRecord @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key) end + def fields + (self[:fields] || []).map { |f| Field.new(self, f) } + end + + def fields_attributes=(attributes) + fields = [] + + attributes.each_value do |attr| + next if attr[:name].blank? + fields << attr + end + + self[:fields] = fields + end + + def build_fields + return if fields.size >= 4 + + raw_fields = self[:fields] || [] + add_fields = 4 - raw_fields.size + add_fields.times { raw_fields << { name: '', value: '' } } + self.fields = raw_fields + end + def magic_key modulus, exponent = [keypair.public_key.n, keypair.public_key.e].map do |component| result = [] @@ -241,6 +266,17 @@ class Account < ApplicationRecord shared_inbox_url.presence || inbox_url end + class Field < ActiveModelSerializers::Model + attributes :name, :value, :account, :errors + + def initialize(account, attr) + @account = account + @name = attr['name'] + @value = attr['value'] + @errors = {} + end + end + class << self def readonly_attributes super - %w(statuses_count following_count followers_count) diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index df3090726..fcf3bdf17 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -11,6 +11,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer has_one :public_key, serializer: ActivityPub::PublicKeySerializer has_many :virtual_tags, key: :tag + has_many :virtual_attachments, key: :attachment attribute :moved_to, if: :moved? @@ -107,10 +108,26 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer object.emojis end + def virtual_attachments + object.fields + end + def moved_to ActivityPub::TagManager.instance.uri_for(object.moved_to_account) end class CustomEmojiSerializer < ActivityPub::EmojiSerializer end + + class Account::FieldSerializer < ActiveModel::Serializer + attributes :type, :name, :value + + def type + 'PropertyValue' + end + + def value + Formatter.instance.format_field(object.account, object.value) + end + end end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 6097acda5..863238eb7 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -9,6 +9,16 @@ class REST::AccountSerializer < ActiveModel::Serializer has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested? + class FieldSerializer < ActiveModel::Serializer + attributes :name, :value + + def value + Formatter.instance.format_field(object.account, object.value) + end + end + + has_many :fields + 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 4475a9079..da32f9615 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -70,6 +70,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.display_name = @json['name'] || '' @account.note = @json['summary'] || '' @account.locked = @json['manuallyApprovesFollowers'] || false + @account.fields = property_values || {} end def set_fetchable_attributes! @@ -126,6 +127,11 @@ class ActivityPub::ProcessAccountService < BaseService end end + def property_values + return unless @json['attachment'].is_a?(Array) + @json['attachment'].select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') } + end + def mismatching_origin?(url) needle = Addressable::URI.parse(url).host haystack = Addressable::URI.parse(@uri).host diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index 73af27a4c..617443e84 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -28,6 +28,14 @@ %th.emojify>!=i[0] %td.emojify>!=i[1] + - unless account.fields.empty? + %table.account__header__fields + %tbody + - account.fields.each do |field| + %tr + %th.emojify= field.name + %td.emojify= Formatter.instance.format_field(account, field.value) + .details-counters .counter{ class: active_nav_class(short_account_url(account)) } = link_to short_account_url(account), class: 'u-url u-uid' do diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index be7bd0ba0..5f63466d9 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -19,6 +19,16 @@ .fields-group = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') + .fields-group + .input.with_block_label + %label= t('simple_form.labels.defaults.fields') + %span.hint= t('simple_form.hints.defaults.fields') + + = f.simple_fields_for :fields do |fields_f| + .row + = fields_f.input :name, placeholder: t('simple_form.labels.account.fields.name') + = fields_f.input :value, placeholder: t('simple_form.labels.account.fields.value') + .actions = f.button :button, t('generic.save_changes'), type: :submit |