From 6793bec4c67e695100cb4d8551f0bda0b7e87b12 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 4 May 2018 21:14:34 +0200 Subject: Store URIs of follows, follow requests and blocks for ActivityPub (#7160) Same URI passed between follow request and follow, since they are the same thing in ActivityPub. Local URIs are generated during creation using UUIDs and are passed to serializers. --- app/models/block.rb | 10 ++++++++++ app/models/concerns/account_interactions.rb | 13 ++++++++----- app/models/follow.rb | 13 +++++++++++++ app/models/follow_request.rb | 16 ++++++++++++++-- 4 files changed, 45 insertions(+), 7 deletions(-) (limited to 'app/models') diff --git a/app/models/block.rb b/app/models/block.rb index df4a6bbac..bf3e07600 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -8,6 +8,7 @@ # updated_at :datetime not null # account_id :bigint(8) not null # target_account_id :bigint(8) not null +# uri :string # class Block < ApplicationRecord @@ -19,7 +20,12 @@ class Block < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } + def local? + false # Force uri_for to use uri attribute + end + after_commit :remove_blocking_cache + before_validation :set_uri, only: :create private @@ -27,4 +33,8 @@ class Block < ApplicationRecord Rails.cache.delete("exclude_account_ids_for:#{account_id}") Rails.cache.delete("exclude_account_ids_for:#{target_account_id}") end + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 4a01eed65..ae43711be 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -82,16 +82,19 @@ module AccountInteractions has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy end - def follow!(other_account, reblogs: nil) + def follow!(other_account, reblogs: nil, uri: nil) reblogs = true if reblogs.nil? - rel = active_relationships.create_with(show_reblogs: reblogs).find_or_create_by!(target_account: other_account) - rel.update!(show_reblogs: reblogs) + rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri) + .find_or_create_by!(target_account: other_account) + + rel.update!(show_reblogs: reblogs) rel end - def block!(other_account) - block_relationships.find_or_create_by!(target_account: other_account) + def block!(other_account, uri: nil) + block_relationships.create_with(uri: uri) + .find_or_create_by!(target_account: other_account) end def mute!(other_account, notifications: nil) diff --git a/app/models/follow.rb b/app/models/follow.rb index 2ca42ff70..eaf8445f3 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -9,6 +9,7 @@ # account_id :bigint(8) not null # target_account_id :bigint(8) not null # show_reblogs :boolean default(TRUE), not null +# uri :string # class Follow < ApplicationRecord @@ -26,4 +27,16 @@ class Follow < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } scope :recent, -> { reorder(id: :desc) } + + def local? + false # Force uri_for to use uri attribute + end + + before_validation :set_uri, only: :create + + private + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index d559a8f62..9c4875564 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -9,6 +9,7 @@ # account_id :bigint(8) not null # target_account_id :bigint(8) not null # show_reblogs :boolean default(TRUE), not null +# uri :string # class FollowRequest < ApplicationRecord @@ -23,11 +24,22 @@ class FollowRequest < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } def authorize! - account.follow!(target_account, reblogs: show_reblogs) + account.follow!(target_account, reblogs: show_reblogs, uri: uri) MergeWorker.perform_async(target_account.id, account.id) - destroy! end alias reject! destroy! + + def local? + false # Force uri_for to use uri attribute + end + + before_validation :set_uri, only: :create + + private + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end -- cgit From c73ce7b695aef1bbfe36bf674173d5a9fb988df5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 5 May 2018 00:54:24 +0200 Subject: Store home feeds for 7 days instead of 14 (#7354) * Store home feeds for 7 days instead of 14 Reduces workload for status fan-out to active followers * Fix test for user model --- app/models/user.rb | 2 +- app/services/fan_out_on_write_service.rb | 4 ++-- spec/models/user_spec.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'app/models') diff --git a/app/models/user.rb b/app/models/user.rb index a9f3e1da2..f5f542f07 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -41,7 +41,7 @@ class User < ApplicationRecord include Settings::Extend include Omniauthable - ACTIVE_DURATION = 14.days + ACTIVE_DURATION = 7.days devise :two_factor_authenticatable, otp_secret_encryption_key: Rails.configuration.x.otp_secret diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 510b80c82..cb82a79ed 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -37,7 +37,7 @@ class FanOutOnWriteService < BaseService def deliver_to_followers(status) Rails.logger.debug "Delivering status #{status.id} to followers" - status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |followers| + status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).select(:id).reorder(nil).find_in_batches do |followers| FeedInsertWorker.push_bulk(followers) do |follower| [status.id, follower.id, :home] end @@ -47,7 +47,7 @@ class FanOutOnWriteService < BaseService def deliver_to_lists(status) Rails.logger.debug "Delivering status #{status.id} to lists" - status.account.lists.joins(account: :user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |lists| + status.account.lists.joins(account: :user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).select(:id).reorder(nil).find_in_batches do |lists| FeedInsertWorker.push_bulk(lists) do |list| [status.id, list.id, :list] end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 760214ded..cc8d88cc8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -75,7 +75,7 @@ RSpec.describe User, type: :model do describe 'inactive' do it 'returns a relation of inactive users' do specified = Fabricate(:user, current_sign_in_at: 15.days.ago) - Fabricate(:user, current_sign_in_at: 13.days.ago) + Fabricate(:user, current_sign_in_at: 6.days.ago) expect(User.inactive).to match_array([specified]) end -- cgit From 8da4bf0f901d7b20f3ca7c6975a0d21a56fb735c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 5 May 2018 21:11:19 +0200 Subject: 4 profile fields max, store only 255 characters per name/value (#7348) Fix #7303 --- app/models/account.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'app/models') diff --git a/app/models/account.rb b/app/models/account.rb index a166fb474..72ba0398e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -74,6 +74,7 @@ class Account < ApplicationRecord validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? } validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? } validates :note, length: { maximum: 160 }, if: -> { local? && will_save_change_to_note? } + validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? } # Timelines has_many :stream_entries, inverse_of: :account, dependent: :destroy @@ -198,9 +199,11 @@ class Account < ApplicationRecord def fields_attributes=(attributes) fields = [] - attributes.each_value do |attr| - next if attr[:name].blank? - fields << attr + if attributes.is_a?(Hash) + attributes.each_value do |attr| + next if attr[:name].blank? + fields << attr + end end self[:fields] = fields @@ -269,8 +272,8 @@ class Account < ApplicationRecord def initialize(account, attr) @account = account - @name = attr['name'] - @value = attr['value'] + @name = attr['name'].strip[0, 255] + @value = attr['value'].strip[0, 255] @errors = {} end -- cgit From 61a90186070395e133ad2f8e959bdf003a8615ca Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 6 May 2018 11:48:51 +0200 Subject: Enable custom emojis in profiles (notes, field values, display names) (#7374) Follow-up to #6124 --- app/helpers/stream_entries_helper.rb | 4 ++-- .../mastodon/actions/importer/normalizer.js | 19 +++++++++++-------- app/lib/formatter.rb | 12 ++++++++++-- app/models/account.rb | 6 +++++- app/serializers/rest/account_serializer.rb | 1 + app/views/about/_administration.html.haml | 2 +- app/views/about/_contact.html.haml | 2 +- app/views/accounts/_grid_card.html.haml | 2 +- app/views/accounts/_header.html.haml | 4 ++-- app/views/accounts/_moved_strip.html.haml | 4 ++-- app/views/admin/reports/_account.html.haml | 2 +- app/views/authorize_follows/_card.html.haml | 2 +- app/views/remote_unfollows/_card.html.haml | 2 +- app/views/shared/_landing_strip.html.haml | 2 +- app/views/stream_entries/_detailed_status.html.haml | 2 +- app/views/stream_entries/_simple_status.html.haml | 2 +- app/views/stream_entries/_status.html.haml | 2 +- 17 files changed, 43 insertions(+), 27 deletions(-) (limited to 'app/models') diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index c6f12ecd4..707c8e26c 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -4,8 +4,8 @@ module StreamEntriesHelper EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' - def display_name(account) - account.display_name.presence || account.username + def display_name(account, **options) + Formatter.instance.format_display_name(account, options) end def account_description(account) diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 5f1274fab..057bff58b 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -3,18 +3,25 @@ import emojify from '../../features/emoji/emoji'; const domParser = new DOMParser(); +const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji; + return obj; +}, {}); + export function normalizeAccount(account) { account = { ...account }; + const emojiMap = makeEmojiMap(account); const displayName = account.display_name.length === 0 ? account.username : account.display_name; - account.display_name_html = emojify(escapeTextContentForBrowser(displayName)); - account.note_emojified = emojify(account.note); + + account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); + account.note_emojified = emojify(account.note, emojiMap); if (account.fields) { account.fields = account.fields.map(pair => ({ ...pair, name_emojified: emojify(escapeTextContentForBrowser(pair.name)), - value_emojified: emojify(pair.value), + value_emojified: emojify(pair.value, emojiMap), })); } @@ -42,11 +49,7 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.hidden = normalOldStatus.get('hidden'); } else { const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); - - const emojiMap = normalStatus.emojis.reduce((obj, emoji) => { - obj[`:${emoji.shortcode}:`] = emoji; - return obj; - }, {}); + const emojiMap = makeEmojiMap(normalStatus); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 050c651ee..e1ab05cc0 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -67,9 +67,17 @@ class Formatter html.html_safe # rubocop:disable Rails/OutputSafety end - def format_field(account, str) + def format_display_name(account, **options) + html = encode(account.display_name.presence || account.username) + html = encode_custom_emojis(html, account.emojis) if options[:custom_emojify] + html.html_safe # rubocop:disable Rails/OutputSafety + end + + def format_field(account, str, **options) 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 + html = encode_and_link_urls(str, me: true) + html = encode_custom_emojis(html, account.emojis) if options[:custom_emojify] + html.html_safe # rubocop:disable Rails/OutputSafety end def linkify(text) diff --git a/app/models/account.rb b/app/models/account.rb index 72ba0398e..4467d1512 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -398,7 +398,7 @@ class Account < ApplicationRecord end def emojis - @emojis ||= CustomEmoji.from_text(note, domain) + @emojis ||= CustomEmoji.from_text(emojifiable_text, domain) end before_create :generate_keys @@ -425,4 +425,8 @@ class Account < ApplicationRecord self.domain = TagManager.instance.normalize_domain(domain) end + + def emojifiable_text + [note, display_name, fields.map(&:value)].join(' ') + end end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 863238eb7..8761bbb5e 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -8,6 +8,7 @@ class REST::AccountSerializer < ActiveModel::Serializer :followers_count, :following_count, :statuses_count has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested? + has_many :emojis, serializer: REST::CustomEmojiSerializer class FieldSerializer < ActiveModel::Serializer attributes :name, :value diff --git a/app/views/about/_administration.html.haml b/app/views/about/_administration.html.haml index ec5834f9c..02286d68b 100644 --- a/app/views/about/_administration.html.haml +++ b/app/views/about/_administration.html.haml @@ -6,7 +6,7 @@ .account__avatar{ style: "background-image: url(#{@instance_presenter.contact_account.avatar.url})" } %span.display-name %bdi - %strong.display-name__html.emojify= display_name(@instance_presenter.contact_account) + %strong.display-name__html.emojify= display_name(@instance_presenter.contact_account, custom_emojify: true) %span.display-name__account @#{@instance_presenter.contact_account.acct} - else .account__display-name diff --git a/app/views/about/_contact.html.haml b/app/views/about/_contact.html.haml index cf21ad5a3..3215d50b5 100644 --- a/app/views/about/_contact.html.haml +++ b/app/views/about/_contact.html.haml @@ -12,7 +12,7 @@ .avatar= image_tag contact.contact_account.avatar.url .name = link_to TagManager.instance.url_for(contact.contact_account) do - %span.display_name.emojify= display_name(contact.contact_account) + %span.display_name.emojify= display_name(contact.contact_account, custom_emojify: true) %span.username @#{contact.contact_account.acct} - else .owner diff --git a/app/views/accounts/_grid_card.html.haml b/app/views/accounts/_grid_card.html.haml index 95acbd581..a59ed128e 100644 --- a/app/views/accounts/_grid_card.html.haml +++ b/app/views/accounts/_grid_card.html.haml @@ -5,7 +5,7 @@ .avatar= image_tag account.avatar.url(:original) .name = link_to TagManager.instance.url_for(account) do - %span.display_name.emojify= display_name(account) + %span.display_name.emojify= display_name(account, custom_emojify: true) %span.username @#{account.local? ? account.local_username_and_domain : account.acct} = fa_icon('lock') if account.locked? diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index 41315f039..13dcaf616 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -5,7 +5,7 @@ .card__bio %h1.name - %span.p-name.emojify= display_name(account) + %span.p-name.emojify= display_name(account, custom_emojify: true) %small< %span>< @#{account.local_username_and_domain} = fa_icon('lock') if account.locked? @@ -28,7 +28,7 @@ - account.fields.each do |field| %dl %dt.emojify{ title: field.name }= field.name - %dd.emojify{ title: field.value }= Formatter.instance.format_field(account, field.value) + %dd.emojify{ title: field.value }= Formatter.instance.format_field(account, field.value, custom_emojify: true) .details-counters .counter{ class: active_nav_class(short_account_url(account)) } diff --git a/app/views/accounts/_moved_strip.html.haml b/app/views/accounts/_moved_strip.html.haml index 6a14a5dd3..ae18c6dc7 100644 --- a/app/views/accounts/_moved_strip.html.haml +++ b/app/views/accounts/_moved_strip.html.haml @@ -3,7 +3,7 @@ .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')) + = t('accounts.moved_html', name: content_tag(:strong, display_name(account, custom_emojify: true), 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 @@ -13,5 +13,5 @@ .account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" } %span.display-name - %strong.emojify= display_name(moved_to_account) + %strong.emojify= display_name(moved_to_account, custom_emojify: true) %span @#{moved_to_account.acct} diff --git a/app/views/admin/reports/_account.html.haml b/app/views/admin/reports/_account.html.haml index 22b7a0861..9ac161c9c 100644 --- a/app/views/admin/reports/_account.html.haml +++ b/app/views/admin/reports/_account.html.haml @@ -15,5 +15,5 @@ .account__avatar{ style: "background-image: url(#{account.avatar.url}); width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px" } %span.display-name %bdi - %strong.display-name__html.emojify= display_name(account) + %strong.display-name__html.emojify= display_name(account, custom_emojify: true) %span.display-name__account @#{account.acct} diff --git a/app/views/authorize_follows/_card.html.haml b/app/views/authorize_follows/_card.html.haml index e81e292ba..9abcfd37e 100644 --- a/app/views/authorize_follows/_card.html.haml +++ b/app/views/authorize_follows/_card.html.haml @@ -6,7 +6,7 @@ %span.display-name - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do - %strong.emojify= display_name(account) + %strong.emojify= display_name(account, custom_emojify: true) %span @#{account.acct} - if account.note? diff --git a/app/views/remote_unfollows/_card.html.haml b/app/views/remote_unfollows/_card.html.haml index e81e292ba..9abcfd37e 100644 --- a/app/views/remote_unfollows/_card.html.haml +++ b/app/views/remote_unfollows/_card.html.haml @@ -6,7 +6,7 @@ %span.display-name - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do - %strong.emojify= display_name(account) + %strong.emojify= display_name(account, custom_emojify: true) %span @#{account.acct} - if account.note? diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml index ae26fc1ff..78f5ed4bc 100644 --- a/app/views/shared/_landing_strip.html.haml +++ b/app/views/shared/_landing_strip.html.haml @@ -2,7 +2,7 @@ = image_tag asset_pack_path('logo.svg'), class: 'logo' %div - = t('landing_strip_html', name: content_tag(:span, display_name(account), class: :emojify), link_to_root_path: link_to(content_tag(:strong, site_hostname), root_path)) + = t('landing_strip_html', name: content_tag(:span, display_name(account, custom_emojify: true), class: :emojify), link_to_root_path: link_to(content_tag(:strong, site_hostname), root_path)) - if open_registrations? = t('landing_strip_signup_html', sign_up_path: new_user_registration_path) diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index afc66d148..c0f1e4f0f 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -4,7 +4,7 @@ .avatar = image_tag status.account.avatar.url(:original), width: 48, height: 48, alt: '', class: 'u-photo' %span.display-name - %strong.p-name.emojify= display_name(status.account) + %strong.p-name.emojify= display_name(status.account, custom_emojify: true) %span= acct(status.account) - if embedded_view? diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index a6f5120fb..b89860ad9 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -10,7 +10,7 @@ %div = image_tag status.account.avatar(:original), width: 48, height: 48, alt: '', class: 'u-photo' %span.display-name - %strong.p-name.emojify= display_name(status.account) + %strong.p-name.emojify= display_name(status.account, custom_emojify: true) %span= acct(status.account) .status__content.p-name.emojify< diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml index 9764bc74d..b87ca2177 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/stream_entries/_status.html.haml @@ -28,7 +28,7 @@ = fa_icon('retweet fw') %span = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do - %strong.emojify= display_name(status.account) + %strong.emojify= display_name(status.account, custom_emojify: true) = t('stream_entries.reblogged') - elsif pinned .pre-header -- cgit From 42cd363542abf0a12a4e877b3ad26024f24577ef Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 7 May 2018 09:31:07 +0200 Subject: Bot nameplates (#7391) * Store actor type in database * Add bot nameplate to web UI, add setting to preferences, API, AP Fix #7365 * Fix code style issues --- app/controllers/api/v1/accounts/credentials_controller.rb | 2 +- app/controllers/settings/profiles_controller.rb | 2 +- app/javascript/mastodon/features/account/components/header.js | 4 ++++ app/javascript/styles/mastodon/components.scss | 6 ++++++ app/javascript/styles/mastodon/forms.scss | 4 ++++ app/models/account.rb | 11 +++++++++++ app/serializers/activitypub/actor_serializer.rb | 2 +- app/serializers/rest/account_serializer.rb | 2 +- app/services/activitypub/process_account_service.rb | 1 + app/views/accounts/_header.html.haml | 6 +++++- app/views/settings/profiles/show.html.haml | 3 +++ config/locales/en.yml | 1 + config/locales/simple_form.en.yml | 2 ++ db/migrate/20180506221944_add_actor_type_to_accounts.rb | 5 +++++ db/schema.rb | 3 ++- 15 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20180506221944_add_actor_type_to_accounts.rb (limited to 'app/models') diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index a3c4008e6..259d07be8 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController private def account_params - params.permit(:display_name, :note, :avatar, :header, :locked, fields_attributes: [:name, :value]) + params.permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value]) end def user_settings_params diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 5d81668de..1b01fc75f 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -27,7 +27,7 @@ class Settings::ProfilesController < ApplicationController private def account_params - params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, fields_attributes: [:name, :value]) + params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value]) end def set_account diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 47915e6fb..7358053da 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -131,6 +131,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'); + const badge = account.get('bot') ? (

) : null; return (
@@ -139,6 +140,9 @@ export default class Header extends ImmutablePureComponent { @{account.get('acct')} {lockedIcon} + + {badge} +
{fields.size > 0 && ( diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 3e2a1ae10..158bf6851 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -5159,6 +5159,12 @@ noscript { } } +.account__header .roles { + margin-top: 20px; + margin-bottom: 20px; + padding: 0 15px; +} + .account__header .account__header__fields { font-size: 14px; line-height: 20px; diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index f97890187..de16784a8 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -87,6 +87,10 @@ code { align-items: flex-start; } + &.file .label_input { + flex-wrap: nowrap; + } + &.select .label_input { align-items: initial; } diff --git a/app/models/account.rb b/app/models/account.rb index 4467d1512..2b3ef5cdc 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -45,6 +45,7 @@ # moved_to_account_id :bigint(8) # featured_collection_url :string # fields :jsonb +# actor_type :string # class Account < ApplicationRecord @@ -149,6 +150,16 @@ class Account < ApplicationRecord moved_to_account_id.present? end + def bot? + %w(Application Service).include? actor_type + end + + alias bot bot? + + def bot=(val) + self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person' + end + def acct local? ? username : "#{username}@#{domain}" end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index fcf3bdf17..41c9aa44e 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -37,7 +37,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer end def type - 'Person' + object.bot? ? 'Service' : 'Person' end def following diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 8761bbb5e..6adcd7039 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -3,7 +3,7 @@ class REST::AccountSerializer < ActiveModel::Serializer include RoutingHelper - attributes :id, :username, :acct, :display_name, :locked, :created_at, + attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at, :note, :url, :avatar, :avatar_static, :header, :header_static, :followers_count, :following_count, :statuses_count diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f67ebb443..cc416b671 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -71,6 +71,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.note = @json['summary'] || '' @account.locked = @json['manuallyApprovesFollowers'] || false @account.fields = property_values || {} + @account.actor_type = @json['type'] end def set_fetchable_attributes! diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index 13dcaf616..4098d6778 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -10,7 +10,11 @@ %span>< @#{account.local_username_and_domain} = fa_icon('lock') if account.locked? - - if Setting.show_staff_badge + - if account.bot? + .roles + .account-role.bot + = t 'accounts.roles.bot' + - elsif Setting.show_staff_badge - if account.user_admin? .roles .account-role.admin diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index f28834d72..a84f8a7da 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -19,6 +19,9 @@ .fields-group = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') + .fields-group + = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot') + .fields-group .input.with_block_label %label= t('simple_form.labels.defaults.fields') diff --git a/config/locales/en.yml b/config/locales/en.yml index e18354eac..5369311ac 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -49,6 +49,7 @@ en: reserved_username: The username is reserved roles: admin: Admin + bot: Bot moderator: Mod unfollow: Unfollow admin: diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 7ba32906d..495c6166b 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -4,6 +4,7 @@ en: hints: defaults: avatar: PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px + bot: Warns people that the account does not represent a person digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence display_name: one: 1 character left @@ -29,6 +30,7 @@ en: value: Content defaults: avatar: Avatar + bot: This is a bot account confirm_new_password: Confirm new password confirm_password: Confirm password current_password: Current password diff --git a/db/migrate/20180506221944_add_actor_type_to_accounts.rb b/db/migrate/20180506221944_add_actor_type_to_accounts.rb new file mode 100644 index 000000000..7cfed640f --- /dev/null +++ b/db/migrate/20180506221944_add_actor_type_to_accounts.rb @@ -0,0 +1,5 @@ +class AddActorTypeToAccounts < ActiveRecord::Migration[5.2] + def change + add_column :accounts, :actor_type, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 566a320d8..f7fa24b83 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: 2018_04_16_210259) do +ActiveRecord::Schema.define(version: 2018_05_06_221944) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -75,6 +75,7 @@ ActiveRecord::Schema.define(version: 2018_04_16_210259) do t.bigint "moved_to_account_id" t.string "featured_collection_url" t.jsonb "fields" + t.string "actor_type" 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" -- cgit From b4fb766b23f4b50b51a366f55b451770ece3153a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 11 May 2018 11:49:12 +0200 Subject: Add REST API for Web Push Notifications subscriptions (#7445) - POST /api/v1/push/subscription - PUT /api/v1/push/subscription - DELETE /api/v1/push/subscription - New OAuth scope: "push" (required for the above methods) --- .../api/v1/push/subscriptions_controller.rb | 50 +++++++++++++ .../api/web/push_subscriptions_controller.rb | 11 +-- app/controllers/shares_controller.rb | 1 + app/models/user.rb | 2 +- app/models/web/push_subscription.rb | 47 +++++++----- app/serializers/initial_state_serializer.rb | 4 +- .../rest/web_push_subscription_serializer.rb | 13 ++++ app/serializers/web/notification_serializer.rb | 2 +- app/services/notify_service.rb | 21 +++--- app/workers/web/push_notification_worker.rb | 18 +++++ app/workers/web_push_notification_worker.rb | 25 ------- config/initializers/doorkeeper.rb | 2 +- config/locales/doorkeeper.en.yml | 1 + config/routes.rb | 4 ++ ...dd_access_token_id_to_web_push_subscriptions.rb | 6 ++ ...0180510230049_migrate_web_push_subscriptions.rb | 13 ++++ db/schema.rb | 8 ++- .../api/v1/push/subscriptions_controller_spec.rb | 83 ++++++++++++++++++++++ .../api/web/push_subscriptions_controller_spec.rb | 16 ++--- spec/models/web/push_subscription_spec.rb | 12 ---- 20 files changed, 258 insertions(+), 81 deletions(-) create mode 100644 app/controllers/api/v1/push/subscriptions_controller.rb create mode 100644 app/serializers/rest/web_push_subscription_serializer.rb create mode 100644 app/workers/web/push_notification_worker.rb delete mode 100644 app/workers/web_push_notification_worker.rb create mode 100644 db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb create mode 100644 db/migrate/20180510230049_migrate_web_push_subscriptions.rb create mode 100644 spec/controllers/api/v1/push/subscriptions_controller_spec.rb (limited to 'app/models') diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb new file mode 100644 index 000000000..5038cc03c --- /dev/null +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class Api::V1::Push::SubscriptionsController < Api::BaseController + before_action -> { doorkeeper_authorize! :push } + before_action :require_user! + before_action :set_web_push_subscription + + def create + @web_subscription&.destroy! + + @web_subscription = ::Web::PushSubscription.create!( + endpoint: subscription_params[:endpoint], + key_p256dh: subscription_params[:keys][:p256dh], + key_auth: subscription_params[:keys][:auth], + data: data_params, + user_id: current_user.id, + access_token_id: doorkeeper_token.id + ) + + render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer + end + + def update + raise ActiveRecord::RecordNotFound if @web_subscription.nil? + + @web_subscription.update!(data: data_params) + + render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer + end + + def destroy + @web_subscription&.destroy! + render_empty + end + + private + + def set_web_push_subscription + @web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id) + end + + def subscription_params + params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh]) + end + + def data_params + return {} if params[:data].blank? + params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention]) + end +end diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 249e7c186..fe8e42580 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -31,22 +31,23 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController endpoint: subscription_params[:endpoint], key_p256dh: subscription_params[:keys][:p256dh], key_auth: subscription_params[:keys][:auth], - data: data + data: data, + user_id: active_session.user_id, + access_token_id: active_session.access_token_id ) active_session.update!(web_push_subscription: web_subscription) - render json: web_subscription.as_payload + render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer end def update params.require([:id]) web_subscription = ::Web::PushSubscription.find(params[:id]) - web_subscription.update!(data: data_params) - render json: web_subscription.as_payload + render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer end private @@ -56,6 +57,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def data_params - @data_params ||= params.require(:data).permit(:alerts) + @data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention]) end end diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb index 3ec831a72..9ef1e0749 100644 --- a/app/controllers/shares_controller.rb +++ b/app/controllers/shares_controller.rb @@ -15,6 +15,7 @@ class SharesController < ApplicationController def initial_state_params text = [params[:title], params[:text], params[:url]].compact.join(' ') + { settings: Web::Setting.find_by(user: current_user)&.data || {}, push_subscription: current_account.user.web_push_subscription(current_session), diff --git a/app/models/user.rb b/app/models/user.rb index f5f542f07..21c217e77 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -245,7 +245,7 @@ class User < ApplicationRecord end def web_push_subscription(session) - session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload + session.web_push_subscription.nil? ? nil : session.web_push_subscription end def invite_code=(code) diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index 1736106f7..df549c6d3 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -3,38 +3,51 @@ # # Table name: web_push_subscriptions # -# id :bigint(8) not null, primary key -# endpoint :string not null -# key_p256dh :string not null -# key_auth :string not null -# data :json -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint(8) not null, primary key +# endpoint :string not null +# key_p256dh :string not null +# key_auth :string not null +# data :json +# created_at :datetime not null +# updated_at :datetime not null +# access_token_id :bigint(8) +# user_id :bigint(8) # -require 'webpush' - class Web::PushSubscription < ApplicationRecord + belongs_to :user, optional: true + belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', optional: true + has_one :session_activation def push(notification) - I18n.with_locale(session_activation.user.locale || I18n.default_locale) do + I18n.with_locale(associated_user.locale || I18n.default_locale) do push_payload(message_from(notification), 48.hours.seconds) end end def pushable?(notification) - data&.key?('alerts') && data['alerts'][notification.type.to_s] + data&.key?('alerts') && ActiveModel::Type::Boolean.new.cast(data['alerts'][notification.type.to_s]) end - def as_payload - payload = { id: id, endpoint: endpoint } - payload[:alerts] = data['alerts'] if data&.key?('alerts') - payload + def associated_user + return @associated_user if defined?(@associated_user) + + @associated_user = if user_id.nil? + session_activation.user + else + user + end end - def access_token - find_or_create_access_token.token + def associated_access_token + return @associated_access_token if defined?(@associated_access_token) + + @associated_access_token = if access_token_id.nil? + find_or_create_access_token.token + else + access_token + end end private diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 3b908e224..6c9fba2f5 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -2,7 +2,9 @@ class InitialStateSerializer < ActiveModel::Serializer attributes :meta, :compose, :accounts, - :media_attachments, :settings, :push_subscription + :media_attachments, :settings + + has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer def meta store = { diff --git a/app/serializers/rest/web_push_subscription_serializer.rb b/app/serializers/rest/web_push_subscription_serializer.rb new file mode 100644 index 000000000..7fd952a56 --- /dev/null +++ b/app/serializers/rest/web_push_subscription_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class REST::WebPushSubscriptionSerializer < ActiveModel::Serializer + attributes :id, :endpoint, :alerts, :server_key + + def alerts + object.data&.dig('alerts') || {} + end + + def server_key + Rails.configuration.x.vapid_public_key + end +end diff --git a/app/serializers/web/notification_serializer.rb b/app/serializers/web/notification_serializer.rb index e5524fe7a..31c703832 100644 --- a/app/serializers/web/notification_serializer.rb +++ b/app/serializers/web/notification_serializer.rb @@ -54,7 +54,7 @@ class Web::NotificationSerializer < ActiveModel::Serializer def access_token return if actions.empty? - current_push_subscription.access_token + current_push_subscription.associated_access_token end def message diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index ba086449c..6490d2735 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -9,6 +9,7 @@ class NotifyService < BaseService return if recipient.user.nil? || blocked? create_notification + push_notification if @notification.browserable? send_email if email_enabled? rescue ActiveRecord::RecordInvalid return @@ -101,25 +102,27 @@ class NotifyService < BaseService def create_notification @notification.save! - return unless @notification.browserable? + end + + def push_notification + return if @notification.activity.nil? + Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) send_push_notifications end def send_push_notifications - # HACK: Can be caused by quickly unfavouriting a status, since creating - # a favourite and creating a notification are not wrapped in a transaction. - return if @notification.activity.nil? - - sessions_with_subscriptions = @recipient.user.session_activations.where.not(web_push_subscription: nil) - sessions_with_subscriptions_ids = sessions_with_subscriptions.select { |session| session.web_push_subscription.pushable? @notification }.map(&:id) + subscriptions_ids = ::Web::PushSubscription.where(user_id: @recipient.user.id) + .select { |subscription| subscription.pushable?(@notification) } + .map(&:id) - WebPushNotificationWorker.push_bulk(sessions_with_subscriptions_ids) do |session_activation_id| - [session_activation_id, @notification.id] + ::Web::PushNotificationWorker.push_bulk(subscriptions_ids) do |subscription_id| + [subscription_id, @notification.id] end end def send_email + return if @notification.activity.nil? NotificationMailer.public_send(@notification.type, @recipient, @notification).deliver_later end diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb new file mode 100644 index 000000000..4a40e5c8b --- /dev/null +++ b/app/workers/web/push_notification_worker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Web::PushNotificationWorker + include Sidekiq::Worker + + sidekiq_options backtrace: true + + def perform(subscription_id, notification_id) + subscription = ::Web::PushSubscription.find(subscription_id) + notification = Notification.find(notification_id) + + subscription.push(notification) unless notification.activity.nil? + rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription + subscription.destroy! + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/web_push_notification_worker.rb b/app/workers/web_push_notification_worker.rb deleted file mode 100644 index eacea04c3..000000000 --- a/app/workers/web_push_notification_worker.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -class WebPushNotificationWorker - include Sidekiq::Worker - - sidekiq_options backtrace: true - - def perform(session_activation_id, notification_id) - session_activation = SessionActivation.find(session_activation_id) - notification = Notification.find(notification_id) - - return if session_activation.web_push_subscription.nil? || notification.activity.nil? - - session_activation.web_push_subscription.push(notification) - rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription - # Subscription expiration is not currently implemented in any browser - - session_activation.web_push_subscription.destroy! - session_activation.update!(web_push_subscription: nil) - - true - rescue ActiveRecord::RecordNotFound - true - end -end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 074f8c410..469553803 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -55,7 +55,7 @@ Doorkeeper.configure do # For more information go to # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes default_scopes :read - optional_scopes :write, :follow + optional_scopes :write, :follow, :push # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 33d544bed..eca1fc675 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -115,5 +115,6 @@ en: title: OAuth authorization required scopes: follow: follow, block, unblock and unfollow accounts + push: receive push notifications for your account read: read your account's data write: post on your behalf diff --git a/config/routes.rb b/config/routes.rb index 4c920cf74..b7bd1a7ed 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -306,6 +306,10 @@ Rails.application.routes.draw do resources :lists, only: [:index, :create, :show, :update, :destroy] do resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' end + + namespace :push do + resource :subscription, only: [:create, :update, :destroy] + end end namespace :web do diff --git a/db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb b/db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb new file mode 100644 index 000000000..94ef8e0f5 --- /dev/null +++ b/db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb @@ -0,0 +1,6 @@ +class AddAccessTokenIdToWebPushSubscriptions < ActiveRecord::Migration[5.2] + def change + add_reference :web_push_subscriptions, :access_token, null: true, default: nil, foreign_key: { on_delete: :cascade, to_table: :oauth_access_tokens }, index: false + add_reference :web_push_subscriptions, :user, null: true, default: nil, foreign_key: { on_delete: :cascade }, index: false + end +end diff --git a/db/migrate/20180510230049_migrate_web_push_subscriptions.rb b/db/migrate/20180510230049_migrate_web_push_subscriptions.rb new file mode 100644 index 000000000..6de1bed79 --- /dev/null +++ b/db/migrate/20180510230049_migrate_web_push_subscriptions.rb @@ -0,0 +1,13 @@ +class MigrateWebPushSubscriptions < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def up + add_index :web_push_subscriptions, :user_id, algorithm: :concurrently + add_index :web_push_subscriptions, :access_token_id, algorithm: :concurrently + end + + def down + remove_index :web_push_subscriptions, :user_id + remove_index :web_push_subscriptions, :access_token_id + end +end diff --git a/db/schema.rb b/db/schema.rb index f7fa24b83..221c08d98 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: 2018_05_06_221944) do +ActiveRecord::Schema.define(version: 2018_05_10_230049) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -539,6 +539,10 @@ ActiveRecord::Schema.define(version: 2018_05_06_221944) do t.json "data" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "access_token_id" + t.bigint "user_id" + t.index ["access_token_id"], name: "index_web_push_subscriptions_on_access_token_id" + t.index ["user_id"], name: "index_web_push_subscriptions_on_user_id" end create_table "web_settings", force: :cascade do |t| @@ -605,5 +609,7 @@ ActiveRecord::Schema.define(version: 2018_05_06_221944) do add_foreign_key "subscriptions", "accounts", name: "fk_9847d1cbb5", on_delete: :cascade add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade add_foreign_key "users", "invites", on_delete: :nullify + add_foreign_key "web_push_subscriptions", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade + add_foreign_key "web_push_subscriptions", "users", on_delete: :cascade add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade end diff --git a/spec/controllers/api/v1/push/subscriptions_controller_spec.rb b/spec/controllers/api/v1/push/subscriptions_controller_spec.rb new file mode 100644 index 000000000..01146294f --- /dev/null +++ b/spec/controllers/api/v1/push/subscriptions_controller_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Push::SubscriptionsController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'push') } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + let(:create_payload) do + { + subscription: { + endpoint: 'https://fcm.googleapis.com/fcm/send/fiuH06a27qE:APA91bHnSiGcLwdaxdyqVXNDR9w1NlztsHb6lyt5WDKOC_Z_Q8BlFxQoR8tWFSXUIDdkyw0EdvxTu63iqamSaqVSevW5LfoFwojws8XYDXv_NRRLH6vo2CdgiN4jgHv5VLt2A8ah6lUX', + keys: { + p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=', + auth: 'eH_C8rq2raXqlcBVDa1gLg==', + }, + } + }.with_indifferent_access + end + + let(:alerts_payload) do + { + data: { + alerts: { + follow: true, + favourite: false, + reblog: true, + mention: false, + } + } + }.with_indifferent_access + end + + describe 'POST #create' do + it 'saves push subscriptions' do + post :create, params: create_payload + + push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) + + expect(push_subscription.endpoint).to eq(create_payload[:subscription][:endpoint]) + expect(push_subscription.key_p256dh).to eq(create_payload[:subscription][:keys][:p256dh]) + expect(push_subscription.key_auth).to eq(create_payload[:subscription][:keys][:auth]) + expect(push_subscription.user_id).to eq user.id + expect(push_subscription.access_token_id).to eq token.id + end + + it 'replaces old subscription on repeat calls' do + post :create, params: create_payload + post :create, params: create_payload + + expect(Web::PushSubscription.where(endpoint: create_payload[:subscription][:endpoint]).count).to eq 1 + end + end + + describe 'PUT #update' do + it 'changes alert settings' do + post :create, params: create_payload + put :update, params: alerts_payload + + push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) + + expect(push_subscription.data.dig('alerts', 'follow')).to eq(alerts_payload[:data][:alerts][:follow].to_s) + expect(push_subscription.data.dig('alerts', 'favourite')).to eq(alerts_payload[:data][:alerts][:favourite].to_s) + expect(push_subscription.data.dig('alerts', 'reblog')).to eq(alerts_payload[:data][:alerts][:reblog].to_s) + expect(push_subscription.data.dig('alerts', 'mention')).to eq(alerts_payload[:data][:alerts][:mention].to_s) + end + end + + describe 'DELETE #destroy' do + it 'removes the subscription' do + post :create, params: create_payload + delete :destroy + + expect(Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])).to be_nil + end + end +end diff --git a/spec/controllers/api/web/push_subscriptions_controller_spec.rb b/spec/controllers/api/web/push_subscriptions_controller_spec.rb index bbf94c5c6..381cdeab9 100644 --- a/spec/controllers/api/web/push_subscriptions_controller_spec.rb +++ b/spec/controllers/api/web/push_subscriptions_controller_spec.rb @@ -59,10 +59,10 @@ describe Api::Web::PushSubscriptionsController do push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) - expect(push_subscription.data['follow']).to eq(alerts_payload[:data][:follow]) - expect(push_subscription.data['favourite']).to eq(alerts_payload[:data][:favourite]) - expect(push_subscription.data['reblog']).to eq(alerts_payload[:data][:reblog]) - expect(push_subscription.data['mention']).to eq(alerts_payload[:data][:mention]) + expect(push_subscription.data['alerts']['follow']).to eq(alerts_payload[:data][:alerts][:follow].to_s) + expect(push_subscription.data['alerts']['favourite']).to eq(alerts_payload[:data][:alerts][:favourite].to_s) + expect(push_subscription.data['alerts']['reblog']).to eq(alerts_payload[:data][:alerts][:reblog].to_s) + expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s) end end end @@ -81,10 +81,10 @@ describe Api::Web::PushSubscriptionsController do push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) - expect(push_subscription.data['follow']).to eq(alerts_payload[:data][:follow]) - expect(push_subscription.data['favourite']).to eq(alerts_payload[:data][:favourite]) - expect(push_subscription.data['reblog']).to eq(alerts_payload[:data][:reblog]) - expect(push_subscription.data['mention']).to eq(alerts_payload[:data][:mention]) + expect(push_subscription.data['alerts']['follow']).to eq(alerts_payload[:data][:alerts][:follow].to_s) + expect(push_subscription.data['alerts']['favourite']).to eq(alerts_payload[:data][:alerts][:favourite].to_s) + expect(push_subscription.data['alerts']['reblog']).to eq(alerts_payload[:data][:alerts][:reblog].to_s) + expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s) end end end diff --git a/spec/models/web/push_subscription_spec.rb b/spec/models/web/push_subscription_spec.rb index 574da55ac..c6665611c 100644 --- a/spec/models/web/push_subscription_spec.rb +++ b/spec/models/web/push_subscription_spec.rb @@ -2,20 +2,8 @@ require 'rails_helper' RSpec.describe Web::PushSubscription, type: :model do let(:alerts) { { mention: true, reblog: false, follow: true, follow_request: false, favourite: true } } - let(:payload_no_alerts) { Web::PushSubscription.new(id: 1, endpoint: 'a', key_p256dh: 'c', key_auth: 'd').as_payload } - let(:payload_alerts) { Web::PushSubscription.new(id: 1, endpoint: 'a', key_p256dh: 'c', key_auth: 'd', data: { alerts: alerts }).as_payload } let(:push_subscription) { Web::PushSubscription.new(data: { alerts: alerts }) } - describe '#as_payload' do - it 'only returns id and endpoint' do - expect(payload_no_alerts.keys).to eq [:id, :endpoint] - end - - it 'returns alerts if set' do - expect(payload_alerts.keys).to eq [:id, :endpoint, :alerts] - end - end - describe '#pushable?' do it 'obeys alert settings' do expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Mention'))).to eq true -- cgit From be7eeb785682d80614340c63f6cf3d36e5e4fe2e Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 11 May 2018 13:14:50 +0200 Subject: Catch Paperclip processing failures (fixes #6378) (#7439) --- app/models/concerns/remotable.rb | 3 +++ 1 file changed, 3 insertions(+) (limited to 'app/models') diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index 7f1ef5191..20ddbca53 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -41,6 +41,9 @@ module Remotable rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}" nil + rescue Paperclip::Error => e + Rails.logger.debug "Error processing remote #{attachment_name}: #{e}" + nil end end -- cgit From f31e58af9e6333f96248562cebfd215b6d32bcfc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 May 2018 20:26:30 +0200 Subject: Fix nil error in StatusFilter (#7470) Fix #7462 --- app/models/concerns/status_threading_concern.rb | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) (limited to 'app/models') diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb index 8e817be00..1ba8fc693 100644 --- a/app/models/concerns/status_threading_concern.rb +++ b/app/models/concerns/status_threading_concern.rb @@ -74,16 +74,7 @@ module StatusThreadingConcern statuses = statuses_with_accounts(ids).to_a account_ids = statuses.map(&:account_id).uniq domains = statuses.map(&:account_domain).compact.uniq - - relations = if account.present? - { - blocking: Account.blocking_map(account_ids, account.id), - blocked_by: Account.blocked_by_map(account_ids, account.id), - muting: Account.muting_map(account_ids, account.id), - following: Account.following_map(account_ids, account.id), - domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id), - } - end + relations = relations_map_for_account(account, account_ids, domains) statuses.reject! { |status| filter_from_context?(status, account, relations) } @@ -91,6 +82,18 @@ module StatusThreadingConcern statuses.sort_by! { |status| ids.index(status.id) } end + def relations_map_for_account(account, account_ids, domains) + return {} if account.nil? + + { + blocking: Account.blocking_map(account_ids, account.id), + blocked_by: Account.blocked_by_map(account_ids, account.id), + muting: Account.muting_map(account_ids, account.id), + following: Account.following_map(account_ids, account.id), + domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id), + } + end + def statuses_with_accounts(ids) Status.where(id: ids).includes(:account) end -- cgit From 1e02dc871533de78174b48a6a527f11b0f2bc7ec Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 18 May 2018 02:26:51 +0200 Subject: Add preference to hide following/followers lists (#7532) * Add preference to hide following/followers lists - Public pages - ActivityPub collections (does not return pages but does give total) - REST API (unless it's your own) (does not federate) Fix #6901 * Add preference * Add delegation * Fix issue * Fix issue --- .../api/v1/accounts/follower_accounts_controller.rb | 2 ++ .../api/v1/accounts/following_accounts_controller.rb | 2 ++ app/controllers/follower_accounts_controller.rb | 4 ++++ app/controllers/following_accounts_controller.rb | 4 ++++ app/controllers/settings/preferences_controller.rb | 1 + app/javascript/styles/mastodon/accounts.scss | 13 +++++++++++-- app/javascript/styles/mastodon/footer.scss | 2 +- app/lib/user_settings_decorator.rb | 5 +++++ app/models/account.rb | 1 + app/models/user.rb | 6 +++++- app/views/accounts/_follow_grid.html.haml | 3 ++- app/views/accounts/_follow_grid_hidden.html.haml | 3 +++ app/views/follower_accounts/index.html.haml | 5 ++++- app/views/following_accounts/index.html.haml | 5 ++++- app/views/layouts/public.html.haml | 4 ++-- app/views/settings/preferences/show.html.haml | 3 +++ config/locales/en.yml | 1 + config/locales/simple_form.en.yml | 2 ++ config/settings.yml | 1 + 19 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 app/views/accounts/_follow_grid_hidden.html.haml (limited to 'app/models') diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index c4f600c54..4578cf6ca 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -19,6 +19,8 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController end def load_accounts + return [] if @account.user_hides_network? && current_account.id != @account.id + default_accounts.merge(paginated_follows).to_a end diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 90b1f7fc5..ce2bbda85 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -19,6 +19,8 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController end def load_accounts + return [] if @account.user_hides_network? && current_account.id != @account.id + default_accounts.merge(paginated_follows).to_a end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index ac0d4c54e..99cb3676f 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -6,11 +6,15 @@ class FollowerAccountsController < ApplicationController def index respond_to do |format| format.html do + next if @account.user_hides_network? + follows @relationships = AccountRelationshipsPresenter.new(follows.map(&:account_id), current_user.account_id) if user_signed_in? end format.json do + raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 974d95c8e..03c4b1046 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -6,11 +6,15 @@ class FollowingAccountsController < ApplicationController def index respond_to do |format| format.html do + next if @account.user_hides_network? + follows @relationships = AccountRelationshipsPresenter.new(follows.map(&:target_account_id), current_user.account_id) if user_signed_in? end format.json do + raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 839763138..57793d776 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -44,6 +44,7 @@ class Settings::PreferencesController < ApplicationController :setting_system_font_ui, :setting_noindex, :setting_theme, + :setting_hide_network, notification_emails: %i(follow follow_request reblog favourite mention digest), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index b063ca52d..93aa134cf 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -322,6 +322,15 @@ z-index: 2; position: relative; + &.empty img { + position: absolute; + opacity: 0.2; + height: 200px; + left: 0; + bottom: 0; + pointer-events: none; + } + @media screen and (max-width: 740px) { border-radius: 0; box-shadow: none; @@ -438,8 +447,8 @@ font-size: 14px; font-weight: 500; text-align: center; - padding: 60px 0; - padding-top: 55px; + padding: 130px 0; + padding-top: 125px; margin: 0 auto; cursor: default; } diff --git a/app/javascript/styles/mastodon/footer.scss b/app/javascript/styles/mastodon/footer.scss index ba2a06954..dd3c1b688 100644 --- a/app/javascript/styles/mastodon/footer.scss +++ b/app/javascript/styles/mastodon/footer.scss @@ -4,7 +4,7 @@ font-size: 12px; color: $darker-text-color; - .domain { + .footer__domain { font-weight: 500; a { diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 9260a81bc..a82f8974b 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -28,6 +28,7 @@ class UserSettingsDecorator user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') user.settings['noindex'] = noindex_preference if change?('setting_noindex') user.settings['theme'] = theme_preference if change?('setting_theme') + user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network') end def merged_notification_emails @@ -78,6 +79,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_noindex' end + def hide_network_preference + boolean_cast_setting 'setting_hide_network' + end + def theme_preference settings['setting_theme'] end diff --git a/app/models/account.rb b/app/models/account.rb index 2b3ef5cdc..72e850aa7 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -136,6 +136,7 @@ class Account < ApplicationRecord :moderator?, :staff?, :locale, + :hides_network?, to: :user, prefix: true, allow_nil: true diff --git a/app/models/user.rb b/app/models/user.rb index 21c217e77..cfbae58ed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -86,7 +86,7 @@ class User < ApplicationRecord has_many :session_activations, dependent: :destroy delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, - :reduce_motion, :system_font_ui, :noindex, :theme, :display_sensitive_media, + :reduce_motion, :system_font_ui, :noindex, :theme, :display_sensitive_media, :hide_network, to: :settings, prefix: :setting, allow_nil: false attr_accessor :invite_code @@ -219,6 +219,10 @@ class User < ApplicationRecord settings.notification_emails['digest'] end + def hides_network? + @hides_network ||= settings.hide_network + end + def token_for_app(a) return nil if a.nil? || a.owner != self Doorkeeper::AccessToken diff --git a/app/views/accounts/_follow_grid.html.haml b/app/views/accounts/_follow_grid.html.haml index a6d0ee817..fdcef84be 100644 --- a/app/views/accounts/_follow_grid.html.haml +++ b/app/views/accounts/_follow_grid.html.haml @@ -1,5 +1,6 @@ -.accounts-grid +.accounts-grid{ class: accounts.empty? ? 'empty' : '' } - if accounts.empty? + = image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational' = render partial: 'accounts/nothing_here' - else = render partial: 'accounts/grid_card', collection: accounts, as: :account, cached: !user_signed_in? diff --git a/app/views/accounts/_follow_grid_hidden.html.haml b/app/views/accounts/_follow_grid_hidden.html.haml new file mode 100644 index 000000000..e970350e6 --- /dev/null +++ b/app/views/accounts/_follow_grid_hidden.html.haml @@ -0,0 +1,3 @@ +.accounts-grid.empty + = image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational' + %p.nothing-here= t('accounts.network_hidden') diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml index a24e4ea20..65af81a5b 100644 --- a/app/views/follower_accounts/index.html.haml +++ b/app/views/follower_accounts/index.html.haml @@ -7,4 +7,7 @@ = render 'accounts/header', account: @account -= render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:account) +- if @account.user_hides_network? + = render 'accounts/follow_grid_hidden' +- else + = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:account) diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml index 67f6cfede..8fd95a0b4 100644 --- a/app/views/following_accounts/index.html.haml +++ b/app/views/following_accounts/index.html.haml @@ -7,4 +7,7 @@ = render 'accounts/header', account: @account -= render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:target_account) +- if @account.user_hides_network? + = render 'accounts/follow_grid_hidden' +- else + = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:target_account) diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index be9648561..63cc3c7a7 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -8,9 +8,9 @@ %span.single-user-login = link_to t('auth.login'), new_user_session_path — - %span.domain= link_to site_hostname, about_path + %span.footer__domain= link_to site_hostname, about_path - else - %span.domain= link_to site_hostname, root_path + %span.footer__domain= link_to site_hostname, root_path %span.powered-by != t('generic.powered_by', link: link_to('Mastodon', 'https://joinmastodon.org')) diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index fd66e13fb..d2e866373 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -26,6 +26,9 @@ .fields-group = f.input :setting_noindex, as: :boolean, wrapper: :with_label + .fields-group + = f.input :setting_hide_network, as: :boolean, wrapper: :with_label + %h4= t 'preferences.web' .fields-group diff --git a/config/locales/en.yml b/config/locales/en.yml index c074fa5b0..0257241cf 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -40,6 +40,7 @@ en: following: Following media: Media moved_html: "%{name} has moved to %{new_profile_link}:" + network_hidden: This information is not available nothing_here: There is nothing here! people_followed_by: People whom %{name} follows people_who_follow: People who follow %{name} diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 7d6907ff5..85597e9a7 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -15,6 +15,7 @@ en: note: one: 1 character left other: %{count} characters left + setting_hide_network: Who you follow and who follows you will not be shown on your profile setting_noindex: Affects your public profile and status pages setting_theme: Affects how Mastodon looks when you're logged in from any device. imports: @@ -54,6 +55,7 @@ en: setting_default_sensitive: Always mark media as sensitive setting_delete_modal: Show confirmation dialog before deleting a toot setting_display_sensitive_media: Always show media marked as sensitive + setting_hide_network: Hide your network setting_noindex: Opt-out of search engine indexing setting_reduce_motion: Reduce motion in animations setting_system_font_ui: Use system's default font diff --git a/config/settings.yml b/config/settings.yml index dcf655008..3581d10a2 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -20,6 +20,7 @@ defaults: &defaults timeline_preview: true show_staff_badge: true default_sensitive: false + hide_network: false unfollow_modal: false boost_modal: false delete_modal: true -- cgit From 4b94e9c65efd227c58f8be9b3db6b667d403ed3c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 19 May 2018 14:46:47 +0200 Subject: Improve payload format of Web Push API now that it's open (#7521) > Good lord what is happening in there Previously the contents of the Web Push API payloads closely resembled the structure of JavaScript's [Notification](https://developer.mozilla.org/en-US/docs/Web/API/Notification). But now that the API is open to non-browser apps, and given that there is no required coupling between contents of the payload and a Notification object, here is how I changed the payload: ```json { "access_token": "...", "preferred_locale": "en", "notification_id": "12345", "notification_type": "follow", "title": "So and so followed you", "body": "This is my bio", "icon": "https://example.com/avatar.png" } ``` The title, body and icon attributes are included as a fallback so you can construct a minimal notification if you cannot perform a network request to the API to get more data. --- app/javascript/mastodon/actions/notifications.js | 1 + app/javascript/mastodon/locales/ar.json | 1 + app/javascript/mastodon/locales/bg.json | 1 + app/javascript/mastodon/locales/ca.json | 1 + app/javascript/mastodon/locales/co.json | 1 + app/javascript/mastodon/locales/de.json | 1 + .../mastodon/locales/defaultMessages.json | 6 +- app/javascript/mastodon/locales/el.json | 1 + app/javascript/mastodon/locales/en.json | 1 + app/javascript/mastodon/locales/eo.json | 1 + app/javascript/mastodon/locales/es.json | 1 + app/javascript/mastodon/locales/eu.json | 1 + app/javascript/mastodon/locales/fa.json | 1 + app/javascript/mastodon/locales/fi.json | 1 + app/javascript/mastodon/locales/fr.json | 1 + app/javascript/mastodon/locales/gl.json | 1 + app/javascript/mastodon/locales/he.json | 1 + app/javascript/mastodon/locales/hr.json | 1 + app/javascript/mastodon/locales/hu.json | 1 + app/javascript/mastodon/locales/hy.json | 1 + app/javascript/mastodon/locales/id.json | 1 + app/javascript/mastodon/locales/io.json | 1 + app/javascript/mastodon/locales/it.json | 1 + app/javascript/mastodon/locales/ja.json | 1 + app/javascript/mastodon/locales/ko.json | 1 + app/javascript/mastodon/locales/nl.json | 1 + app/javascript/mastodon/locales/no.json | 1 + app/javascript/mastodon/locales/oc.json | 1 + app/javascript/mastodon/locales/pl.json | 1 + app/javascript/mastodon/locales/pt-BR.json | 1 + app/javascript/mastodon/locales/pt.json | 1 + app/javascript/mastodon/locales/ru.json | 1 + app/javascript/mastodon/locales/sk.json | 1 + app/javascript/mastodon/locales/sl.json | 1 + app/javascript/mastodon/locales/sr-Latn.json | 1 + app/javascript/mastodon/locales/sr.json | 1 + app/javascript/mastodon/locales/sv.json | 1 + app/javascript/mastodon/locales/te.json | 1 + app/javascript/mastodon/locales/th.json | 1 + app/javascript/mastodon/locales/tr.json | 1 + app/javascript/mastodon/locales/uk.json | 1 + app/javascript/mastodon/locales/zh-CN.json | 1 + app/javascript/mastodon/locales/zh-HK.json | 1 + app/javascript/mastodon/locales/zh-TW.json | 1 + .../mastodon/service_worker/web_push_locales.js | 30 ++++ .../service_worker/web_push_notifications.js | 178 +++++++++++++-------- app/models/web/push_subscription.rb | 21 +-- app/serializers/web/notification_serializer.rb | 163 ++----------------- config/locales/ar.yml | 13 -- config/locales/ca.yml | 14 -- config/locales/co.yml | 14 -- config/locales/de.yml | 14 -- config/locales/en.yml | 14 -- config/locales/eo.yml | 14 -- config/locales/es.yml | 14 -- config/locales/fa.yml | 14 -- config/locales/fi.yml | 14 -- config/locales/fr.yml | 14 -- config/locales/gl.yml | 14 -- config/locales/hu.yml | 14 -- config/locales/it.yml | 12 -- config/locales/ja.yml | 14 -- config/locales/ko.yml | 14 -- config/locales/nl.yml | 14 -- config/locales/no.yml | 14 -- config/locales/oc.yml | 14 -- config/locales/pl.yml | 14 -- config/locales/pt-BR.yml | 14 -- config/locales/pt.yml | 14 -- config/locales/ru.yml | 14 -- config/locales/sk.yml | 14 -- config/locales/sr-Latn.yml | 14 -- config/locales/sr.yml | 14 -- config/locales/sv.yml | 14 -- config/locales/zh-CN.yml | 14 -- config/locales/zh-HK.yml | 14 -- 76 files changed, 212 insertions(+), 618 deletions(-) create mode 100644 app/javascript/mastodon/service_worker/web_push_locales.js (limited to 'app/models') diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 641ad0e14..3f95f6667 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -22,6 +22,7 @@ export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, + group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, }); const fetchRelatedRelationships = (dispatch, notifications) => { diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 1199c9535..bb4189514 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "الترقيّات:", "notifications.column_settings.show": "إعرِضها في عمود", "notifications.column_settings.sound": "أصدر صوتا", + "notifications.group": "{count} notifications", "onboarding.done": "تم", "onboarding.next": "التالي", "onboarding.page_five.public_timelines": "تُعرَض في الخيط الزمني المحلي المشاركات العامة المحررة من طرف جميع المسجلين في {domain}. أما في الخيط الزمني الموحد ، فإنه يتم عرض جميع المشاركات العامة المنشورة من طرف جميع الأشخاص المتابَعين من طرف أعضاء {domain}. هذه هي الخيوط الزمنية العامة، وهي طريقة رائعة للتعرف أشخاص جدد.", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index dd747ab3b..aeb4f09da 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Споделяния:", "notifications.column_settings.show": "Покажи в колона", "notifications.column_settings.sound": "Play sound", + "notifications.group": "{count} notifications", "onboarding.done": "Done", "onboarding.next": "Next", "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 68fc4a44c..4bf8ad1c6 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.show": "Mostrar en la columna", "notifications.column_settings.sound": "Reproduïr so", + "notifications.group": "{count} notifications", "onboarding.done": "Fet", "onboarding.next": "Següent", "onboarding.page_five.public_timelines": "La línia de temps local mostra missatges públics de tothom de {domain}. La línia de temps federada mostra els missatges públics de tothom que la gent de {domain} segueix. Aquests són les línies de temps Públiques, una bona manera de descobrir noves persones.", diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json index 9d8d950a8..8d924f973 100644 --- a/app/javascript/mastodon/locales/co.json +++ b/app/javascript/mastodon/locales/co.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Spartere:", "notifications.column_settings.show": "Mustrà indè a colonna", "notifications.column_settings.sound": "Sunà", + "notifications.group": "{count} notifications", "onboarding.done": "Fatta", "onboarding.next": "Siguente", "onboarding.page_five.public_timelines": "A linea pubblica lucale mostra statuti pubblichi da tuttu u mondu nant'à {domain}. A linea pubblica glubale mostra ancu quelli di a ghjente seguitata da l'utilizatori di {domain}. Quesse sò una bona manera d'incuntrà nove parsone.", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index fe5bc4443..84b815d50 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Geteilte Beiträge:", "notifications.column_settings.show": "In der Spalte anzeigen", "notifications.column_settings.sound": "Ton abspielen", + "notifications.group": "{count} notifications", "onboarding.done": "Fertig", "onboarding.next": "Weiter", "onboarding.page_five.public_timelines": "Die lokale Zeitleiste zeigt alle Beiträge von Leuten, die auch auf {domain} sind. Das gesamte bekannte Netz zeigt Beiträge von allen, denen von Leuten auf {domain} gefolgt wird. Zusammen sind sie die öffentlichen Zeitleisten, ein guter Weg, um neue Leute zu finden.", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 49b287928..e2f3ac6dd 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -17,6 +17,10 @@ { "defaultMessage": "{name} mentioned you", "id": "notification.mention" + }, + { + "defaultMessage": "{count} notifications", + "id": "notifications.group" } ], "path": "app/javascript/mastodon/actions/notifications.json" @@ -870,7 +874,7 @@ "id": "compose_form.hashtag_warning" }, { - "defaultMessage": "This toot will only be visible to all the mentioned users.", + "defaultMessage": "This toot will only be sent to all the mentioned users. However, the operators of your instance and any receiving instances may see this message.", "id": "compose_form.direct_message_warning" } ], diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index d1421be76..6e8aa0e24 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", + "notifications.group": "{count} notifications", "onboarding.done": "Done", "onboarding.next": "Next", "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 478caaaf8..11589d797 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", + "notifications.group": "{count} notifications", "onboarding.done": "Done", "onboarding.next": "Next", "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index d5ddc23d4..479fc6c6e 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Diskonigoj:", "notifications.column_settings.show": "Montri en kolumno", "notifications.column_settings.sound": "Eligi sonon", + "notifications.group": "{count} notifications", "onboarding.done": "Farita", "onboarding.next": "Sekva", "onboarding.page_five.public_timelines": "La loka tempolinio montras publikajn mesaĝojn de ĉiuj en {domain}. La fratara tempolinio montras publikajn mesaĝojn de ĉiuj, kiuj estas sekvataj de homoj en {domain}. Tio estas la publikaj tempolinioj, kio estas bona maniero por malkovri novajn homojn.", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 8aac2d77d..233e46561 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Retoots:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Reproducir sonido", + "notifications.group": "{count} notifications", "onboarding.done": "Listo", "onboarding.next": "Siguiente", "onboarding.page_five.public_timelines": "La línea de tiempo local muestra toots públicos de todos en {domain}. La línea de tiempo federada muestra toots públicos de cualquiera a quien la gente de {domain} siga. Estas son las líneas de tiempo públicas, una buena forma de conocer gente nueva.", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index e927547e3..773d69287 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", + "notifications.group": "{count} notifications", "onboarding.done": "Done", "onboarding.next": "Next", "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index e19b1cd85..957046c27 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "بازبوق‌ها:", "notifications.column_settings.show": "نمایش در ستون", "notifications.column_settings.sound": "پخش صدا", + "notifications.group": "{count} notifications", "onboarding.done": "پایان", "onboarding.next": "بعدی", "onboarding.page_five.public_timelines": "نوشته‌های محلی یعنی نوشته‌های همهٔ کاربران {domain}. نوشته‌های همه‌جا یعنی نوشته‌های همهٔ کسانی که کاربران {domain} آن‌ها را پی می‌گیرند. این فهرست‌های عمومی راه خوبی برای یافتن کاربران تازه هستند.", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index cfe9c4f33..55b9da70a 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Buustit:", "notifications.column_settings.show": "Näytä sarakkeessa", "notifications.column_settings.sound": "Äänimerkki", + "notifications.group": "{count} notifications", "onboarding.done": "Valmis", "onboarding.next": "Seuraava", "onboarding.page_five.public_timelines": "Paikallisella aikajanalla näytetään instanssin {domain} kaikkien käyttäjien julkiset julkaisut. Yleisellä aikajanalla näytetään kaikkien instanssin {domain} käyttäjien seuraamien käyttäjien julkiset julkaisut. Nämä julkiset aikajanat ovat loistavia paikkoja löytää uusia ihmisiä.", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index c4a41583c..44464cbc3 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Partages :", "notifications.column_settings.show": "Afficher dans la colonne", "notifications.column_settings.sound": "Émettre un son", + "notifications.group": "{count} notifications", "onboarding.done": "Effectué", "onboarding.next": "Suivant", "onboarding.page_five.public_timelines": "Le fil public global affiche les messages de toutes les personnes suivies par les membres de {domain}. Le fil public local est identique, mais se limite aux membres de {domain}.", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 1eaaebc53..aa7ba0d1e 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Promocións:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Reproducir son", + "notifications.group": "{count} notifications", "onboarding.done": "Feito", "onboarding.next": "Seguinte", "onboarding.page_five.public_timelines": "A liña de tempo local mostra as publicacións públicas de todos en {domain}. A liña de tempo federada mostra as publicacións públicas de todos os que as persoas en {domain} seguen. Estas son as Liñas de tempo públicas, unha boa forma de descubrir novas persoas.", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index a4bcba8ff..324ea725e 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "הדהודים:", "notifications.column_settings.show": "הצגה בטור", "notifications.column_settings.sound": "שמע מופעל", + "notifications.group": "{count} notifications", "onboarding.done": "יציאה", "onboarding.next": "הלאה", "onboarding.page_five.public_timelines": "ציר הזמן המקומי מראה הודעות פומביות מכל באי קהילת {domain}. ציר הזמן העולמי מראה הודעות פומביות מאת כי מי שבאי קהילת {domain} עוקבים אחריו. אלו צירי הזמן הפומביים, דרך נהדרת לגלות אנשים חדשים.", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index bd1dfac4c..0216d0837 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Boostovi:", "notifications.column_settings.show": "Prikaži u stupcu", "notifications.column_settings.sound": "Sviraj zvuk", + "notifications.group": "{count} notifications", "onboarding.done": "Učinjeno", "onboarding.next": "Sljedeće", "onboarding.page_five.public_timelines": "Lokalni timeline prikazuje javne postove sviju od svakog na {domain}. Federalni timeline prikazuje javne postove svakog koga ljudi na {domain} slijede. To su Javni Timelineovi, sjajan način za otkriti nove ljude.", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 5059a45d4..795c9b7cd 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Rebloggolások:", "notifications.column_settings.show": "Oszlopban mutatás", "notifications.column_settings.sound": "Hang lejátszása", + "notifications.group": "{count} notifications", "onboarding.done": "Befejezve", "onboarding.next": "Következő", "onboarding.page_five.public_timelines": "A helyi idővonal mindenkinek a publikus posztját mutatja a(z) {domain}-n. A federált idővonal mindenki publikus posztját mutatja akit {domain} felhasználói követnek. Ezek a publikus idővonalak, nagyszerű mód új emberek megismerésére.", diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json index 7fe723892..2c7496d89 100644 --- a/app/javascript/mastodon/locales/hy.json +++ b/app/javascript/mastodon/locales/hy.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Տարածածներից՝", "notifications.column_settings.show": "Ցուցադրել սյունում", "notifications.column_settings.sound": "Ձայն հանել", + "notifications.group": "{count} notifications", "onboarding.done": "Պատրաստ է", "onboarding.next": "Հաջորդ", "onboarding.page_five.public_timelines": "Տեղական հոսքը ցույց է տալիս {domain} տիրույթից բոլորի հրապարակային թթերը։ Դաշնային հոսքը ցույց է տալիս հրապարակային թթերը բոլորից, ում {domain} տիրույթի մարդիկ հետեւում են։ Սրանք Հրապարակային հոսքերն են՝ նոր մարդկանց բացահայտելու հրաշալի միջոց։", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 07b26a0a5..2fcd5c482 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Boost:", "notifications.column_settings.show": "Tampilkan dalam kolom", "notifications.column_settings.sound": "Mainkan suara", + "notifications.group": "{count} notifications", "onboarding.done": "Selesei", "onboarding.next": "Selanjutnya", "onboarding.page_five.public_timelines": "Linimasa lokal menampilkan semua postingan publik dari semua orang di {domain}. Linimasa gabungan menampilkan postingan publik dari semua orang yang diikuti oleh {domain}. Ini semua adalah Linimasa Publik, cara terbaik untuk bertemu orang lain.", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index a8ab72339..9d429bda8 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Repeti:", "notifications.column_settings.show": "Montrar en kolumno", "notifications.column_settings.sound": "Plear sono", + "notifications.group": "{count} notifications", "onboarding.done": "Done", "onboarding.next": "Next", "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 8bd652864..64baf5440 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Post condivisi:", "notifications.column_settings.show": "Mostra in colonna", "notifications.column_settings.sound": "Riproduci suono", + "notifications.group": "{count} notifications", "onboarding.done": "Fatto", "onboarding.next": "Prossimo", "onboarding.page_five.public_timelines": "La timeline locale mostra i post pubblici di tutti gli utenti di {domain}. La timeline federata mostra i post pubblici di tutti gli utenti seguiti da quelli di {domain}. Queste sono le timeline pubbliche, che vi danno grandi possibilità di scoprire nuovi utenti.", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 8be04cf73..95f8438d5 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "ブースト:", "notifications.column_settings.show": "カラムに表示", "notifications.column_settings.sound": "通知音を再生", + "notifications.group": "{count} notifications", "onboarding.done": "完了", "onboarding.next": "次へ", "onboarding.page_five.public_timelines": "連合タイムラインでは{domain}の人がフォローしているMastodon全体での公開投稿を表示します。同じくローカルタイムラインでは{domain}のみの公開投稿を表示します。", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index fc13a5b5b..bb6f3558c 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "부스트:", "notifications.column_settings.show": "컬럼에 표시", "notifications.column_settings.sound": "효과음 재생", + "notifications.group": "{count} notifications", "onboarding.done": "완료", "onboarding.next": "다음", "onboarding.page_five.public_timelines": "연합 타임라인에서는 {domain}의 사람들이 팔로우 중인 Mastodon 전체 인스턴스의 공개 포스트를 표시합니다. 로컬 타임라인에서는 {domain} 만의 공개 포스트를 표시합니다.", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 047583ce2..63a452377 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "In kolom tonen", "notifications.column_settings.sound": "Geluid afspelen", + "notifications.group": "{count} notifications", "onboarding.done": "Klaar", "onboarding.next": "Volgende", "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodonservers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te leren kennen.", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 05dae0630..1496afb83 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Fremhevet:", "notifications.column_settings.show": "Vis i kolonne", "notifications.column_settings.sound": "Spill lyd", + "notifications.group": "{count} notifications", "onboarding.done": "Ferdig", "onboarding.next": "Neste", "onboarding.page_five.public_timelines": "Den lokale tidslinjen viser offentlige poster fra alle på {domain}. Felles tidslinje viser offentlige poster fra alle som brukere på {domain} følger. Dette er de offentlige tidslinjene, et fint sted å oppdage nye brukere.", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 615af998f..3ae29129f 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Partatges :", "notifications.column_settings.show": "Mostrar dins la colomna", "notifications.column_settings.sound": "Emetre un son", + "notifications.group": "{count} notifications", "onboarding.done": "Sortir", "onboarding.next": "Seguent", "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 0df36f969..be5693bc9 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Podbicia:", "notifications.column_settings.show": "Pokaż w kolumnie", "notifications.column_settings.sound": "Odtwarzaj dźwięk", + "notifications.group": "{count} notifications", "onboarding.done": "Gotowe", "onboarding.next": "Dalej", "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Globalna oś czasu wyświetla publiczne wpisy śledzonych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 2360c41e7..26ae5ef30 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Compartilhamento:", "notifications.column_settings.show": "Mostrar nas colunas", "notifications.column_settings.sound": "Reproduzir som", + "notifications.group": "{count} notifications", "onboarding.done": "Pronto", "onboarding.next": "Próximo", "onboarding.page_five.public_timelines": "A timeline local mostra postagens públicas de todos os usuários no {domain}. A timeline federada mostra todas as postagens de todas as pessoas que pessoas no {domain} seguem. Estas são as timelines públicas, uma ótima maneira de conhecer novas pessoas.", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index 3ac92dd57..02ffc825c 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Partilhas:", "notifications.column_settings.show": "Mostrar nas colunas", "notifications.column_settings.sound": "Reproduzir som", + "notifications.group": "{count} notifications", "onboarding.done": "Pronto", "onboarding.next": "Próximo", "onboarding.page_five.public_timelines": "A timeline local mostra as publicações de todos os utilizadores em {domain}. A timeline global mostra as publicações de todas as pessoas que pessoas em {domain} seguem. Estas são as timelines públicas, uma óptima forma de conhecer novas pessoas.", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 5cf983b83..ca8574578 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Продвижения:", "notifications.column_settings.show": "Показывать в колонке", "notifications.column_settings.sound": "Проигрывать звук", + "notifications.group": "{count} notifications", "onboarding.done": "Готово", "onboarding.next": "Далее", "onboarding.page_five.public_timelines": "Локальная лента показывает публичные посты всех пользователей {domain}. Глобальная лента показывает публичные посты всех людей, на которых подписаны пользователи {domain}. Это - публичные ленты, отличный способ найти новые знакомства.", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 4f7969bd3..cf9ead5e7 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Boosty:", "notifications.column_settings.show": "Zobraziť v stĺpci", "notifications.column_settings.sound": "Prehrať zvuk", + "notifications.group": "{count} notifications", "onboarding.done": "Koniec", "onboarding.next": "Ďalej", "onboarding.page_five.public_timelines": "Lokálna časová os zobrazuje verejné správy od všetkých na {domain}. Federovaná časová os zobrazuje verejné správy od všetkých tých, čo následujú užívatrľov {domain} z iných serverov. Tieto sú takzvané Verejné Časové Osi, výborná možnosť ako nájsť a spoznať nových ľudí.", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 69b416258..dbad35c34 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", + "notifications.group": "{count} notifications", "onboarding.done": "Done", "onboarding.next": "Next", "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index 490b3a51a..bc54a0865 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Podrški:", "notifications.column_settings.show": "Prikaži u koloni", "notifications.column_settings.sound": "Puštaj zvuk", + "notifications.group": "{count} notifications", "onboarding.done": "Gotovo", "onboarding.next": "Sledeće", "onboarding.page_five.public_timelines": "Lokalna lajna prikazuje sve javne statuse od svih na domenu {domain}. Federisana lajna prikazuje javne statuse od svih ljudi koje prate korisnici sa domena {domain}. Ovo su javne lajne, sjajan način da otkrijete nove ljude.", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index b331f9f48..5cdeb24fc 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Подршки:", "notifications.column_settings.show": "Прикажи у колони", "notifications.column_settings.sound": "Пуштај звук", + "notifications.group": "{count} notifications", "onboarding.done": "Готово", "onboarding.next": "Следеће", "onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Федерисана лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 36b200764..579921046 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Knuffar:", "notifications.column_settings.show": "Visa i kolumnen", "notifications.column_settings.sound": "Spela upp ljud", + "notifications.group": "{count} notifications", "onboarding.done": "Klart", "onboarding.next": "Nästa", "onboarding.page_five.public_timelines": "Den lokala tidslinjen visar offentliga inlägg från alla på {domain}. Den förenade tidslinjen visar offentliga inlägg från alla personer på {domain} som följer. Dom här offentliga tidslinjerna är ett bra sätt att upptäcka nya människor.", diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json index 038ac6abb..55415da81 100644 --- a/app/javascript/mastodon/locales/te.json +++ b/app/javascript/mastodon/locales/te.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", + "notifications.group": "{count} notifications", "onboarding.done": "Done", "onboarding.next": "Next", "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 8d24395af..243941caf 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", + "notifications.group": "{count} notifications", "onboarding.done": "Done", "onboarding.next": "Next", "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index a377e7b74..c6d27b609 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Boost’lar:", "notifications.column_settings.show": "Bildirimlerde göster", "notifications.column_settings.sound": "Ses çal", + "notifications.group": "{count} notifications", "onboarding.done": "Tamam", "onboarding.next": "Sıradaki", "onboarding.page_five.public_timelines": "Yerel zaman tüneli, bu sunucudaki herkesten gelen gönderileri gösterir.Federe zaman tüneli, kullanıcıların diğer sunuculardan takip ettiği kişilerin herkese açık gönderilerini gösterir. Bunlar herkese açık zaman tünelleridir ve yeni insanlarla tanışmak için harika yerlerdir. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new ", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 613d1a00d..2e174e186 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "Передмухи:", "notifications.column_settings.show": "Показати в колонці", "notifications.column_settings.sound": "Відтворювати звук", + "notifications.group": "{count} notifications", "onboarding.done": "Готово", "onboarding.next": "Далі", "onboarding.page_five.public_timelines": "Локальна стрічка показує публічні пости усіх користувачів {domain}. Глобальна стрічка показує публічні пости усіх людей, на яких підписані користувачі {domain}. Це публичні стрічки, відмінний спосіб знайти нових людей.", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 073dbe6cb..254d078eb 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "当有人转嘟了你的嘟文时:", "notifications.column_settings.show": "在通知栏显示", "notifications.column_settings.sound": "播放音效", + "notifications.group": "{count} notifications", "onboarding.done": "出发!", "onboarding.next": "下一步", "onboarding.page_five.public_timelines": "“本站时间轴”显示的是由本站({domain})用户发布的所有公开嘟文。“跨站公共时间轴”显示的的是由本站用户关注对象所发布的所有公开嘟文。这些就是寻人好去处的公共时间轴啦。", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 9334b5d20..9fb8aa2f9 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "轉推你的文章:", "notifications.column_settings.show": "在通知欄顯示", "notifications.column_settings.sound": "播放音效", + "notifications.group": "{count} notifications", "onboarding.done": "開始使用", "onboarding.next": "繼續", "onboarding.page_five.public_timelines": "「本站時間軸」顯示在 {domain} 各用戶的公開文章。「跨站時間軸」顯示在 {domain} 各人關注的所有用戶(包括其他服務站)的公開文章。這些都是「公共時間軸」,是認識新朋友的好地方。", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 0f4d04947..ae917a5ac 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -186,6 +186,7 @@ "notifications.column_settings.reblog": "轉推:", "notifications.column_settings.show": "顯示在欄位中", "notifications.column_settings.sound": "播放音效", + "notifications.group": "{count} notifications", "onboarding.done": "完成", "onboarding.next": "下一步", "onboarding.page_five.public_timelines": "本地時間軸顯示 {domain} 上所有人的公開貼文。聯盟時間軸顯示 {domain} 上所有人關注的公開貼文。這就是公開時間軸,發現新朋友的好地方。", diff --git a/app/javascript/mastodon/service_worker/web_push_locales.js b/app/javascript/mastodon/service_worker/web_push_locales.js new file mode 100644 index 000000000..ce96ae297 --- /dev/null +++ b/app/javascript/mastodon/service_worker/web_push_locales.js @@ -0,0 +1,30 @@ +/* @preval */ + +const fs = require('fs'); +const path = require('path'); + +const filtered = {}; +const filenames = fs.readdirSync(path.resolve(__dirname, '../locales')); + +filenames.forEach(filename => { + if (!filename.match(/\.json$/) || filename.match(/defaultMessages|whitelist/)) return; + + const content = fs.readFileSync(path.resolve(__dirname, `../locales/${filename}`), 'utf-8'); + const full = JSON.parse(content); + const locale = filename.split('.')[0]; + + filtered[locale] = { + 'notification.favourite': full['notification.favourite'] || '', + 'notification.follow': full['notification.follow'] || '', + 'notification.mention': full['notification.mention'] || '', + 'notification.reblog': full['notification.reblog'] || '', + + 'status.show_more': full['status.show_more'] || '', + 'status.reblog': full['status.reblog'] || '', + 'status.favourite': full['status.favourite'] || '', + + 'notifications.group': full['notifications.group'] || '', + }; +}); + +module.exports = JSON.parse(JSON.stringify(filtered)); diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js index f63cff335..16eaefcbb 100644 --- a/app/javascript/mastodon/service_worker/web_push_notifications.js +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -1,36 +1,32 @@ +import IntlMessageFormat from 'intl-messageformat'; +import locales from './web_push_locales'; + const MAX_NOTIFICATIONS = 5; const GROUP_TAG = 'tag'; -// Avoid loading intl-messageformat and dealing with locales in the ServiceWorker -const formatGroupTitle = (message, count) => message.replace('%{count}', count); - const notify = options => self.registration.getNotifications().then(notifications => { - if (notifications.length === MAX_NOTIFICATIONS) { - // Reached the maximum number of notifications, proceed with grouping + if (notifications.length >= MAX_NOTIFICATIONS) { // Reached the maximum number of notifications, proceed with grouping const group = { - title: formatGroupTitle(options.data.message, notifications.length + 1), - body: notifications - .sort((n1, n2) => n1.timestamp < n2.timestamp) - .map(notification => notification.title).join('\n'), + title: formatMessage('notifications.group', options.data.preferred_locale, { count: notifications.length + 1 }), + body: notifications.sort((n1, n2) => n1.timestamp < n2.timestamp).map(notification => notification.title).join('\n'), badge: '/badge.png', icon: '/android-chrome-192x192.png', tag: GROUP_TAG, data: { url: (new URL('/web/notifications', self.location)).href, count: notifications.length + 1, - message: options.data.message, + preferred_locale: options.data.preferred_locale, }, }; notifications.forEach(notification => notification.close()); return self.registration.showNotification(group.title, group); - } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { - // Already grouped, proceed with appending the notification to the group - const group = cloneNotification(notifications[0]); + } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { // Already grouped, proceed with appending the notification to the group + const group = { ...notifications[0] }; - group.title = formatGroupTitle(group.data.message, group.data.count + 1); + group.title = formatMessage('notifications.group', options.data.preferred_locale, { count: group.data.count + 1 }); group.body = `${options.title}\n${group.body}`; group.data = { ...group.data, count: group.data.count + 1 }; @@ -40,57 +36,87 @@ const notify = options => return self.registration.showNotification(options.title, options); }); -const handlePush = (event) => { - const options = event.data.json(); - - options.body = options.data.nsfw || options.data.content; - options.dir = options.data.dir; - options.image = options.image || undefined; // Null results in a network request (404) - options.timestamp = options.timestamp && new Date(options.timestamp); - - const expandAction = options.data.actions.find(action => action.todo === 'expand'); +const fetchFromApi = (path, method, accessToken) => { + const url = (new URL(path, self.location)).href; - if (expandAction) { - options.actions = [expandAction]; - options.hiddenActions = options.data.actions.filter(action => action !== expandAction); - options.data.hiddenImage = options.image; - options.image = undefined; - } else { - options.actions = options.data.actions; - } + return fetch(url, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, - event.waitUntil(notify(options)); + method: method, + credentials: 'include', + }).then(res => { + if (res.ok) { + return res; + } else { + throw new Error(res.status); + } + }).then(res => res.json()); }; -const cloneNotification = (notification) => { - const clone = { }; +const formatMessage = (messageId, locale, values = {}) => + (new IntlMessageFormat(locales[locale][messageId], locale)).format(values); - for(var k in notification) { - clone[k] = notification[k]; - } +const handlePush = (event) => { + const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json(); + + // Placeholder until more information can be loaded + event.waitUntil( + notify({ + title, + body, + icon, + tag: notification_id, + timestamp: new Date(), + badge: '/badge.png', + data: { access_token, preferred_locale, url: '/web/notifications' }, + }).then(() => fetchFromApi(`/api/v1/notifications/${notification_id}`, 'get', access_token)).then(notification => { + const options = {}; + + options.title = formatMessage(`notification.${notification.type}`, preferred_locale, { name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); + options.body = notification.status && notification.status.content; + options.icon = notification.account.avatar_static; + options.timestamp = notification.created_at && new Date(notification.created_at); + options.tag = notification.id; + options.badge = '/badge.png'; + options.image = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined; + options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/statuses/${notification.status.id}` : `/web/accounts/${notification.account.id}` }; + + if (notification.status && notification.status.sensitive) { + options.data.hiddenBody = notification.status.content; + options.data.hiddenImage = notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url; + + options.body = undefined; + options.image = undefined; + options.actions = [actionExpand(preferred_locale)]; + } else if (notification.status) { + options.actions = [actionReblog(preferred_locale), actionFavourite(preferred_locale)]; + } - return clone; + return notify(options); + }) + ); }; -const expandNotification = (notification) => { - const nextNotification = cloneNotification(notification); +const actionExpand = preferred_locale => ({ + action: 'expand', + icon: '/web-push-icon_expand.png', + title: formatMessage('status.show_more', preferred_locale), +}); - nextNotification.body = notification.data.content; - nextNotification.image = notification.data.hiddenImage; - nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand'); +const actionReblog = preferred_locale => ({ + action: 'reblog', + icon: '/web-push-icon_reblog.png', + title: formatMessage('status.reblog', preferred_locale), +}); - return self.registration.showNotification(nextNotification.title, nextNotification); -}; - -const makeRequest = (notification, action) => - fetch(action.action, { - headers: { - 'Authorization': `Bearer ${notification.data.access_token}`, - 'Content-Type': 'application/json', - }, - method: action.method, - credentials: 'include', - }); +const actionFavourite = preferred_locale => ({ + action: 'favourite', + icon: '/web-push-icon_favourite.png', + title: formatMessage('status.favourite', preferred_locale), +}); const findBestClient = clients => { const focusedClient = clients.find(client => client.focused); @@ -99,6 +125,24 @@ const findBestClient = clients => { return focusedClient || visibleClient || clients[0]; }; +const expandNotification = notification => { + const newNotification = { ...notification }; + + newNotification.body = newNotification.data.hiddenBody; + newNotification.image = newNotification.data.hiddenImage; + newNotification.actions = [actionReblog(notification.data.preferred_locale), actionFavourite(notification.data.preferred_locale)]; + + return self.registration.showNotification(newNotification.title, newNotification); +}; + +const removeActionFromNotification = (notification, action) => { + const newNotification = { ...notification }; + + newNotification.actions = newNotification.actions.filter(item => item.action !== action); + + return self.registration.showNotification(newNotification.title, newNotification); +}; + const openUrl = url => self.clients.matchAll({ type: 'window' }).then(clientList => { if (clientList.length !== 0) { @@ -124,27 +168,19 @@ const openUrl = url => return self.clients.openWindow(url); }); -const removeActionFromNotification = (notification, action) => { - const actions = notification.actions.filter(act => act.action !== action.action); - const nextNotification = cloneNotification(notification); - - nextNotification.actions = actions; - - return self.registration.showNotification(nextNotification.title, nextNotification); -}; - const handleNotificationClick = (event) => { const reactToNotificationClick = new Promise((resolve, reject) => { if (event.action) { - const action = event.notification.data.actions.find(({ action }) => action === event.action); - - if (action.todo === 'expand') { + if (event.action === 'expand') { resolve(expandNotification(event.notification)); - } else if (action.todo === 'request') { - resolve(makeRequest(event.notification, action) - .then(() => removeActionFromNotification(event.notification, action))); + } else if (event.action === 'reblog') { + const { data } = event.notification; + resolve(fetchFromApi(`/api/v1/statuses/${data.id}/reblog`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'reblog'))); + } else if (event.action === 'favourite') { + const { data } = event.notification; + resolve(fetchFromApi(`/api/v1/statuses/${data.id}/favourite`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'favourite'))); } else { - reject(`Unknown action: ${action.todo}`); + reject(`Unknown action: ${event.action}`); } } else { event.notification.close(); diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index df549c6d3..7da3428fe 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -21,8 +21,8 @@ class Web::PushSubscription < ApplicationRecord has_one :session_activation def push(notification) - I18n.with_locale(associated_user.locale || I18n.default_locale) do - push_payload(message_from(notification), 48.hours.seconds) + I18n.with_locale(associated_user&.locale || I18n.default_locale) do + push_payload(payload_for_notification(notification), 48.hours.seconds) end end @@ -46,16 +46,13 @@ class Web::PushSubscription < ApplicationRecord @associated_access_token = if access_token_id.nil? find_or_create_access_token.token else - access_token + access_token.token end end private def push_payload(message, ttl = 5.minutes.seconds) - # TODO: Make sure that the payload does not - # exceed 4KB - Webpush::PayloadTooLarge - Webpush.payload_send( message: Oj.dump(message), endpoint: endpoint, @@ -70,16 +67,20 @@ class Web::PushSubscription < ApplicationRecord ) end - def message_from(notification) - serializable_resource = ActiveModelSerializers::SerializableResource.new(notification, serializer: Web::NotificationSerializer, scope: self, scope_name: :current_push_subscription) - serializable_resource.as_json + def payload_for_notification(notification) + ActiveModelSerializers::SerializableResource.new( + notification, + serializer: Web::NotificationSerializer, + scope: self, + scope_name: :current_push_subscription + ).as_json end def find_or_create_access_token Doorkeeper::AccessToken.find_or_create_for( Doorkeeper::Application.find_by(superapp: true), session_activation.user_id, - Doorkeeper::OAuth::Scopes.from_string('read write follow'), + Doorkeeper::OAuth::Scopes.from_string('read write follow push'), Doorkeeper.configuration.access_token_expires_in, Doorkeeper.configuration.refresh_token_enabled? ) diff --git a/app/serializers/web/notification_serializer.rb b/app/serializers/web/notification_serializer.rb index 31c703832..f3c4ffc47 100644 --- a/app/serializers/web/notification_serializer.rb +++ b/app/serializers/web/notification_serializer.rb @@ -2,168 +2,37 @@ class Web::NotificationSerializer < ActiveModel::Serializer include RoutingHelper - include StreamEntriesHelper + include ActionView::Helpers::TextHelper + include ActionView::Helpers::SanitizeHelper - class DataSerializer < ActiveModel::Serializer - include RoutingHelper - include StreamEntriesHelper - include ActionView::Helpers::SanitizeHelper + attributes :access_token, :preferred_locale, :notification_id, + :notification_type, :icon, :title, :body - attributes :content, :nsfw, :url, :actions, - :access_token, :message, :dir - - def content - decoder.decode(strip_tags(body)) - end - - def dir - rtl?(body) ? 'rtl' : 'ltr' - end - - def nsfw - return if object.target_status.nil? - object.target_status.spoiler_text.presence - end - - def url - case object.type - when :mention - web_url("statuses/#{object.target_status.id}") - when :follow - web_url("accounts/#{object.from_account.id}") - when :favourite - web_url("statuses/#{object.target_status.id}") - when :reblog - web_url("statuses/#{object.target_status.id}") - end - end - - def actions - return @actions if defined?(@actions) - - @actions = [] - - if object.type == :mention - @actions << expand_action if collapsed? - @actions << favourite_action - @actions << reblog_action if rebloggable? - end - - @actions - end - - def access_token - return if actions.empty? - current_push_subscription.associated_access_token - end - - def message - I18n.t('push_notifications.group.title') - end - - private - - def body - case object.type - when :mention - object.target_status.text - when :follow - object.from_account.note - when :favourite - object.target_status.text - when :reblog - object.target_status.text - end - end - - def decoder - @decoder ||= HTMLEntities.new - end - - def expand_action - { - title: I18n.t('push_notifications.mention.action_expand'), - icon: full_asset_url('web-push-icon_expand.png', skip_pipeline: true), - todo: 'expand', - action: 'expand', - } - end - - def favourite_action - { - title: I18n.t('push_notifications.mention.action_favourite'), - icon: full_asset_url('web-push-icon_favourite.png', skip_pipeline: true), - todo: 'request', - method: 'POST', - action: "/api/v1/statuses/#{object.target_status.id}/favourite", - } - end - - def reblog_action - { - title: I18n.t('push_notifications.mention.action_boost'), - icon: full_asset_url('web-push-icon_reblog.png', skip_pipeline: true), - todo: 'request', - method: 'POST', - action: "/api/v1/statuses/#{object.target_status.id}/reblog", - } - end - - def collapsed? - !object.target_status.nil? && (object.target_status.sensitive? || object.target_status.spoiler_text.present?) - end - - def rebloggable? - !object.target_status.nil? && !object.target_status.hidden? - end + def access_token + current_push_subscription.associated_access_token end - attributes :title, :image, :badge, :tag, - :timestamp, :icon - - has_one :data, serializer: DataSerializer - - def title - case object.type - when :mention - I18n.t('push_notifications.mention.title', name: name) - when :follow - I18n.t('push_notifications.follow.title', name: name) - when :favourite - I18n.t('push_notifications.favourite.title', name: name) - when :reblog - I18n.t('push_notifications.reblog.title', name: name) - end + def preferred_locale + current_push_subscription.associated_user&.locale || I18n.default_locale end - def image - return if object.target_status.nil? || object.target_status.media_attachments.empty? - full_asset_url(object.target_status.media_attachments.first.file.url(:small)) - end - - def badge - full_asset_url('badge.png', skip_pipeline: true) - end - - def tag + def notification_id object.id end - def timestamp - object.created_at + def notification_type + object.type end def icon - object.from_account.avatar_static_url + full_asset_url(object.from_account.avatar_static_url) end - def data - object + def title + I18n.t("notification_mailer.#{object.type}.subject", name: object.from_account.display_name.presence || object.from_account.username) end - private - - def name - display_name(object.from_account) + def body + truncate(strip_tags(object.target_status&.spoiler_text&.presence || object.target_status&.text || object.from_account.note), length: 140) end end diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 194d91cb0..f187bd6ac 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -576,19 +576,6 @@ ar: other: إعدادات أخرى publishing: النشر web: الويب - push_notifications: - favourite: - title: أعجب %{name} بمنشورك - follow: - title: "%{name} من متتبعيك الآن" - group: - title: "%{count} إخطارات" - mention: - action_boost: ترقية - action_expand: عرض المزيد - title: أشار إليك %{name} - reblog: - title: قام %{name} بترقية منشورك remote_follow: acct: قم بإدخال عنوان حسابك username@domain الذي من خلاله تود المتابعة missing_resource: تعذر العثور على رابط التحويل المطلوب الخاص بحسابك diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 97b52cfa5..4e051bc36 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -594,20 +594,6 @@ ca: other: Altre publishing: Publicació web: Web - push_notifications: - favourite: - title: "%{name} ha marcat com a preferit el teu estat" - follow: - title: "%{name} ara et segueix" - group: - title: "%{count} notificacions" - mention: - action_boost: Retooteja - action_expand: Mostra'n més - action_favourite: Preferit - title: "%{name} t'ha mencionat" - reblog: - title: "%{name} t'ha retootejat" remote_follow: acct: Escriu el teu usuari@domini des del qual vols seguir missing_resource: No s'ha pogut trobar la URL de redirecció necessaria per al compte diff --git a/config/locales/co.yml b/config/locales/co.yml index 637491d6b..32661b2c5 100644 --- a/config/locales/co.yml +++ b/config/locales/co.yml @@ -593,20 +593,6 @@ co: other: Altre publishing: Pubblicazione web: Web - push_notifications: - favourite: - title: "%{name} hà aghjuntu u vostru statutu à i so favuriti" - follow: - title: "%{name} vi seguita" - group: - title: "%{count} nutificazioni" - mention: - action_boost: Sparte - action_expand: Vede di più - action_favourite: Aghjunghje à i favuriti - title: "%{name} v’hà mintuvatu·a" - reblog: - title: "%{name} hà spartutu u vostru statutu" remote_follow: acct: Entrate u vostru cugnome@istanza da induve vulete siguità stu contu missing_resource: Ùn avemu pussutu à truvà l’indirizzu di ridirezzione diff --git a/config/locales/de.yml b/config/locales/de.yml index c0254f73f..75c8b11cf 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -594,20 +594,6 @@ de: other: Weiteres publishing: Beiträge web: Web - push_notifications: - favourite: - title: "%{name} hat deinen Beitrag favorisiert" - follow: - title: "%{name} folgt dir nun" - group: - title: "%{count} Benachrichtigungen" - mention: - action_boost: Teilen - action_expand: Mehr anzeigen - action_favourite: Favorisieren - title: "%{name} hat dich erwähnt" - reblog: - title: "%{name} hat deinen Beitrag geteilt" remote_follow: acct: Profilname@Domain, von wo aus du dieser Person folgen möchtest missing_resource: Die erforderliche Weiterleitungs-URL für dein Konto konnte nicht gefunden werden diff --git a/config/locales/en.yml b/config/locales/en.yml index 0257241cf..b9429dbee 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -595,20 +595,6 @@ en: other: Other publishing: Publishing web: Web - push_notifications: - favourite: - title: "%{name} favourited your status" - follow: - title: "%{name} is now following you" - group: - title: "%{count} notifications" - mention: - action_boost: Boost - action_expand: Show more - action_favourite: Favourite - title: "%{name} mentioned you" - reblog: - title: "%{name} boosted your status" remote_follow: acct: Enter your username@domain you want to follow from missing_resource: Could not find the required redirect URL for your account diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 0903e3517..892070914 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -596,20 +596,6 @@ eo: other: Aliaj aferoj publishing: Publikado web: Reto - push_notifications: - favourite: - title: "%{name} stelumis vian mesaĝon" - follow: - title: "%{name} eksekvis vin" - group: - title: "%{count} sciigoj" - mention: - action_boost: Diskonigi - action_expand: Montri pli - action_favourite: Stelumi - title: "%{name} menciis vin" - reblog: - title: "%{name} diskonigis vian mesaĝon" remote_follow: acct: Enmetu vian uzantnomo@domajno de kie vi volas sekvi missing_resource: La URL de plusendado ne estis trovita diff --git a/config/locales/es.yml b/config/locales/es.yml index 593716b16..3e22e2643 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -593,20 +593,6 @@ es: other: Otros publishing: Publicación web: Web - push_notifications: - favourite: - title: "%A {name} le gustó tu estado" - follow: - title: "%{name} te ha empezado a seguir" - group: - title: "%{count} notificaciones" - mention: - action_boost: Retoot - action_expand: Mostrar más - action_favourite: Me Gusta - title: "%{name} te mencionó" - reblog: - title: "%{name} boosteó tu estado" remote_follow: acct: Ingesa tu usuario@dominio desde el que quieres seguir missing_resource: No se pudo encontrar la URL de redirección requerida para tu cuenta diff --git a/config/locales/fa.yml b/config/locales/fa.yml index 1a8372d63..b73f2401d 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -593,20 +593,6 @@ fa: other: سایر publishing: انتشار web: وب - push_notifications: - favourite: - title: "%{name} نوشتهٔ شما را پسندید" - follow: - title: "%{name} هم‌اینک پیگیر شماست" - group: - title: "%{count} اعلان" - mention: - action_boost: بازبوق - action_expand: نمایش بیشتر - action_favourite: پسندیدن - title: "%{name} از شما نام برد" - reblog: - title: "%{name} نوشتهٔ شما را بازبوقید" remote_follow: acct: نشانی حساب username@domain خود را این‌جا بنویسید missing_resource: نشانی اینترنتی برای رسیدن به حساب شما پیدا نشد diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 68a239ba3..1e02efbd2 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -590,20 +590,6 @@ fi: other: Muut publishing: Julkaiseminen web: Web - push_notifications: - favourite: - title: "%{name} tykkäsi tilastasi" - follow: - title: "%{name} seuraa nyt sinua" - group: - title: "%{count} ilmoitusta" - mention: - action_boost: Buustaa - action_expand: Näytä lisää - action_favourite: Tykkää - title: "%{nimi} mainitsi sinut" - reblog: - title: "%{name} buustasi tilaasi" remote_follow: acct: Syötä se käyttäjätunnus@verkkotunnus, josta haluat seurata missing_resource: Vaadittavaa uudelleenohjaus-URL:ää tiliisi ei löytynyt diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f8f9026c8..4265a7248 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -594,20 +594,6 @@ fr: other: Autre publishing: Publication web: Web - push_notifications: - favourite: - title: "%{name} a mis votre statut en favori" - follow: - title: "%{name} vous suit" - group: - title: "%{count} notifications" - mention: - action_boost: Partager - action_expand: Montrer plus - action_favourite: Ajouter aux favoris - title: "%{name} vous a mentionné·e" - reblog: - title: "%{name} a partagé votre statut" remote_follow: acct: Entrez votre pseudo@instance depuis lequel vous voulez suivre cet·te utilisateur⋅ice missing_resource: L’URL de redirection n’a pas pu être trouvée diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 6cf83ca27..d38cffb96 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -594,20 +594,6 @@ gl: other: Outro publishing: Publicando web: Web - push_notifications: - favourite: - title: "%{name} marcou favorito o seu estado" - follow: - title: "%{name} agora está a seguila" - group: - title: "%{count} notificacións" - mention: - action_boost: Promover - action_expand: Mostar máis - action_favourite: Favorito - title: "%{name} mencionouna" - reblog: - title: "%{name} promoveu un dos seus estados" remote_follow: acct: Introduza o seu nomedeusuaria@dominio desde onde quere facer seguimento missing_resource: Non se puido atopar o URL de redirecionamento requerido para a súa conta diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 0cd0021c1..41093aa43 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -531,20 +531,6 @@ hu: other: Egyéb publishing: Közzététel web: Web - push_notifications: - favourite: - title: "%{name} a kedvenceihez adta a tülköd" - follow: - title: "%{name} mostantól követ téged" - group: - title: "%{count} értesítés" - mention: - action_boost: Reblog - action_expand: Mutass többet - action_favourite: Kedvencekhez adás - title: "%{name} megemlített téged" - reblog: - title: "%{name} reblogolta a tülköd" remote_follow: acct: Írd be a felhasználódat, amelyről követni szeretnéd felhasznalonev@domain formátumban missing_resource: A fiókodnál nem található a szükséges átirányítási URL diff --git a/config/locales/it.yml b/config/locales/it.yml index b53645c9f..a761bff4e 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -506,18 +506,6 @@ it: other: Altro publishing: Pubblicazione web: Web - push_notifications: - favourite: - title: "%{name} ha segnato il tuo status come apprezzato" - follow: - title: "%{name} ha iniziato a seguirti" - group: - title: "%{count} notifiche" - mention: - action_expand: Mostra altro - action_favourite: Apprezzato - reblog: - title: "%{name} ha condiviso il tuo status" remote_follow: acct: Inserisci il tuo username@dominio da cui vuoi seguire questo utente missing_resource: Impossibile trovare l'URL di reindirizzamento richiesto per il tuo account diff --git a/config/locales/ja.yml b/config/locales/ja.yml index b6c8f7bcc..cb7340dde 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -594,20 +594,6 @@ ja: other: その他 publishing: 投稿 web: ウェブ - push_notifications: - favourite: - title: あなたのトゥートが %{name} さんにお気に入り登録されました - follow: - title: "%{name} さんにフォローされました" - group: - title: "%{count} 件の通知" - mention: - action_boost: ブースト - action_expand: もっと見る - action_favourite: お気に入り - title: "%{name} さんから返信がありました" - reblog: - title: あなたのトゥートが %{name} さんにブーストされました remote_follow: acct: あなたの ユーザー名@ドメイン を入力してください missing_resource: リダイレクト先が見つかりませんでした diff --git a/config/locales/ko.yml b/config/locales/ko.yml index c0da95db3..38f411dd5 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -596,20 +596,6 @@ ko: other: 기타 publishing: 퍼블리싱 web: 웹 - push_notifications: - favourite: - title: "%{name} 님이 당신의 툿를 즐겨찾기에 등록했습니다" - follow: - title: "%{name} 님이 나를 팔로우 하고 있습니다" - group: - title: "%{count} 건의 알림" - mention: - action_boost: 부스트 - action_expand: 더보기 - action_favourite: 즐겨찾기 - title: "%{name} 님이 답장을 보냈습니다" - reblog: - title: "%{name} 님이 당신의 툿을 부스트 했습니다" remote_follow: acct: 아이디@도메인을 입력해 주십시오 missing_resource: 리디렉션 대상을 찾을 수 없습니다 diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 53468a67d..7ec512d9e 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -594,20 +594,6 @@ nl: other: Overig publishing: Publiceren web: Webapp - push_notifications: - favourite: - title: "%{name} markeerde jouw toot als favoriet" - follow: - title: "%{name} volgt jou nu" - group: - title: "%{count} meldingen" - mention: - action_boost: Boost - action_expand: Meer tonen - action_favourite: Favoriet - title: "%{name} vermeldde jou" - reblog: - title: "%{name} boostte jouw toot" remote_follow: acct: Geef jouw account@domein.tld op waarvandaan je wilt volgen missing_resource: Kon vereiste doorverwijzings-URL voor jouw account niet vinden diff --git a/config/locales/no.yml b/config/locales/no.yml index 837042f85..eb1d27a19 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -531,20 +531,6 @@ other: Annet publishing: Publisering web: Web - push_notifications: - favourite: - title: "%{name} favoriserte din status" - follow: - title: "%{name} følger deg nå" - group: - title: "%{count} varslinger" - mention: - action_boost: Fremhev - action_expand: Vis mer - action_favourite: Favoritter - title: "%{name} nevnte deg" - reblog: - title: "%{name} fremhevde din status" remote_follow: acct: Tast inn brukernavn@domene som du vil følge fra missing_resource: Kunne ikke finne URLen for din konto diff --git a/config/locales/oc.yml b/config/locales/oc.yml index c9a33aad6..4a8453457 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -668,20 +668,6 @@ oc: other: Autre publishing: Publicar web: Interfàcia Web - push_notifications: - favourite: - title: "%{name} a mes vòstre estatut en favorit" - follow: - title: "%{name} vos sèc ara" - group: - title: "%{count} notificacions" - mention: - action_boost: Partejar - action_expand: Ne veire mai - action_favourite: Ajustar als favorits - title: "%{name} vos a mencionat" - reblog: - title: "%{name} a partejat vòstre estatut" remote_follow: acct: Picatz vòstre utilizaire@instància que cal utilizar per sègre aqueste utilizaire missing_resource: URL de redireccion pas trobada diff --git a/config/locales/pl.yml b/config/locales/pl.yml index e465388e2..15dfdf740 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -603,20 +603,6 @@ pl: other: Pozostałe publishing: Publikowanie web: Sieć - push_notifications: - favourite: - title: "%{name} dodał Twój wpis do ulubionych" - follow: - title: "%{name} zaczął Cię śledzić" - group: - title: "%{count} powiadomień" - mention: - action_boost: Podbij - action_expand: Pokaż więcej - action_favourite: Dodaj do ulubionych - title: "%{name} wspomniał o Tobie" - reblog: - title: "%{name} podbił Twój wpis" remote_follow: acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 702236198..cf7584999 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -593,20 +593,6 @@ pt-BR: other: Outro publishing: Publicação web: Web - push_notifications: - favourite: - title: "%{name} favoritou a sua postagem" - follow: - title: "%{name} está te seguindo" - group: - title: "%{count} notificações" - mention: - action_boost: Compartilhar - action_expand: Mostrar mais - action_favourite: Favoritar - title: "%{name} mencionou você" - reblog: - title: "%{name} compartilhou a sua postagem" remote_follow: acct: Insira o seu usuário@domínio do qual você quer seguir missing_resource: Não foi possível encontrar a URL de direcionamento para a sua conta diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 7c10124ff..a1370c91d 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -534,20 +534,6 @@ pt: other: Outro publishing: Publicação web: Web - push_notifications: - favourite: - title: "%{name} adicionou o teu post aos favoritos" - follow: - title: "%{name} começou a seguir-te" - group: - title: "%{count} notificações" - mention: - action_boost: Partilhar - action_expand: Mostrar mais - action_favourite: Adicionar aos favoritos - title: "%{name} mencionou-te" - reblog: - title: "%{name} partilhou o teu post" remote_follow: acct: Entre seu usuário@domínio do qual quer seguir missing_resource: Não foi possível achar a URL de redirecionamento para sua conta diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 2363bba12..19b844612 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -606,20 +606,6 @@ ru: other: Другое publishing: Публикация web: WWW - push_notifications: - favourite: - title: Ваш статус понравился %{name} - follow: - title: "%{name} теперь подписан(а) на Вас" - group: - title: "%{count} уведомлений" - mention: - action_boost: Продвинуть - action_expand: Развернуть - action_favourite: Нравится - title: Вас упомянул(а) %{name} - reblog: - title: "%{name} продвинул(а) Ваш статус" remote_follow: acct: Введите username@domain, откуда Вы хотите подписаться missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 8debd87ff..3e1300b2b 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -591,20 +591,6 @@ sk: other: Ostatné publishing: Publikovanie web: Web - push_notifications: - favourite: - title: "%{name} si obľúbil/a tvoj príspevok" - follow: - title: "%{name} ťa teraz následuje" - group: - title: "%{count} notifikácie" - mention: - action_boost: Pozdvihni - action_expand: Ukáž viac - action_favourite: Obľúbené - title: "%{name} ťa spomenul/a" - reblog: - title: "%{name} vyzdvihli tvoj príspevok" remote_follow: acct: Napíš svoju prezývku@doménu z ktorej chceš následovať missing_resource: Nemôžeme nájsť potrebnú presmerovaciu adresu k tvojmu účtu diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index 0c7756c53..15c6b00ac 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -525,20 +525,6 @@ sr-Latn: other: Ostali publishing: Objavljivanje web: Veb - push_notifications: - favourite: - title: "%{name} je stavio Vaš status za omiljeni" - follow: - title: "%{name} Vas je zapratio" - group: - title: "%{count} obaveštenja" - mention: - action_boost: Podrži - action_expand: Prikaži još - action_favourite: Omiljeni - title: "%{name} Vas je pomenuo" - reblog: - title: "%{name} je podržao(la) Vaš status" remote_follow: acct: Unesite Vaš korisnik@domen sa koga želite da pratite missing_resource: Ne mogu da nađem zahtevanu adresu preusmeravanja za Vaš nalog diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 1cb87ff30..d34a2ecbf 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -525,20 +525,6 @@ sr: other: Остали publishing: Објављивање web: Веб - push_notifications: - favourite: - title: "%{name} је ставио Ваш статус за омиљени" - follow: - title: "%{name} Вас је запратио" - group: - title: "%{count} обавештења" - mention: - action_boost: Подржи - action_expand: Прикажи још - action_favourite: Омиљени - title: "%{name} Вас је поменуо" - reblog: - title: "%{name} је подржао(ла) Ваш статус" remote_follow: acct: Унесите Ваш корисник@домен са кога желите да пратите missing_resource: Не могу да нађем захтевану адресу преусмеравања за Ваш налог diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 1b0965655..73645d013 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -592,20 +592,6 @@ sv: other: Annat publishing: Publicering web: Webb - push_notifications: - favourite: - title: "%{name} favoriserade din status" - follow: - title: "%{name} följer nu dig" - group: - title: "%{count} meddelanden" - mention: - action_boost: Knuffa - action_expand: Visa mer - action_favourite: Favoriter - title: "%{name} nämnde dig" - reblog: - title: "%{name} boostade din status" remote_follow: acct: Ange ditt användarnamn@domän du vill följa från missing_resource: Det gick inte att hitta den begärda omdirigeringsadressen för ditt konto diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 5eca9e6a5..9f98a893d 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -523,20 +523,6 @@ zh-CN: other: 其他 publishing: 发布 web: 站内 - push_notifications: - favourite: - title: "%{name} 收藏了你的嘟文" - follow: - title: "%{name} 关注了你" - group: - title: "%{count} 条新通知" - mention: - action_boost: 转嘟 - action_expand: 显示更多 - action_favourite: 收藏 - title: "%{name} 提到了你" - reblog: - title: "%{name} 转嘟了你的嘟文" remote_follow: acct: 请输入你的“用户名@实例域名” missing_resource: 无法确定你的帐户的跳转 URL diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index 04df558b1..14eedc9ba 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -593,20 +593,6 @@ zh-HK: other: 其他 publishing: 發佈 web: 站内 - push_notifications: - favourite: - title: "%{name} 收藏了你的文章" - follow: - title: "%{name} 關注了你" - group: - title: "%{count} 條新通知" - mention: - action_boost: 轉推 - action_expand: 顯示更多 - action_favourite: 收藏 - title: "%{name} 提到了你" - reblog: - title: "%{name} 轉推了你的文章" remote_follow: acct: 請輸入你的︰用戶名稱@服務點域名 missing_resource: 無法找到你用戶的轉接網址 -- cgit From 8378b72ebacc51e5e090faa527462b801e4c2803 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 19 May 2018 21:05:08 +0200 Subject: Ensure push subscription is immediately removed when application is revoked (#7548) * Ensure push subscription is immediately removed when application is revoked * When token is revoked from app, unsubscribe too --- .../oauth/authorized_applications_controller.rb | 5 +++++ app/controllers/oauth/tokens_controller.rb | 14 +++++++++++++ app/models/web/push_subscription.rb | 9 +++++++++ config/routes.rb | 4 +++- .../authorized_applications_controller_spec.rb | 20 +++++++++++++++++++ spec/controllers/oauth/tokens_controller_spec.rb | 23 ++++++++++++++++++++++ .../web_push_subscription_fabricator.rb | 2 +- spec/fabricators/web_setting_fabricator.rb | 3 +-- 8 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 app/controllers/oauth/tokens_controller.rb create mode 100644 spec/controllers/oauth/tokens_controller_spec.rb (limited to 'app/models') diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 395fbc51b..0c28d194b 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -8,6 +8,11 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio include Localized + def destroy + Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner) + super + end + private def store_current_location diff --git a/app/controllers/oauth/tokens_controller.rb b/app/controllers/oauth/tokens_controller.rb new file mode 100644 index 000000000..fa6d58f25 --- /dev/null +++ b/app/controllers/oauth/tokens_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class Oauth::TokensController < Doorkeeper::TokensController + def revoke + unsubscribe_for_token if authorized? && token.accessible? + super + end + + private + + def unsubscribe_for_token + Web::PushSubscription.where(access_token_id: token.id).delete_all + end +end diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index 7da3428fe..867bc9519 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -50,6 +50,15 @@ class Web::PushSubscription < ApplicationRecord end end + class << self + def unsubscribe_for(application_id, resource_owner) + access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id, revoked_at: nil) + .pluck(:id) + + where(access_token_id: access_token_ids).delete_all + end + end + private def push_payload(message, ttl = 5.minutes.seconds) diff --git a/config/routes.rb b/config/routes.rb index bd9d09226..3042b5ea0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,7 +14,9 @@ Rails.application.routes.draw do end use_doorkeeper do - controllers authorizations: 'oauth/authorizations', authorized_applications: 'oauth/authorized_applications' + controllers authorizations: 'oauth/authorizations', + authorized_applications: 'oauth/authorized_applications', + tokens: 'oauth/tokens' end get '.well-known/host-meta', to: 'well_known/host_meta#show', as: :host_meta, defaults: { format: 'xml' } diff --git a/spec/controllers/oauth/authorized_applications_controller_spec.rb b/spec/controllers/oauth/authorized_applications_controller_spec.rb index f967b507f..901e538e9 100644 --- a/spec/controllers/oauth/authorized_applications_controller_spec.rb +++ b/spec/controllers/oauth/authorized_applications_controller_spec.rb @@ -39,4 +39,24 @@ describe Oauth::AuthorizedApplicationsController do include_examples 'stores location for user' end end + + describe 'DELETE #destroy' do + let!(:user) { Fabricate(:user) } + let!(:application) { Fabricate(:application) } + let!(:access_token) { Fabricate(:accessible_access_token, application: application, resource_owner_id: user.id) } + let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } + + before do + sign_in user, scope: :user + post :destroy, params: { id: application.id } + end + + it 'revokes access tokens for the application' do + expect(Doorkeeper::AccessToken.where(application: application).first.revoked_at).to_not be_nil + end + + it 'removes subscriptions for the application\'s access tokens' do + expect(Web::PushSubscription.where(user: user).count).to eq 0 + end + end end diff --git a/spec/controllers/oauth/tokens_controller_spec.rb b/spec/controllers/oauth/tokens_controller_spec.rb new file mode 100644 index 000000000..ba8e367a6 --- /dev/null +++ b/spec/controllers/oauth/tokens_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Oauth::TokensController, type: :controller do + describe 'POST #revoke' do + let!(:user) { Fabricate(:user) } + let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) } + let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } + + before do + post :revoke, params: { token: access_token.token } + end + + it 'revokes the token' do + expect(access_token.reload.revoked_at).to_not be_nil + end + + it 'removes web push subscription for token' do + expect(Web::PushSubscription.where(access_token: access_token).count).to eq 0 + end + end +end diff --git a/spec/fabricators/web_push_subscription_fabricator.rb b/spec/fabricators/web_push_subscription_fabricator.rb index 72d11b77c..97f90675d 100644 --- a/spec/fabricators/web_push_subscription_fabricator.rb +++ b/spec/fabricators/web_push_subscription_fabricator.rb @@ -1,4 +1,4 @@ -Fabricator(:web_push_subscription) do +Fabricator(:web_push_subscription, from: Web::PushSubscription) do endpoint Faker::Internet.url key_p256dh Faker::Internet.password key_auth Faker::Internet.password diff --git a/spec/fabricators/web_setting_fabricator.rb b/spec/fabricators/web_setting_fabricator.rb index e5136829b9..369b86bc1 100644 --- a/spec/fabricators/web_setting_fabricator.rb +++ b/spec/fabricators/web_setting_fabricator.rb @@ -1,3 +1,2 @@ -Fabricator('Web::Setting') do - +Fabricator(:web_setting, from: Web::Setting) do end -- cgit From 63c7b9157274f57c496399a1a5c728b32415034c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 27 May 2018 04:58:08 +0200 Subject: Validate that e-mail resolves with MX and it's not blacklisted (#7631) Original patch by @j-a4 --- app/models/user.rb | 1 + app/validators/email_mx_validator.rb | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 app/validators/email_mx_validator.rb (limited to 'app/models') diff --git a/app/models/user.rb b/app/models/user.rb index cfbae58ed..0becfa7e9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -65,6 +65,7 @@ class User < ApplicationRecord validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates_with BlacklistedEmailValidator, if: :email_changed? + validates_with EmailMxValidator, if: :email_changed? scope :recent, -> { order(id: :desc) } scope :admins, -> { where(admin: true) } diff --git a/app/validators/email_mx_validator.rb b/app/validators/email_mx_validator.rb new file mode 100644 index 000000000..d4c7cc252 --- /dev/null +++ b/app/validators/email_mx_validator.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'resolv' + +class EmailMxValidator < ActiveModel::Validator + def validate(user) + return if Rails.env.test? + user.errors.add(:email, I18n.t('users.invalid_email')) if invalid_mx?(user.email) + end + + private + + def invalid_mx?(value) + _, domain = value.split('@', 2) + + return true if domain.nil? + + records = Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s } + records.empty? || on_blacklist?(records) + end + + def on_blacklist?(values) + EmailDomainBlock.where(domain: values).any? + end +end -- cgit From 9bd23dc4e51ba47283a8e3a66cd94b4e624a5235 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 27 May 2018 21:45:30 +0200 Subject: Track trending tags (#7638) * Track trending tags - Half-life of 1 day - Historical usage in daily buckets (last 7 days stored) - GET /api/v1/trends Fix #271 * Add trends to web UI * Don't render compose form on search route, adjust search results header * Disqualify tag from trends if it's in disallowed hashtags setting * Count distinct accounts using tag, ignore silenced accounts --- app/controllers/api/v1/trends_controller.rb | 17 +++++ app/javascript/mastodon/actions/trends.js | 32 +++++++++ .../features/compose/components/search_results.js | 59 ++++++++++++++- .../compose/containers/search_results_container.js | 8 ++- app/javascript/mastodon/features/compose/index.js | 4 +- app/javascript/mastodon/reducers/index.js | 2 + app/javascript/mastodon/reducers/trends.js | 13 ++++ app/javascript/styles/mastodon/components.scss | 83 +++++++++++++++++++++- app/models/tag.rb | 16 +++++ app/models/trending_tags.rb | 61 ++++++++++++++++ app/serializers/rest/tag_serializer.rb | 11 +++ app/services/process_hashtags_service.rb | 6 +- config/routes.rb | 1 + package.json | 1 + yarn.lock | 6 ++ 15 files changed, 310 insertions(+), 10 deletions(-) create mode 100644 app/controllers/api/v1/trends_controller.rb create mode 100644 app/javascript/mastodon/actions/trends.js create mode 100644 app/javascript/mastodon/reducers/trends.js create mode 100644 app/models/trending_tags.rb create mode 100644 app/serializers/rest/tag_serializer.rb (limited to 'app/models') diff --git a/app/controllers/api/v1/trends_controller.rb b/app/controllers/api/v1/trends_controller.rb new file mode 100644 index 000000000..bcea9857e --- /dev/null +++ b/app/controllers/api/v1/trends_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Api::V1::TrendsController < Api::BaseController + before_action :set_tags + + respond_to :json + + def index + render json: @tags, each_serializer: REST::TagSerializer + end + + private + + def set_tags + @tags = TrendingTags.get(limit_param(10)) + end +end diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js new file mode 100644 index 000000000..853e4f60a --- /dev/null +++ b/app/javascript/mastodon/actions/trends.js @@ -0,0 +1,32 @@ +import api from '../api'; + +export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST'; +export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS'; +export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL'; + +export const fetchTrends = () => (dispatch, getState) => { + dispatch(fetchTrendsRequest()); + + api(getState) + .get('/api/v1/trends') + .then(({ data }) => dispatch(fetchTrendsSuccess(data))) + .catch(err => dispatch(fetchTrendsFail(err))); +}; + +export const fetchTrendsRequest = () => ({ + type: TRENDS_FETCH_REQUEST, + skipLoading: true, +}); + +export const fetchTrendsSuccess = trends => ({ + type: TRENDS_FETCH_SUCCESS, + trends, + skipLoading: true, +}); + +export const fetchTrendsFail = error => ({ + type: TRENDS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index 84455563c..f2655c14d 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -1,23 +1,75 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, FormattedNumber } from 'react-intl'; import AccountContainer from '../../../containers/account_container'; import StatusContainer from '../../../containers/status_container'; import { Link } from 'react-router-dom'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + +const shortNumberFormat = number => { + if (number < 1000) { + return ; + } else { + return K; + } +}; export default class SearchResults extends ImmutablePureComponent { static propTypes = { results: ImmutablePropTypes.map.isRequired, + trends: ImmutablePropTypes.list, + fetchTrends: PropTypes.func.isRequired, }; + componentDidMount () { + const { fetchTrends } = this.props; + fetchTrends(); + } + render () { - const { results } = this.props; + const { results, trends } = this.props; let accounts, statuses, hashtags; let count = 0; + if (results.isEmpty()) { + return ( +
+
+
+ + +
+ + {trends && trends.map(hashtag => ( +
+
+ + #{hashtag.get('name')} + + + {shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))} }} /> +
+ +
+ {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))} +
+ +
+ day.get('uses')).toArray()}> + + +
+
+ ))} +
+
+ ); + } + if (results.get('accounts') && results.get('accounts').size > 0) { count += results.get('accounts').size; accounts = ( @@ -48,7 +100,7 @@ export default class SearchResults extends ImmutablePureComponent { {results.get('hashtags').map(hashtag => ( - #{hashtag} + {hashtag} ))}
@@ -58,6 +110,7 @@ export default class SearchResults extends ImmutablePureComponent { return (
+
diff --git a/app/javascript/mastodon/features/compose/containers/search_results_container.js b/app/javascript/mastodon/features/compose/containers/search_results_container.js index 16d95d417..7273460e2 100644 --- a/app/javascript/mastodon/features/compose/containers/search_results_container.js +++ b/app/javascript/mastodon/features/compose/containers/search_results_container.js @@ -1,8 +1,14 @@ import { connect } from 'react-redux'; import SearchResults from '../components/search_results'; +import { fetchTrends } from '../../../actions/trends'; const mapStateToProps = state => ({ results: state.getIn(['search', 'results']), + trends: state.get('trends'), }); -export default connect(mapStateToProps)(SearchResults); +const mapDispatchToProps = dispatch => ({ + fetchTrends: () => dispatch(fetchTrends()), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(SearchResults); diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 19aae0332..d8e9ad9ee 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -101,7 +101,7 @@ export default class Compose extends React.PureComponent { {(multiColumn || isSearchPage) && }
-
+ {!isSearchPage &&
{multiColumn && ( @@ -109,7 +109,7 @@ export default class Compose extends React.PureComponent {
)} -
+
} {({ x }) => ( diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 3d9a6a132..019c1f466 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -26,6 +26,7 @@ import height_cache from './height_cache'; import custom_emojis from './custom_emojis'; import lists from './lists'; import listEditor from './list_editor'; +import trends from './trends'; const reducers = { dropdown_menu, @@ -55,6 +56,7 @@ const reducers = { custom_emojis, lists, listEditor, + trends, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/trends.js b/app/javascript/mastodon/reducers/trends.js new file mode 100644 index 000000000..95cf8f284 --- /dev/null +++ b/app/javascript/mastodon/reducers/trends.js @@ -0,0 +1,13 @@ +import { TRENDS_FETCH_SUCCESS } from '../actions/trends'; +import { fromJS } from 'immutable'; + +const initialState = null; + +export default function trendsReducer(state = initialState, action) { + switch(action.type) { + case TRENDS_FETCH_SUCCESS: + return fromJS(action.trends); + default: + return state; + } +}; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 2724454fb..c66bc427c 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3334,9 +3334,15 @@ a.status-card { color: $dark-text-color; background: lighten($ui-base-color, 2%); border-bottom: 1px solid darken($ui-base-color, 4%); - padding: 15px 10px; - font-size: 14px; + padding: 15px; font-weight: 500; + font-size: 16px; + cursor: default; + + .fa { + display: inline-block; + margin-right: 5px; + } } .search-results__section { @@ -5209,3 +5215,76 @@ noscript { background: $ui-base-color; } } + +.trends { + &__header { + color: $dark-text-color; + background: lighten($ui-base-color, 2%); + border-bottom: 1px solid darken($ui-base-color, 4%); + font-weight: 500; + padding: 15px; + font-size: 16px; + cursor: default; + + .fa { + display: inline-block; + margin-right: 5px; + } + } + + &__item { + display: flex; + align-items: center; + padding: 15px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + + &:last-child { + border-bottom: 0; + } + + &__name { + flex: 1 1 auto; + color: $dark-text-color; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + strong { + font-weight: 500; + } + + a { + color: $darker-text-color; + text-decoration: none; + font-size: 14px; + font-weight: 500; + display: block; + + &:hover, + &:focus, + &:active { + span { + text-decoration: underline; + } + } + } + } + + &__current { + width: 100px; + font-size: 24px; + line-height: 36px; + font-weight: 500; + text-align: center; + color: $secondary-text-color; + } + + &__sparkline { + width: 50px; + + path { + stroke: lighten($highlight-text-color, 6%) !important; + } + } + } +} diff --git a/app/models/tag.rb b/app/models/tag.rb index 8b1b02412..4f31f796e 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -21,6 +21,22 @@ class Tag < ApplicationRecord name end + def history + days = [] + + 7.times do |i| + day = i.days.ago.beginning_of_day.to_i + + days << { + day: day.to_s, + uses: Redis.current.get("activity:tags:#{id}:#{day}") || '0', + accounts: Redis.current.pfcount("activity:tags:#{id}:#{day}:accounts").to_s, + } + end + + days + end + class << self def search_for(term, limit = 5) pattern = sanitize_sql_like(term.strip) + '%' diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb new file mode 100644 index 000000000..eedd92644 --- /dev/null +++ b/app/models/trending_tags.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class TrendingTags + KEY = 'trending_tags' + HALF_LIFE = 1.day.to_i + MAX_ITEMS = 500 + EXPIRE_HISTORY_AFTER = 7.days.seconds + + class << self + def record_use!(tag, account, at_time = Time.now.utc) + return if disallowed_hashtags.include?(tag.name) || account.silenced? + + increment_vote!(tag.id, at_time) + increment_historical_use!(tag.id, at_time) + increment_unique_use!(tag.id, account.id, at_time) + end + + def get(limit) + tag_ids = redis.zrevrange(KEY, 0, limit).map(&:to_i) + tags = Tag.where(id: tag_ids).to_a.map { |tag| [tag.id, tag] }.to_h + tag_ids.map { |tag_id| tags[tag_id] }.compact + end + + private + + def increment_vote!(tag_id, at_time) + redis.zincrby(KEY, (2**((at_time.to_i - epoch) / HALF_LIFE)).to_f, tag_id.to_s) + redis.zremrangebyrank(KEY, 0, -MAX_ITEMS) if rand < (2.to_f / MAX_ITEMS) + end + + def increment_historical_use!(tag_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}" + redis.incrby(key, 1) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + def increment_unique_use!(tag_id, account_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}:accounts" + redis.pfadd(key, account_id) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + # The epoch needs to be 2.5 years in the future if the half-life is one day + # While dynamic, it will always be the same within one year + def epoch + @epoch ||= Date.new(Date.current.year + 2.5, 10, 1).to_datetime.to_i + end + + def disallowed_hashtags + return @disallowed_hashtags if defined?(@disallowed_hashtags) + + @disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags + @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String + @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) + end + + def redis + Redis.current + end + end +end diff --git a/app/serializers/rest/tag_serializer.rb b/app/serializers/rest/tag_serializer.rb new file mode 100644 index 000000000..74aa571a4 --- /dev/null +++ b/app/serializers/rest/tag_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class REST::TagSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name, :url, :history + + def url + tag_url(object) + end +end diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index 5b45c865f..0695922b8 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -4,8 +4,10 @@ class ProcessHashtagsService < BaseService def call(status, tags = []) tags = Extractor.extract_hashtags(status.text) if status.local? - tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |tag| - status.tags << Tag.where(name: tag).first_or_initialize(name: tag) + tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name| + tag = Tag.where(name: name).first_or_create(name: name) + status.tags << tag + TrendingTags.record_use!(tag, status.account, status.created_at) end end end diff --git a/config/routes.rb b/config/routes.rb index 3042b5ea0..2fcb885ed 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -254,6 +254,7 @@ Rails.application.routes.draw do resources :mutes, only: [:index] resources :favourites, only: [:index] resources :reports, only: [:index, :create] + resources :trends, only: [:index] namespace :apps do get :verify_credentials, to: 'credentials#show' diff --git a/package.json b/package.json index 61f38409c..6ee6f98d3 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "react-redux-loading-bar": "^2.9.3", "react-router-dom": "^4.1.1", "react-router-scroll-4": "^1.0.0-beta.1", + "react-sparklines": "^1.7.0", "react-swipeable-views": "^0.12.3", "react-textarea-autosize": "^5.2.1", "react-toggle": "^4.0.1", diff --git a/yarn.lock b/yarn.lock index 50c88557d..de48c995a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6124,6 +6124,12 @@ react-router@^4.2.0: prop-types "^15.5.4" warning "^3.0.0" +react-sparklines@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" + dependencies: + prop-types "^15.5.10" + react-swipeable-views-core@^0.12.11: version "0.12.11" resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.12.11.tgz#3cf2b4daffbb36f9d69bd19bf5b2d5370b6b2c1b" -- cgit From b87a1229c74aebec02cd08091f25613f6dff4428 Mon Sep 17 00:00:00 2001 From: tateisu Date: Mon, 28 May 2018 18:04:06 +0900 Subject: optimize direct timeline (#7614) * optimize direct timeline * fix typo in class name * change filter condition for direct timeline * fix codestyle issue * revoke index_accounts_not_silenced because direct timeline does not use it. * revoke index_accounts_not_silenced because direct timeline does not use it. * fix rspec test condition. * fix rspec test condition. * fix rspec test condition. * revoke adding column and partial index * (direct timeline) move merging logic to model * fix pagination parameter * add method arguments that switches return array of status or cache_ids * fix order by * returns ActiveRecord.Relation in default behavor * fix codestyle issue --- .../api/v1/timelines/direct_controller.rb | 15 +++++--- app/models/status.rb | 43 +++++++++++++++++++--- spec/models/status_spec.rb | 23 ++++++------ 3 files changed, 59 insertions(+), 22 deletions(-) (limited to 'app/models') diff --git a/app/controllers/api/v1/timelines/direct_controller.rb b/app/controllers/api/v1/timelines/direct_controller.rb index d455227eb..ef64078be 100644 --- a/app/controllers/api/v1/timelines/direct_controller.rb +++ b/app/controllers/api/v1/timelines/direct_controller.rb @@ -23,15 +23,18 @@ class Api::V1::Timelines::DirectController < Api::BaseController end def direct_statuses - direct_timeline_statuses.paginate_by_max_id( - limit_param(DEFAULT_STATUSES_LIMIT), - params[:max_id], - params[:since_id] - ) + direct_timeline_statuses end def direct_timeline_statuses - Status.as_direct_timeline(current_account) + # this query requires built in pagination. + Status.as_direct_timeline( + current_account, + limit_param(DEFAULT_STATUSES_LIMIT), + params[:max_id], + params[:since_id], + true # returns array of cache_ids object + ) end def insert_pagination_headers diff --git a/app/models/status.rb b/app/models/status.rb index 853e75b43..54f3f68f5 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -188,12 +188,45 @@ class Status < ApplicationRecord where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private]) end - def as_direct_timeline(account) - query = joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}") - .where("mentions.account_id = #{account.id} OR statuses.account_id = #{account.id}") - .where(visibility: [:direct]) + def as_direct_timeline(account, limit = 20, max_id = nil, since_id = nil, cache_ids = false) + # direct timeline is mix of direct message from_me and to_me. + # 2 querys are executed with pagination. + # constant expression using arel_table is required for partial index + + # _from_me part does not require any timeline filters + query_from_me = where(account_id: account.id) + .where(Status.arel_table[:visibility].eq(3)) + .limit(limit) + .order('statuses.id DESC') + + # _to_me part requires mute and block filter. + # FIXME: may we check mutes.hide_notifications? + query_to_me = Status + .joins(:mentions) + .merge(Mention.where(account_id: account.id)) + .where(Status.arel_table[:visibility].eq(3)) + .limit(limit) + .order('mentions.status_id DESC') + .not_excluded_by_account(account) + + if max_id.present? + query_from_me = query_from_me.where('statuses.id < ?', max_id) + query_to_me = query_to_me.where('mentions.status_id < ?', max_id) + end + + if since_id.present? + query_from_me = query_from_me.where('statuses.id > ?', since_id) + query_to_me = query_to_me.where('mentions.status_id > ?', since_id) + end - apply_timeline_filters(query, account, false) + if cache_ids + # returns array of cache_ids object that have id and updated_at + (query_from_me.cache_ids.to_a + query_to_me.cache_ids.to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) + else + # returns ActiveRecord.Relation + items = (query_from_me.select(:id).to_a + query_to_me.select(:id).to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) + Status.where(id: items.map(&:id)) + end end def as_public_timeline(account = nil, local_only = false) diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index c6701018e..aee4f49b4 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -154,7 +154,7 @@ RSpec.describe Status, type: :model do describe '#target' do it 'returns nil if the status is self-contained' do - expect(subject.target).to be_nil + expect(subject.target).to be_nil end it 'returns nil if the status is a reply' do @@ -333,24 +333,25 @@ RSpec.describe Status, type: :model do expect(@results).to_not include(@followed_public_status) end - it 'includes direct statuses mentioning recipient from followed' do - Fabricate(:mention, account: account, status: @followed_direct_status) - expect(@results).to include(@followed_direct_status) - end - it 'does not include direct statuses not mentioning recipient from followed' do expect(@results).to_not include(@followed_direct_status) end - it 'includes direct statuses mentioning recipient from non-followed' do - Fabricate(:mention, account: account, status: @not_followed_direct_status) - expect(@results).to include(@not_followed_direct_status) - end - it 'does not include direct statuses not mentioning recipient from non-followed' do expect(@results).to_not include(@not_followed_direct_status) end + it 'includes direct statuses mentioning recipient from followed' do + Fabricate(:mention, account: account, status: @followed_direct_status) + results2 = Status.as_direct_timeline(account) + expect(results2).to include(@followed_direct_status) + end + + it 'includes direct statuses mentioning recipient from non-followed' do + Fabricate(:mention, account: account, status: @not_followed_direct_status) + results2 = Status.as_direct_timeline(account) + expect(results2).to include(@not_followed_direct_status) + end end describe '.as_public_timeline' do -- cgit From e599d7caf2642c7143616e8402b7d730d32c349d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 29 May 2018 01:39:02 +0200 Subject: Rescue Mastodon::DimensionsValidationError in Remoteable (#7662) Fix #7660 --- app/models/concerns/remotable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/models') diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index 20ddbca53..c17f04776 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -41,7 +41,7 @@ module Remotable rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}" nil - rescue Paperclip::Error => e + rescue Paperclip::Error, Mastodon::DimensionsValidationError => e Rails.logger.debug "Error processing remote #{attachment_name}: #{e}" nil end -- cgit