From 0167659f5f0b61e5a44df33df2510c2388a1a297 Mon Sep 17 00:00:00 2001 From: Marek Ľach <45913977+marek-lach@users.noreply.github.com> Date: Fri, 1 Feb 2019 00:07:08 +0100 Subject: Distinguish error messaging for mystyped URLs and deleted accounts (#9957) --- config/locales/en.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'config/locales/en.yml') diff --git a/config/locales/en.yml b/config/locales/en.yml index 10749b21b..fc0f63415 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -563,8 +563,8 @@ en: other: "%{count} people" errors: '403': You don't have permission to view this page. - '404': The page you were looking for doesn't exist. - '410': The page you were looking for doesn't exist anymore. + '404': The page you are looking for isn't here. + '410': The page you were looking for doesn't exist here anymore. '422': content: Security verification failed. Are you blocking cookies? title: Security verification failed @@ -577,7 +577,7 @@ en: archive_takeout: date: Date download: Download your archive - hint_html: You can request an archive of your toots and uploaded media. The exported data will be in ActivityPub format, readable by any compliant software. You can request an archive every 7 days. + hint_html: You can request an archive of your toots and uploaded media. The exported data will be in the ActivityPub format, readable by any compliant software. You can request an archive every 7 days. in_progress: Compiling your archive... request: Request your archive size: Size -- cgit From ad8c71c9855634bd58b05825f4e331d75f734aaa Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 1 Feb 2019 00:15:38 +0100 Subject: Fix link color in high-contrast theme, add underlines (#9949) Improve sorting of default themes in the dropdown --- app/javascript/styles/contrast/diff.scss | 51 ++++++++++++++++++++++++++++++++ config/locales/en.yml | 6 ++-- 2 files changed, 54 insertions(+), 3 deletions(-) (limited to 'config/locales/en.yml') diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss index eee9ecc3e..7d8993a50 100644 --- a/app/javascript/styles/contrast/diff.scss +++ b/app/javascript/styles/contrast/diff.scss @@ -12,3 +12,54 @@ } } } + +.status__content a, +.reply-indicator__content a { + color: lighten($ui-highlight-color, 12%); + text-decoration: underline; + + &.mention { + text-decoration: none; + } + + &.mention span { + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + + &.status__content__spoiler-link { + color: $secondary-text-color; + text-decoration: none; + } +} + +.status__content__read-more-button { + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } +} + +.getting-started__footer a { + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } +} diff --git a/config/locales/en.yml b/config/locales/en.yml index fc0f63415..c7ef36598 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -929,9 +929,9 @@ en:

Originally adapted from the Discourse privacy policy.

title: "%{instance} Terms of Service and Privacy Policy" themes: - contrast: High contrast - default: Mastodon - mastodon-light: Mastodon (light) + contrast: Mastodon (High contrast) + default: Mastodon (Dark) + mastodon-light: Mastodon (Light) time: formats: default: "%b %d, %Y, %H:%M" -- cgit From d14c276e58f0f223b0e4889d342a948c961081b2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 3 Feb 2019 03:59:51 +0100 Subject: Add option to overwrite imported data (#9962) * Add option to overwrite imported data Fix #7465 * Add import for domain blocks --- app/models/account_domain_block.rb | 1 + app/models/concerns/domain_normalizable.rb | 2 +- app/models/export.rb | 1 + app/models/import.rb | 14 +++- app/services/import_service.rb | 90 ++++++++++++++++++++++ app/views/settings/imports/show.html.haml | 7 +- app/workers/import/relationship_worker.rb | 8 +- app/workers/import_worker.rb | 38 +-------- config/locales/en.yml | 6 ++ .../20190201012802_add_overwrite_to_imports.rb | 17 ++++ db/schema.rb | 3 +- spec/models/concerns/account_interactions_spec.rb | 4 +- 12 files changed, 148 insertions(+), 43 deletions(-) create mode 100644 app/services/import_service.rb create mode 100644 db/migrate/20190201012802_add_overwrite_to_imports.rb (limited to 'config/locales/en.yml') diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb index e352000c3..7c0d60379 100644 --- a/app/models/account_domain_block.rb +++ b/app/models/account_domain_block.rb @@ -12,6 +12,7 @@ class AccountDomainBlock < ApplicationRecord include Paginable + include DomainNormalizable belongs_to :account validates :domain, presence: true, uniqueness: { scope: :account_id } diff --git a/app/models/concerns/domain_normalizable.rb b/app/models/concerns/domain_normalizable.rb index dff3e5414..fb84058fc 100644 --- a/app/models/concerns/domain_normalizable.rb +++ b/app/models/concerns/domain_normalizable.rb @@ -10,6 +10,6 @@ module DomainNormalizable private def normalize_domain - self.domain = TagManager.instance.normalize_domain(domain) + self.domain = TagManager.instance.normalize_domain(domain&.strip) end end diff --git a/app/models/export.rb b/app/models/export.rb index a2520e9c2..fc4bb6964 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'csv' class Export diff --git a/app/models/import.rb b/app/models/import.rb index 55e970b0d..a7a0d8065 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -13,20 +13,30 @@ # data_file_size :integer # data_updated_at :datetime # account_id :bigint(8) not null +# overwrite :boolean default(FALSE), not null # class Import < ApplicationRecord - FILE_TYPES = ['text/plain', 'text/csv'].freeze + FILE_TYPES = %w(text/plain text/csv).freeze + MODES = %i(merge overwrite).freeze self.inheritance_column = false belongs_to :account - enum type: [:following, :blocking, :muting] + enum type: [:following, :blocking, :muting, :domain_blocking] validates :type, presence: true has_attached_file :data validates_attachment_content_type :data, content_type: FILE_TYPES validates_attachment_presence :data + + def mode + overwrite? ? :overwrite : :merge + end + + def mode=(str) + self.overwrite = str.to_sym == :overwrite + end end diff --git a/app/services/import_service.rb b/app/services/import_service.rb new file mode 100644 index 000000000..3f558626e --- /dev/null +++ b/app/services/import_service.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'csv' + +class ImportService < BaseService + ROWS_PROCESSING_LIMIT = 20_000 + + def call(import) + @import = import + @account = @import.account + @data = CSV.new(import_data).reject(&:blank?) + + case @import.type + when 'following' + import_follows! + when 'blocking' + import_blocks! + when 'muting' + import_mutes! + when 'domain_blocking' + import_domain_blocks! + end + end + + private + + def import_follows! + import_relationships!('follow', 'unfollow', @account.following, follow_limit) + end + + def import_blocks! + import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT) + end + + def import_mutes! + import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT) + end + + def import_domain_blocks! + items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row.first.strip } + + if @import.overwrite? + presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true } + + @account.domain_blocks.find_each do |domain_block| + if presence_hash[domain_block.domain] + items.delete(domain_block.domain) + else + @account.unblock_domain!(domain_block.domain) + end + end + end + + items.each do |domain| + @account.block_domain!(domain) + end + + AfterAccountDomainBlockWorker.push_bulk(items) do |domain| + [@account.id, domain] + end + end + + def import_relationships!(action, undo_action, overwrite_scope, limit) + items = @data.take(limit).map { |row| row.first.strip } + + if @import.overwrite? + presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true } + + overwrite_scope.find_each do |target_account| + if presence_hash[target_account.acct] + items.delete(target_account.acct) + else + Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action) + end + end + end + + Import::RelationshipWorker.push_bulk(items) do |acct| + [@account.id, acct, action] + end + end + + def import_data + Paperclip.io_adapters.for(@import.data).read + end + + def follow_limit + FollowLimitValidator.limit_for_account(@account) + end +end diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml index 4512fc714..7bb4beb01 100644 --- a/app/views/settings/imports/show.html.haml +++ b/app/views/settings/imports/show.html.haml @@ -5,8 +5,11 @@ .field-group = f.input :type, collection: Import.types.keys, wrapper: :with_block_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, hint: t('imports.preface') - .field-group - = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data') + .fields-row + .fields-group.fields-row__column.fields-row__column-6 + = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data') + .fields-group.fields-row__column.fields-row__column-6 + = f.input :mode, as: :radio_buttons, collection: Import::MODES, label_method: lambda { |mode| safe_join([I18n.t("imports.modes.#{mode}"), content_tag(:span, I18n.t("imports.modes.#{mode}_long"), class: 'hint')]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' .actions = f.button :button, t('imports.upload'), type: :submit diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb index 1dd8bf8fb..e9db20a46 100644 --- a/app/workers/import/relationship_worker.rb +++ b/app/workers/import/relationship_worker.rb @@ -13,11 +13,17 @@ class Import::RelationshipWorker case relationship when 'follow' - FollowService.new.call(from_account, target_account.acct) + FollowService.new.call(from_account, target_account) + when 'unfollow' + UnfollowService.new.call(from_account, target_account) when 'block' BlockService.new.call(from_account, target_account) + when 'unblock' + UnblockService.new.call(from_account, target_account) when 'mute' MuteService.new.call(from_account, target_account) + when 'unmute' + UnmuteService.new.call(from_account, target_account) end rescue ActiveRecord::RecordNotFound true diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb index aeb221cf6..dfa71b29e 100644 --- a/app/workers/import_worker.rb +++ b/app/workers/import_worker.rb @@ -1,44 +1,14 @@ # frozen_string_literal: true -require 'csv' - class ImportWorker include Sidekiq::Worker sidekiq_options queue: 'pull', retry: false - attr_reader :import - def perform(import_id) - @import = Import.find(import_id) - - Import::RelationshipWorker.push_bulk(import_rows) do |row| - [@import.account_id, row.first, relationship_type] - end - - @import.destroy - end - - private - - def import_contents - Paperclip.io_adapters.for(@import.data).read - end - - def relationship_type - case @import.type - when 'following' - 'follow' - when 'blocking' - 'block' - when 'muting' - 'mute' - end - end - - def import_rows - rows = CSV.new(import_contents).reject(&:blank?) - rows = rows.take(FollowLimitValidator.limit_for_account(@import.account)) if @import.type == 'following' - rows + import = Import.find(import_id) + ImportService.new.call(import) + ensure + import&.destroy end end diff --git a/config/locales/en.yml b/config/locales/en.yml index c7ef36598..f23f867e5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -628,10 +628,16 @@ en: one: Something isn't quite right yet! Please review the error below other: Something isn't quite right yet! Please review %{count} errors below imports: + modes: + merge: Merge + merge_long: Keep existing records and add new ones + overwrite: Overwrite + overwrite_long: Replace current records with the new ones preface: You can import data that you have exported from another instance, such as a list of the people you are following or blocking. success: Your data was successfully uploaded and will now be processed in due time types: blocking: Blocking list + domain_blocking: Domain blocking list following: Following list muting: Muting list upload: Upload diff --git a/db/migrate/20190201012802_add_overwrite_to_imports.rb b/db/migrate/20190201012802_add_overwrite_to_imports.rb new file mode 100644 index 000000000..89b262cc7 --- /dev/null +++ b/db/migrate/20190201012802_add_overwrite_to_imports.rb @@ -0,0 +1,17 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddOverwriteToImports < ActiveRecord::Migration[5.2] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def up + safety_assured do + add_column_with_default :imports, :overwrite, :boolean, default: false, allow_null: false + end + end + + def down + remove_column :imports, :overwrite, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 3487adf08..44e00df40 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: 2019_01_17_114553) do +ActiveRecord::Schema.define(version: 2019_02_01_012802) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -290,6 +290,7 @@ ActiveRecord::Schema.define(version: 2019_01_17_114553) do t.integer "data_file_size" t.datetime "data_updated_at" t.bigint "account_id", null: false + t.boolean "overwrite", default: false, null: false end create_table "invites", force: :cascade do |t| diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb index 8df52b770..e8ef61f66 100644 --- a/spec/models/concerns/account_interactions_spec.rb +++ b/spec/models/concerns/account_interactions_spec.rb @@ -237,9 +237,9 @@ describe AccountInteractions do end describe '#block_domain!' do - let(:domain_block) { Fabricate(:domain_block) } + let(:domain) { 'example.com' } - subject { account.block_domain!(domain_block) } + subject { account.block_domain!(domain) } it 'creates and returns AccountDomainBlock' do expect do -- cgit From 364f2ff9aa2b4bf601d68a12bce758aeb5530467 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 4 Feb 2019 04:25:59 +0100 Subject: Add featured hashtags to profiles (#9755) * Add hashtag filter to profiles GET /@:username/tagged/:hashtag GET /api/v1/accounts/:id/statuses?tagged=:hashtag * Display featured hashtags on public profile * Use separate model for featured tags * Update featured hashtag counters on-write * Limit featured tags to 10 --- app/controllers/accounts_controller.rb | 14 +++++- .../api/v1/accounts/statuses_controller.rb | 5 +++ .../settings/featured_tags_controller.rb | 51 ++++++++++++++++++++++ app/controllers/settings/profiles_controller.rb | 2 +- app/controllers/settings/sessions_controller.rb | 1 + app/javascript/styles/mastodon/accounts.scss | 4 ++ app/javascript/styles/mastodon/admin.scss | 7 ++- app/javascript/styles/mastodon/widgets.scss | 7 ++- app/models/concerns/account_associations.rb | 1 + app/models/featured_tag.rb | 46 +++++++++++++++++++ app/models/tag.rb | 2 + app/services/process_hashtags_service.rb | 12 ++++- app/services/remove_status_service.rb | 4 ++ app/views/accounts/show.html.haml | 13 ++++++ app/views/settings/featured_tags/index.html.haml | 27 ++++++++++++ config/locales/en.yml | 5 +++ config/locales/simple_form.en.yml | 4 ++ config/navigation.rb | 1 + config/routes.rb | 2 + ...171005102658_create_account_moderation_notes.rb | 1 + db/migrate/20190203180359_create_featured_tags.rb | 12 +++++ db/schema.rb | 15 ++++++- spec/fabricators/featured_tag_fabricator.rb | 6 +++ spec/models/featured_tag_spec.rb | 4 ++ 24 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 app/controllers/settings/featured_tags_controller.rb create mode 100644 app/models/featured_tag.rb create mode 100644 app/views/settings/featured_tags/index.html.haml create mode 100644 db/migrate/20190203180359_create_featured_tags.rb create mode 100644 spec/fabricators/featured_tag_fabricator.rb create mode 100644 spec/models/featured_tag_spec.rb (limited to 'config/locales/en.yml') diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index f788a9078..6e3a23073 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -57,6 +57,7 @@ class AccountsController < ApplicationController def filtered_statuses default_statuses.tap do |statuses| + statuses.merge!(hashtag_scope) if tag_requested? statuses.merge!(only_media_scope) if media_requested? statuses.merge!(no_replies_scope) unless replies_requested? end @@ -78,12 +79,15 @@ class AccountsController < ApplicationController Status.without_replies end + def hashtag_scope + Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id) + end + def set_account @account = Account.find_local!(params[:username]) end def older_url - ::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}") pagination_url(max_id: @statuses.last.id) end @@ -92,7 +96,9 @@ class AccountsController < ApplicationController end def pagination_url(max_id: nil, min_id: nil) - if media_requested? + if tag_requested? + short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id) + elsif media_requested? short_account_media_url(@account, max_id: max_id, min_id: min_id) elsif replies_requested? short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) @@ -109,6 +115,10 @@ class AccountsController < ApplicationController request.path.ends_with?('/with_replies') end + def tag_requested? + request.path.ends_with?("/tagged/#{params[:tag]}") + end + def filtered_status_page(params) if params[:min_id].present? filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 6c2a5c141..6fdc827cb 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) + statuses.merge!(hashtag_scope) if params[:tagged].present? statuses end @@ -67,6 +68,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController Status.without_reblogs end + def hashtag_scope + Status.tagged_with(Tag.find_by(name: params[:tagged])&.id) + end + def pagination_params(core_params) params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb new file mode 100644 index 000000000..19815e416 --- /dev/null +++ b/app/controllers/settings/featured_tags_controller.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class Settings::FeaturedTagsController < Settings::BaseController + layout 'admin' + + before_action :authenticate_user! + before_action :set_featured_tags, only: :index + before_action :set_featured_tag, except: [:index, :create] + before_action :set_most_used_tags, only: :index + + def index + @featured_tag = FeaturedTag.new + end + + def create + @featured_tag = current_account.featured_tags.new(featured_tag_params) + @featured_tag.reset_data + + if @featured_tag.save + redirect_to settings_featured_tags_path + else + set_featured_tags + set_most_used_tags + + render :index + end + end + + def destroy + @featured_tag.destroy! + redirect_to settings_featured_tags_path + end + + private + + def set_featured_tag + @featured_tag = current_account.featured_tags.find(params[:id]) + end + + def set_featured_tags + @featured_tags = current_account.featured_tags.reject(&:new_record?) + end + + def set_most_used_tags + @most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) + end + + def featured_tag_params + params.require(:featured_tag).permit(:name) + end +end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index db9081fdf..8b640cdca 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -32,6 +32,6 @@ class Settings::ProfilesController < Settings::BaseController end def set_account - @account = current_user.account + @account = current_account end end diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index 11b150ffd..84ebb21f2 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Settings::SessionsController < Settings::BaseController + before_action :authenticate_user! before_action :set_session, only: :destroy def destroy diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index 63a5c61b8..f4f458cf4 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -288,3 +288,7 @@ border-bottom: 0; } } + +.directory__tag .trends__item__current { + width: auto; +} diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 177f8145f..6d785707c 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -153,10 +153,15 @@ $content-width: 840px; font-weight: 500; } - .directory__tag a { + .directory__tag > a, + .directory__tag > div { box-shadow: none; } + .directory__tag .table-action-link .fa { + color: inherit; + } + .directory__tag h4 { font-size: 18px; font-weight: 700; diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index c97337e4e..1eaf30c5b 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -269,7 +269,8 @@ box-sizing: border-box; margin-bottom: 10px; - a { + & > a, + & > div { display: flex; align-items: center; justify-content: space-between; @@ -279,7 +280,9 @@ text-decoration: none; color: inherit; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + } + & > a { &:hover, &:active, &:focus { @@ -287,7 +290,7 @@ } } - &.active a { + &.active > a { background: $ui-highlight-color; cursor: default; } diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 7dafeee34..397ec4a22 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -55,5 +55,6 @@ module AccountAssociations # Hashtags has_and_belongs_to_many :tags + has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account end end diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb new file mode 100644 index 000000000..b5a10ad2d --- /dev/null +++ b/app/models/featured_tag.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: featured_tags +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# tag_id :bigint(8) +# statuses_count :bigint(8) default(0), not null +# last_status_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# + +class FeaturedTag < ApplicationRecord + belongs_to :account, inverse_of: :featured_tags, required: true + belongs_to :tag, inverse_of: :featured_tags, required: true + + delegate :name, to: :tag, allow_nil: true + + validates :name, presence: true + validate :validate_featured_tags_limit, on: :create + + def name=(str) + self.tag = Tag.find_or_initialize_by(name: str.delete('#').mb_chars.downcase.to_s) + end + + def increment(timestamp) + update(statuses_count: statuses_count + 1, last_status_at: timestamp) + end + + def decrement(deleted_status_id) + update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at) + end + + def reset_data + self.statuses_count = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).count + self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at + end + + private + + def validate_featured_tags_limit + errors.add(:base, I18n.t('featured_tags.errors.limit')) if account.featured_tags.count >= 10 + end +end diff --git a/app/models/tag.rb b/app/models/tag.rb index 99830ae92..4373e967b 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -14,6 +14,7 @@ class Tag < ApplicationRecord has_and_belongs_to_many :accounts has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account' + has_many :featured_tags, dependent: :destroy, inverse_of: :tag has_one :account_tag_stat, dependent: :destroy HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*' @@ -23,6 +24,7 @@ class Tag < ApplicationRecord scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) } scope :hidden, -> { where(account_tag_stats: { hidden: true }) } + scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) } delegate :accounts_count, :accounts_count=, diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index cf7471c98..d5ec076a8 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -2,12 +2,22 @@ class ProcessHashtagsService < BaseService def call(status, tags = []) - tags = Extractor.extract_hashtags(status.text) if status.local? + tags = Extractor.extract_hashtags(status.text) if status.local? + records = [] 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 + records << tag + TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility? end + + return unless status.public_visibility? || status.unlisted_visibility? + + status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag| + featured_tag.increment(status.created_at) + end end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 28c5224b0..2012f15fc 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -131,6 +131,10 @@ class RemoveStatusService < BaseService end def remove_from_hashtags + @account.featured_tags.where(tag_id: @status.tags.pluck(:id)).each do |featured_tag| + featured_tag.decrement(@status.id) + end + return unless @status.public_visibility? @tags.each do |hashtag| diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 0ee9dd7de..23a595205 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -63,4 +63,17 @@ - @endorsed_accounts.each do |account| = account_link_to account + - @account.featured_tags.each do |featured_tag| + .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil } + = link_to short_account_tag_path(@account, featured_tag.tag) do + %h4 + = fa_icon 'hashtag' + = featured_tag.name + %small + - if featured_tag.last_status_at.nil? + = t('accounts.nothing_here') + - else + %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at + .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true + = render 'application/sidebar' diff --git a/app/views/settings/featured_tags/index.html.haml b/app/views/settings/featured_tags/index.html.haml new file mode 100644 index 000000000..5f69517f3 --- /dev/null +++ b/app/views/settings/featured_tags/index.html.haml @@ -0,0 +1,27 @@ +- content_for :page_title do + = t('settings.featured_tags') + += simple_form_for @featured_tag, url: settings_featured_tags_path do |f| + = render 'shared/error_messages', object: @featured_tag + + .fields-group + = f.input :name, wrapper: :with_block_label, hint: safe_join([t('simple_form.hints.featured_tag.name'), safe_join(@most_used_tags.map { |tag| link_to("##{tag.name}", settings_featured_tags_path(featured_tag: { name: tag.name }), method: :post) }, ', ')], ' ') + + .actions + = f.button :button, t('featured_tags.add_new'), type: :submit + +%hr.spacer/ + +- @featured_tags.each do |featured_tag| + .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil } + %div + %h4 + = fa_icon 'hashtag' + = featured_tag.name + %small + - if featured_tag.last_status_at.nil? + = t('accounts.nothing_here') + - else + %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at + = table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } + .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true diff --git a/config/locales/en.yml b/config/locales/en.yml index f23f867e5..c92fc781c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -588,6 +588,10 @@ en: lists: Lists mutes: You mute storage: Media storage + featured_tags: + add_new: Add new + errors: + limit: You have already featured the maximum amount of hashtags filters: contexts: home: Home timeline @@ -807,6 +811,7 @@ en: development: Development edit_profile: Edit profile export: Data export + featured_tags: Featured hashtags followers: Authorized followers import: Import migrate: Account migration diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 325114755..3a2746a53 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -37,6 +37,8 @@ en: setting_theme: Affects how Mastodon looks when you're logged in from any device. username: Your username will be unique on %{domain} whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + featured_tag: + name: 'You might want to use one of these:' imports: data: CSV file exported from another Mastodon instance sessions: @@ -110,6 +112,8 @@ en: username: Username username_or_email: Username or Email whole_word: Whole word + featured_tag: + name: Hashtag interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow diff --git a/config/navigation.rb b/config/navigation.rb index a9521f956..1be621ac2 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -6,6 +6,7 @@ SimpleNavigation::Configuration.run do |navigation| primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings| settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration} + settings.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url settings.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_notifications_url settings.item :password, safe_join([fa_icon('lock fw'), t('auth.security')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete} diff --git a/config/routes.rb b/config/routes.rb index af49845cc..ded62981d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,7 @@ Rails.application.routes.draw do get '/@:username', to: 'accounts#show', as: :short_account get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies get '/@:username/media', to: 'accounts#show', as: :short_account_media + get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status @@ -116,6 +117,7 @@ Rails.application.routes.draw do resource :migration, only: [:show, :update] resources :sessions, only: [:destroy] + resources :featured_tags, only: [:index, :create, :destroy] end resources :media, only: [:show] do diff --git a/db/migrate/20171005102658_create_account_moderation_notes.rb b/db/migrate/20171005102658_create_account_moderation_notes.rb index d1802b5b3..974ed9940 100644 --- a/db/migrate/20171005102658_create_account_moderation_notes.rb +++ b/db/migrate/20171005102658_create_account_moderation_notes.rb @@ -7,6 +7,7 @@ class CreateAccountModerationNotes < ActiveRecord::Migration[5.1] t.timestamps end + add_foreign_key :account_moderation_notes, :accounts, column: :target_account_id end end diff --git a/db/migrate/20190203180359_create_featured_tags.rb b/db/migrate/20190203180359_create_featured_tags.rb new file mode 100644 index 000000000..b08410a3a --- /dev/null +++ b/db/migrate/20190203180359_create_featured_tags.rb @@ -0,0 +1,12 @@ +class CreateFeaturedTags < ActiveRecord::Migration[5.2] + def change + create_table :featured_tags do |t| + t.references :account, foreign_key: { on_delete: :cascade } + t.references :tag, foreign_key: { on_delete: :cascade } + t.bigint :statuses_count, default: 0, null: false + t.datetime :last_status_at + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 44e00df40..e9fb358f8 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: 2019_02_01_012802) do +ActiveRecord::Schema.define(version: 2019_02_03_180359) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -250,6 +250,17 @@ ActiveRecord::Schema.define(version: 2019_02_01_012802) do t.index ["status_id"], name: "index_favourites_on_status_id" end + create_table "featured_tags", force: :cascade do |t| + t.bigint "account_id" + t.bigint "tag_id" + t.bigint "statuses_count", default: 0, null: false + t.datetime "last_status_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_featured_tags_on_account_id" + t.index ["tag_id"], name: "index_featured_tags_on_tag_id" + end + create_table "follow_requests", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -708,6 +719,8 @@ ActiveRecord::Schema.define(version: 2019_02_01_012802) do add_foreign_key "custom_filters", "accounts", on_delete: :cascade add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade + add_foreign_key "featured_tags", "accounts", on_delete: :cascade + add_foreign_key "featured_tags", "tags", on_delete: :cascade add_foreign_key "follow_requests", "accounts", column: "target_account_id", name: "fk_9291ec025d", on_delete: :cascade add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade diff --git a/spec/fabricators/featured_tag_fabricator.rb b/spec/fabricators/featured_tag_fabricator.rb new file mode 100644 index 000000000..25cbdaac0 --- /dev/null +++ b/spec/fabricators/featured_tag_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:featured_tag) do + account + tag + statuses_count 1_337 + last_status_at Time.now.utc +end diff --git a/spec/models/featured_tag_spec.rb b/spec/models/featured_tag_spec.rb new file mode 100644 index 000000000..07533e0b9 --- /dev/null +++ b/spec/models/featured_tag_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe FeaturedTag, type: :model do +end -- cgit From 46e806cd2f14a5e45d66b4c23040855202818984 Mon Sep 17 00:00:00 2001 From: mayaeh Date: Wed, 6 Feb 2019 03:11:24 +0900 Subject: Rename from instance to server. (#9938) --- .../mastodon/features/getting_started/index.js | 2 +- .../mastodon/features/public_timeline/index.js | 2 +- .../features/ui/components/report_modal.js | 2 +- .../mastodon/locales/defaultMessages.json | 6 +-- app/javascript/mastodon/locales/en.json | 6 +-- app/javascript/mastodon/locales/ja.json | 8 ++-- config/locales/devise.en.yml | 6 +-- config/locales/devise.ja.yml | 6 +-- config/locales/en.yml | 34 +++++++-------- config/locales/ja.yml | 48 +++++++++++----------- config/locales/simple_form.en.yml | 2 +- config/locales/simple_form.ja.yml | 2 +- 12 files changed, 62 insertions(+), 62 deletions(-) (limited to 'config/locales/en.yml') diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index e277a73c7..e1f84de27 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -160,7 +160,7 @@ class GettingStarted extends ImmutablePureComponent { {invitesEnabled &&
  • ·
  • } {multiColumn &&
  • ·
  • }
  • ·
  • -
  • ·
  • +
  • ·
  • ·
  • ·
  • ·
  • diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index d640033eb..2b7d9c56f 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -124,7 +124,7 @@ class PublicTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} trackScroll={!pinned} scrollKey={`public_timeline-${columnId}`} - emptyMessage={} + emptyMessage={} shouldUpdateScroll={shouldUpdateScroll} /> diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js index bc6b18664..2e41f784d 100644 --- a/app/javascript/mastodon/features/ui/components/report_modal.js +++ b/app/javascript/mastodon/features/ui/components/report_modal.js @@ -97,7 +97,7 @@ class ReportModal extends ImmutablePureComponent {
    -

    +