From 76188d61f2c3bb5747208df92835e912db18d7b6 Mon Sep 17 00:00:00 2001 From: shel Date: Sun, 26 Mar 2017 12:49:14 -0400 Subject: Clarify post privacy warning I was informed that the current warning if you @ a remote server in a private post is inadequate. These are suggested changes to better inform users. --- app/assets/javascripts/components/locales/en.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index 5af44ea4b..2d3360b6b 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -40,7 +40,7 @@ const en = { "compose_form.sensitive": "Mark media as sensitive", "compose_form.spoiler": "Hide text behind warning", "compose_form.private": "Mark as private", - "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}} to not leak your status?", + "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.", "compose_form.unlisted": "Do not display on public timelines", "navigation_bar.edit_profile": "Edit profile", "navigation_bar.preferences": "Preferences", -- cgit From e8875c6046615778c7ae6f1fc0c4a195fb5d3a03 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 30 Mar 2017 19:42:33 +0200 Subject: Import feature for following/blocking lists (addresses #62, #177, #201, #454) --- app/controllers/api/v1/timelines_controller.rb | 12 ++--- app/controllers/settings/imports_controller.rb | 34 ++++++++++++++ app/models/import.rb | 14 ++++++ app/views/layouts/admin.html.haml | 9 ++++ app/views/settings/imports/show.html.haml | 11 +++++ app/workers/import_worker.rb | 54 ++++++++++++++++++++++ config/locales/en.yml | 8 ++++ config/locales/simple_form.en.yml | 4 ++ config/navigation.rb | 1 + config/routes.rb | 1 + db/migrate/20170330163835_create_imports.rb | 11 +++++ ...0170330164118_add_attachment_data_to_imports.rb | 11 +++++ db/schema.rb | 14 +++++- spec/fabricators/import_fabricator.rb | 2 + spec/models/import_spec.rb | 5 ++ 15 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 app/controllers/settings/imports_controller.rb create mode 100644 app/models/import.rb create mode 100644 app/views/settings/imports/show.html.haml create mode 100644 app/workers/import_worker.rb create mode 100644 db/migrate/20170330163835_create_imports.rb create mode 100644 db/migrate/20170330164118_add_attachment_data_to_imports.rb create mode 100644 spec/fabricators/import_fabricator.rb create mode 100644 spec/models/import_spec.rb (limited to 'app') diff --git a/app/controllers/api/v1/timelines_controller.rb b/app/controllers/api/v1/timelines_controller.rb index af6e5b7df..0446b9e4d 100644 --- a/app/controllers/api/v1/timelines_controller.rb +++ b/app/controllers/api/v1/timelines_controller.rb @@ -11,8 +11,8 @@ class Api::V1::TimelinesController < ApiController @statuses = cache_collection(@statuses) set_maps(@statuses) - set_counters_maps(@statuses) - set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq) + # set_counters_maps(@statuses) + # set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq) next_path = api_v1_home_timeline_url(max_id: @statuses.last.id) unless @statuses.empty? prev_path = api_v1_home_timeline_url(since_id: @statuses.first.id) unless @statuses.empty? @@ -27,8 +27,8 @@ class Api::V1::TimelinesController < ApiController @statuses = cache_collection(@statuses) set_maps(@statuses) - set_counters_maps(@statuses) - set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq) + # set_counters_maps(@statuses) + # set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq) next_path = api_v1_public_timeline_url(max_id: @statuses.last.id) unless @statuses.empty? prev_path = api_v1_public_timeline_url(since_id: @statuses.first.id) unless @statuses.empty? @@ -44,8 +44,8 @@ class Api::V1::TimelinesController < ApiController @statuses = cache_collection(@statuses) set_maps(@statuses) - set_counters_maps(@statuses) - set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq) + # set_counters_maps(@statuses) + # set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq) next_path = api_v1_hashtag_timeline_url(params[:id], max_id: @statuses.last.id) unless @statuses.empty? prev_path = api_v1_hashtag_timeline_url(params[:id], since_id: @statuses.first.id) unless @statuses.empty? diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb new file mode 100644 index 000000000..cbb5e65da --- /dev/null +++ b/app/controllers/settings/imports_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class Settings::ImportsController < ApplicationController + layout 'admin' + + before_action :authenticate_user! + before_action :set_account + + def show + @import = Import.new + end + + def create + @import = Import.new(import_params) + @import.account = @account + + if @import.save + ImportWorker.perform_async(@import.id) + redirect_to settings_import_path, notice: I18n.t('imports.success') + else + render action: :show + end + end + + private + + def set_account + @account = current_user.account + end + + def import_params + params.require(:import).permit(:data, :type) + end +end diff --git a/app/models/import.rb b/app/models/import.rb new file mode 100644 index 000000000..255063c53 --- /dev/null +++ b/app/models/import.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class Import < ApplicationRecord + self.inheritance_column = false + + enum type: [:following, :blocking] + + belongs_to :account + + FILE_TYPES = ['text/plain', 'text/csv'].freeze + + has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV.fetch('PAPERCLIP_SECRET') + validates_attachment_content_type :data, content_type: FILE_TYPES +end diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 750d6036f..59fe078df 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -12,6 +12,15 @@ .content-wrapper .content %h2= yield :page_title + + - if flash[:notice] + .flash-message.notice + %strong= flash[:notice] + + - if flash[:alert] + .flash-message.alert + %strong= flash[:alert] + = yield = render template: "layouts/application", locals: { body_classes: 'admin' } diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml new file mode 100644 index 000000000..8502913dc --- /dev/null +++ b/app/views/settings/imports/show.html.haml @@ -0,0 +1,11 @@ +- content_for :page_title do + = t('settings.import') + +%p.hint= t('imports.preface') + += simple_form_for @import, url: settings_import_path do |f| + = f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") } + = f.input :data, wrapper: :with_label, hint: t('simple_form.hints.imports.data') + + .actions + = f.button :button, t('imports.upload'), type: :submit diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb new file mode 100644 index 000000000..a3ae2a85a --- /dev/null +++ b/app/workers/import_worker.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'csv' + +class ImportWorker + include Sidekiq::Worker + + sidekiq_options retry: false + + def perform(import_id) + import = Import.find(import_id) + + case import.type + when 'blocking' + process_blocks(import) + when 'following' + process_follows(import) + end + + import.destroy + end + + private + + def process_blocks(import) + from_account = import.account + + CSV.foreach(import.data.path) do |row| + next if row.size != 1 + + begin + target_account = FollowRemoteAccountService.new.call(row[0]) + next if target_account.nil? + BlockService.new.call(from_account, target_account) + rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError + next + end + end + end + + def process_follows(import) + from_account = import.account + + CSV.foreach(import.data.path) do |row| + next if row.size != 1 + + begin + FollowService.new.call(from_account, row[0]) + rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError + next + end + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 3e130aaf8..965001e05 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -85,6 +85,13 @@ en: validation_errors: 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: + preface: You can import certain data like all the people you are following or blocking into your account on this instance, from files created by an export on another instance. + success: Your data was successfully uploaded and will now be processed in due time + types: + blocking: Blocking list + following: Following list + upload: Upload landing_strip_html: %{name} is a user on %{domain}. You can follow them or interact with them if you have an account anywhere in the fediverse. If you don't, you can sign up here. notification_mailer: digest: @@ -124,6 +131,7 @@ en: back: Back to Mastodon edit_profile: Edit profile export: Data export + import: Import preferences: Preferences settings: Settings two_factor_auth: Two-factor Authentication diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index c4bd0ad96..df4f6ca00 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -8,12 +8,15 @@ en: header: PNG, GIF or JPG. At most 2MB. Will be downscaled to 700x335px locked: Requires you to manually approve followers and defaults post privacy to followers-only note: At most 160 characters + imports: + data: CSV file exported from another Mastodon instance labels: defaults: avatar: Avatar confirm_new_password: Confirm new password confirm_password: Confirm password current_password: Current password + data: Data display_name: Display name email: E-mail address header: Header @@ -24,6 +27,7 @@ en: otp_attempt: Two-factor code password: Password setting_default_privacy: Post privacy + type: Import type username: Username interactions: must_be_follower: Block notifications from non-followers diff --git a/config/navigation.rb b/config/navigation.rb index 607a0ff10..77556e5aa 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -9,6 +9,7 @@ SimpleNavigation::Configuration.run do |navigation| settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url settings.item :password, safe_join([fa_icon('cog fw'), t('auth.change_password')]), edit_user_registration_url settings.item :two_factor_auth, safe_join([fa_icon('mobile fw'), t('settings.two_factor_auth')]), settings_two_factor_auth_url + settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url end diff --git a/config/routes.rb b/config/routes.rb index cf8364968..bfca5c734 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -51,6 +51,7 @@ Rails.application.routes.draw do namespace :settings do resource :profile, only: [:show, :update] resource :preferences, only: [:show, :update] + resource :import, only: [:show, :create] resource :export, only: [:show] do collection do diff --git a/db/migrate/20170330163835_create_imports.rb b/db/migrate/20170330163835_create_imports.rb new file mode 100644 index 000000000..d6f74823d --- /dev/null +++ b/db/migrate/20170330163835_create_imports.rb @@ -0,0 +1,11 @@ +class CreateImports < ActiveRecord::Migration[5.0] + def change + create_table :imports do |t| + t.integer :account_id, null: false + t.integer :type, null: false + t.boolean :approved + + t.timestamps + end + end +end diff --git a/db/migrate/20170330164118_add_attachment_data_to_imports.rb b/db/migrate/20170330164118_add_attachment_data_to_imports.rb new file mode 100644 index 000000000..4850b0663 --- /dev/null +++ b/db/migrate/20170330164118_add_attachment_data_to_imports.rb @@ -0,0 +1,11 @@ +class AddAttachmentDataToImports < ActiveRecord::Migration + def self.up + change_table :imports do |t| + t.attachment :data + end + end + + def self.down + remove_attachment :imports, :data + end +end diff --git a/db/schema.rb b/db/schema.rb index 7675ed1a9..5a9ca1426 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: 20170330021336) do +ActiveRecord::Schema.define(version: 20170330164118) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -93,6 +93,18 @@ ActiveRecord::Schema.define(version: 20170330021336) do t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true, using: :btree end + create_table "imports", force: :cascade do |t| + t.integer "account_id", null: false + t.integer "type", null: false + t.boolean "approved" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "data_file_name" + t.string "data_content_type" + t.integer "data_file_size" + t.datetime "data_updated_at" + end + create_table "media_attachments", force: :cascade do |t| t.bigint "status_id" t.string "file_file_name" diff --git a/spec/fabricators/import_fabricator.rb b/spec/fabricators/import_fabricator.rb new file mode 100644 index 000000000..e2eb1e0df --- /dev/null +++ b/spec/fabricators/import_fabricator.rb @@ -0,0 +1,2 @@ +Fabricator(:import) do +end diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb new file mode 100644 index 000000000..fa52077cd --- /dev/null +++ b/spec/models/import_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Import, type: :model do + +end -- cgit From bf61bc1b96e63a848e7ec7984be54cb508b4bfe7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 31 Mar 2017 11:48:25 +0200 Subject: Fix drag & drop overlay flickering --- .../javascripts/components/features/ui/index.jsx | 51 ++++++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index b7e8f32a4..4b7e4bb46 100644 --- a/app/assets/javascripts/components/features/ui/index.jsx +++ b/app/assets/javascripts/components/features/ui/index.jsx @@ -36,15 +36,31 @@ const UI = React.createClass({ this.setState({ width: window.innerWidth }); }, + handleDragEnter (e) { + e.preventDefault(); + + if (!this.dragTargets) { + this.dragTargets = []; + } + + if (this.dragTargets.indexOf(e.target) === -1) { + this.dragTargets.push(e.target); + } + + this.setState({ draggingOver: true }); + }, + handleDragOver (e) { e.preventDefault(); e.stopPropagation(); - e.dataTransfer.dropEffect = 'copy'; + try { + e.dataTransfer.dropEffect = 'copy'; + } catch (err) { - if (e.dataTransfer.effectAllowed === 'all' || e.dataTransfer.effectAllowed === 'uninitialized') { - this.setState({ draggingOver: true }); } + + return false; }, handleDrop (e) { @@ -57,14 +73,25 @@ const UI = React.createClass({ } }, - handleDragLeave () { + handleDragLeave (e) { + e.preventDefault(); + e.stopPropagation(); + + this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el)); + + if (this.dragTargets.length > 0) { + return; + } + this.setState({ draggingOver: false }); }, componentWillMount () { window.addEventListener('resize', this.handleResize, { passive: true }); - window.addEventListener('dragover', this.handleDragOver); - window.addEventListener('drop', this.handleDrop); + document.addEventListener('dragenter', this.handleDragEnter, false); + document.addEventListener('dragover', this.handleDragOver, false); + document.addEventListener('drop', this.handleDrop, false); + document.addEventListener('dragleave', this.handleDragLeave, false); this.props.dispatch(refreshTimeline('home')); this.props.dispatch(refreshNotifications()); @@ -72,8 +99,14 @@ const UI = React.createClass({ componentWillUnmount () { window.removeEventListener('resize', this.handleResize); - window.removeEventListener('dragover', this.handleDragOver); - window.removeEventListener('drop', this.handleDrop); + document.removeEventListener('dragenter', this.handleDragEnter); + document.removeEventListener('dragover', this.handleDragOver); + document.removeEventListener('drop', this.handleDrop); + document.removeEventListener('dragleave', this.handleDragLeave); + }, + + setRef (c) { + this.node = c; }, render () { @@ -100,7 +133,7 @@ const UI = React.createClass({ } return ( -
+
{mountedColumns} -- cgit From 3ac4455160f1fd95c33fbc053c9456f155b65a8a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 31 Mar 2017 12:08:51 +0200 Subject: :active and :focus states same as :hover for buttons --- app/assets/stylesheets/components.scss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 04d37546c..1c1e8bffc 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -21,7 +21,7 @@ text-decoration: none; transition: all 100ms ease-in; - &:hover { + &:hover, &:active, &:focus { background-color: lighten($color4, 7%); transition: all 200ms ease-out; } @@ -54,7 +54,7 @@ cursor: pointer; transition: all 100ms ease-in; - &:hover { + &:hover, &:active, &:focus { color: lighten($color1, 33%); transition: all 200ms ease-out; } @@ -79,7 +79,7 @@ &.inverted { color: lighten($color1, 33%); - &:hover { + &:hover, &:active, &:focus { color: lighten($color1, 26%); } @@ -105,7 +105,7 @@ outline: 0; transition: all 100ms ease-in; - &:hover { + &:hover, &:active, &:focus { color: lighten($color1, 26%); transition: all 200ms ease-out; } @@ -1640,7 +1640,7 @@ button.active i.fa-retweet { margin-top: 2px; } - &:hover { + &:hover, &:active, &:focus { img { opacity: 1; filter: none; -- cgit From 5e26295e061b653d0134caf0b54447a3683aecba Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 31 Mar 2017 13:54:36 +0200 Subject: Fix #700 - hide spoilers on static pages --- app/assets/javascripts/extras.jsx | 13 +++++++++++++ app/assets/stylesheets/stream_entries.scss | 18 ++++++++++++++++++ app/views/stream_entries/_detailed_status.html.haml | 6 ++++-- app/views/stream_entries/_simple_status.html.haml | 6 ++++-- config/locales/en.yml | 1 + 5 files changed, 40 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/extras.jsx b/app/assets/javascripts/extras.jsx index 5738863dd..c13feceff 100644 --- a/app/assets/javascripts/extras.jsx +++ b/app/assets/javascripts/extras.jsx @@ -24,4 +24,17 @@ $(() => { window.location.href = $(e.target).attr('href'); } }); + + $('.status__content__spoiler-link').on('click', e => { + e.preventDefault(); + const contentEl = $(e.target).parent().parent().find('div'); + + if (contentEl.is(':visible')) { + contentEl.hide(); + $(e.target).parent().attr('style', 'margin-bottom: 0'); + } else { + contentEl.show(); + $(e.target).parent().attr('style', null); + } + }); }); diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss index b9a9a1da3..4a6dc6aa4 100644 --- a/app/assets/stylesheets/stream_entries.scss +++ b/app/assets/stylesheets/stream_entries.scss @@ -97,6 +97,15 @@ a { color: $color4; } + + a.status__content__spoiler-link { + color: $color5; + background: $color3; + + &:hover { + background: lighten($color3, 8%); + } + } } .status__attachments { @@ -163,6 +172,15 @@ a { color: $color4; } + + a.status__content__spoiler-link { + color: $color5; + background: $color3; + + &:hover { + background: lighten($color3, 8%); + } + } } .detailed-status__meta { diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 8c0456b1f..11940883e 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -9,8 +9,10 @@ .status__content.e-content.p-name.emojify< - unless status.spoiler_text.blank? - %p= status.spoiler_text - %div{ style: "direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status) + %p{ style: 'margin-bottom: 0' }< + %span>= "#{status.spoiler_text} " + %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more') + %div{ style: "display: #{status.spoiler_text.blank? ? 'block' : 'none'}; direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status) - unless status.media_attachments.empty? - if status.media_attachments.first.video? diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index cb2c976ce..2eb9bf166 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -14,8 +14,10 @@ .status__content.e-content.p-name.emojify< - unless status.spoiler_text.blank? - %p= status.spoiler_text - %div{ style: "direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status) + %p{ style: 'margin-bottom: 0' }< + %span>= "#{status.spoiler_text} " + %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more') + %div{ style: "display: #{status.spoiler_text.blank? ? 'block' : 'none'}; direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status) - unless status.media_attachments.empty? .status__attachments diff --git a/config/locales/en.yml b/config/locales/en.yml index 965001e05..157f107a5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -138,6 +138,7 @@ en: statuses: open_in_web: Open in web over_character_limit: character limit of %{max} exceeded + show_more: Show more visibilities: private: Only show to followers public: Public -- cgit From bde5c0eaf9866386879d6b726296d7e3be1fc064 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 31 Mar 2017 14:02:07 +0200 Subject: Fix some views still not using counter caches --- app/views/accounts/_header.html.haml | 6 +++--- app/views/admin/accounts/show.html.haml | 6 +++--- app/views/api/v1/accounts/show.rabl | 6 +++--- app/views/api/v1/statuses/_show.rabl | 4 ++-- app/views/stream_entries/_detailed_status.html.haml | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) (limited to 'app') diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index e35b08317..0d43fba30 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -23,12 +23,12 @@ .counter{ class: active_nav_class(short_account_url(@account)) } = link_to short_account_url(@account), class: 'u-url u-uid' do %span.counter-label= t('accounts.posts') - %span.counter-number= number_with_delimiter @account.statuses.count + %span.counter-number= number_with_delimiter @account.statuses_count .counter{ class: active_nav_class(following_account_url(@account)) } = link_to following_account_url(@account) do %span.counter-label= t('accounts.following') - %span.counter-number= number_with_delimiter @account.following.count + %span.counter-number= number_with_delimiter @account.following_count .counter{ class: active_nav_class(followers_account_url(@account)) } = link_to followers_account_url(@account) do %span.counter-label= t('accounts.followers') - %span.counter-number= number_with_delimiter @account.followers.count + %span.counter-number= number_with_delimiter @account.followers_count diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index b528e161e..ba1c3bae7 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -47,13 +47,13 @@ %tr %th Follows - %td= @account.following.count + %td= @account.following_count %tr %th Followers - %td= @account.followers.count + %td= @account.followers_count %tr %th Statuses - %td= @account.statuses.count + %td= @account.statuses_count %tr %th Media attachments %td diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl index e21fe7941..32df0457a 100644 --- a/app/views/api/v1/accounts/show.rabl +++ b/app/views/api/v1/accounts/show.rabl @@ -6,6 +6,6 @@ node(:note) { |account| Formatter.instance.simplified_format(account) node(:url) { |account| TagManager.instance.url_for(account) } node(:avatar) { |account| full_asset_url(account.avatar.url(:original)) } node(:header) { |account| full_asset_url(account.header.url(:original)) } -node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : (account.try(:followers_count) || account.followers.count) } -node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : (account.try(:following_count) || account.following.count) } -node(:statuses_count) { |account| defined?(@statuses_counts_map) ? (@statuses_counts_map[account.id] || 0) : (account.try(:statuses_count) || account.statuses.count) } +node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : account.followers_count } +node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : account.following_count } +node(:statuses_count) { |account| defined?(@statuses_counts_map) ? (@statuses_counts_map[account.id] || 0) : account.statuses_count } diff --git a/app/views/api/v1/statuses/_show.rabl b/app/views/api/v1/statuses/_show.rabl index f384b6d14..54e8a86d8 100644 --- a/app/views/api/v1/statuses/_show.rabl +++ b/app/views/api/v1/statuses/_show.rabl @@ -3,8 +3,8 @@ attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, :sensitiv node(:uri) { |status| TagManager.instance.uri_for(status) } node(:content) { |status| Formatter.instance.format(status) } node(:url) { |status| TagManager.instance.url_for(status) } -node(:reblogs_count) { |status| defined?(@reblogs_counts_map) ? (@reblogs_counts_map[status.id] || 0) : (status.try(:reblogs_count) || status.reblogs.count) } -node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : (status.try(:favourites_count) || status.favourites.count) } +node(:reblogs_count) { |status| defined?(@reblogs_counts_map) ? (@reblogs_counts_map[status.id] || 0) : status.reblogs_count } +node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites_count } child :application do extends 'api/v1/apps/show' diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 11940883e..8495f28b9 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -41,11 +41,11 @@ · %span< = fa_icon('retweet') - %span= status.reblogs.count + %span= status.reblogs_count · %span< = fa_icon('star') - %span= status.favourites.count + %span= status.favourites_count - if user_signed_in? · -- cgit From 680f9efe9c4aa7fce1f4dd6a35ef4aca7a80c1f3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 31 Mar 2017 14:23:44 +0200 Subject: Fix web UI profile clickable area overlapping with follow button area --- .../features/account/components/header.jsx | 51 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx index e1aae3c77..a359963c4 100644 --- a/app/assets/javascripts/components/features/account/components/header.jsx +++ b/app/assets/javascripts/components/features/account/components/header.jsx @@ -4,6 +4,7 @@ import emojify from '../../../emoji'; import escapeTextContentForBrowser from 'escape-html'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import IconButton from '../../../components/icon_button'; +import { Motion, spring } from 'react-motion'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -11,6 +12,47 @@ const messages = defineMessages({ requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' } }); +const Avatar = React.createClass({ + + propTypes: { + account: ImmutablePropTypes.map.isRequired + }, + + getInitialState () { + return { + isHovered: false + }; + }, + + mixins: [PureRenderMixin], + + handleMouseOver () { + if (this.state.isHovered) return; + this.setState({ isHovered: true }); + }, + + handleMouseOut () { + if (!this.state.isHovered) return; + this.setState({ isHovered: false }); + }, + + render () { + const { account } = this.props; + const { isHovered } = this.state; + + return ( + + {({ radius }) => + + {account.get('acct')} + + } + + ); + } + +}); + const Header = React.createClass({ propTypes: { @@ -68,14 +110,9 @@ const Header = React.createClass({ return (
- -
- -
- - -
+ + @{account.get('acct')} {lockedIcon}
-- cgit From b4046c5957f16437910cdfe1ab45ee818118e7d7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 31 Mar 2017 19:59:54 +0200 Subject: Rework search --- .../javascripts/components/actions/search.jsx | 66 ++++++---- .../features/compose/components/drawer.jsx | 44 ------- .../features/compose/components/search.jsx | 114 +++++------------ .../features/compose/components/search_results.jsx | 68 ++++++++++ .../compose/components/sensitive_toggle.jsx | 31 ----- .../features/compose/components/spoiler_toggle.jsx | 27 ---- .../compose/containers/search_container.jsx | 17 ++- .../containers/search_results_container.jsx | 8 ++ .../components/features/compose/index.jsx | 64 ++++++++-- .../javascripts/components/reducers/accounts.jsx | 4 +- .../javascripts/components/reducers/search.jsx | 31 +++-- .../javascripts/components/reducers/statuses.jsx | 4 +- app/assets/stylesheets/components.scss | 137 +++++++++++++++++---- 13 files changed, 351 insertions(+), 264 deletions(-) delete mode 100644 app/assets/javascripts/components/features/compose/components/drawer.jsx create mode 100644 app/assets/javascripts/components/features/compose/components/search_results.jsx delete mode 100644 app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx delete mode 100644 app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx create mode 100644 app/assets/javascripts/components/features/compose/containers/search_results_container.jsx (limited to 'app') diff --git a/app/assets/javascripts/components/actions/search.jsx b/app/assets/javascripts/components/actions/search.jsx index e4af716ee..9d28ed11e 100644 --- a/app/assets/javascripts/components/actions/search.jsx +++ b/app/assets/javascripts/components/actions/search.jsx @@ -1,9 +1,12 @@ import api from '../api' -export const SEARCH_CHANGE = 'SEARCH_CHANGE'; -export const SEARCH_SUGGESTIONS_CLEAR = 'SEARCH_SUGGESTIONS_CLEAR'; -export const SEARCH_SUGGESTIONS_READY = 'SEARCH_SUGGESTIONS_READY'; -export const SEARCH_RESET = 'SEARCH_RESET'; +export const SEARCH_CHANGE = 'SEARCH_CHANGE'; +export const SEARCH_CLEAR = 'SEARCH_CLEAR'; +export const SEARCH_SHOW = 'SEARCH_SHOW'; + +export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST'; +export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS'; +export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL'; export function changeSearch(value) { return { @@ -12,42 +15,55 @@ export function changeSearch(value) { }; }; -export function clearSearchSuggestions() { - return { - type: SEARCH_SUGGESTIONS_CLEAR - }; -}; - -export function readySearchSuggestions(value, { accounts, hashtags, statuses }) { +export function clearSearch() { return { - type: SEARCH_SUGGESTIONS_READY, - value, - accounts, - hashtags, - statuses + type: SEARCH_CLEAR }; }; -export function fetchSearchSuggestions(value) { +export function submitSearch() { return (dispatch, getState) => { - if (getState().getIn(['search', 'loaded_value']) === value) { - return; - } + const value = getState().getIn(['search', 'value']); + + dispatch(fetchSearchRequest()); api(getState).get('/api/v1/search', { params: { q: value, - resolve: true, - limit: 4 + resolve: true } }).then(response => { - dispatch(readySearchSuggestions(value, response.data)); + dispatch(fetchSearchSuccess(response.data)); + }).catch(error => { + dispatch(fetchSearchFail(error)); }); }; }; -export function resetSearch() { +export function fetchSearchRequest() { + return { + type: SEARCH_FETCH_REQUEST + }; +}; + +export function fetchSearchSuccess(results) { + return { + type: SEARCH_FETCH_SUCCESS, + results, + accounts: results.accounts, + statuses: results.statuses + }; +}; + +export function fetchSearchFail(error) { + return { + type: SEARCH_FETCH_FAIL, + error + }; +}; + +export function showSearch() { return { - type: SEARCH_RESET + type: SEARCH_SHOW }; }; diff --git a/app/assets/javascripts/components/features/compose/components/drawer.jsx b/app/assets/javascripts/components/features/compose/components/drawer.jsx deleted file mode 100644 index ab67c86ea..000000000 --- a/app/assets/javascripts/components/features/compose/components/drawer.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Link } from 'react-router'; -import { injectIntl, defineMessages } from 'react-intl'; - -const messages = defineMessages({ - start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, - public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' }, - community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, - logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' } -}); - -const Drawer = ({ children, withHeader, intl }) => { - let header = ''; - - if (withHeader) { - header = ( -
- - - - - -
- ); - } - - return ( -
- {header} - -
- {children} -
-
- ); -}; - -Drawer.propTypes = { - withHeader: React.PropTypes.bool, - children: React.PropTypes.node, - intl: React.PropTypes.object -}; - -export default injectIntl(Drawer); diff --git a/app/assets/javascripts/components/features/compose/components/search.jsx b/app/assets/javascripts/components/features/compose/components/search.jsx index a0e8f82fb..8e86f053e 100644 --- a/app/assets/javascripts/components/features/compose/components/search.jsx +++ b/app/assets/javascripts/components/features/compose/components/search.jsx @@ -1,123 +1,67 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import Autosuggest from 'react-autosuggest'; -import AutosuggestAccountContainer from '../containers/autosuggest_account_container'; -import AutosuggestStatusContainer from '../containers/autosuggest_status_container'; -import { debounce } from 'react-decoration'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' } }); -const getSuggestionValue = suggestion => suggestion.value; - -const renderSuggestion = suggestion => { - if (suggestion.type === 'account') { - return ; - } else if (suggestion.type === 'hashtag') { - return #{suggestion.id}; - } else { - return ; - } -}; - -const renderSectionTitle = section => ( - -); - -const getSectionSuggestions = section => section.items; - -const outerStyle = { - padding: '10px', - lineHeight: '20px', - position: 'relative' -}; - -const iconStyle = { - position: 'absolute', - top: '18px', - right: '20px', - fontSize: '18px', - pointerEvents: 'none' -}; - const Search = React.createClass({ - contextTypes: { - router: React.PropTypes.object - }, - propTypes: { - suggestions: React.PropTypes.array.isRequired, value: React.PropTypes.string.isRequired, onChange: React.PropTypes.func.isRequired, + onSubmit: React.PropTypes.func.isRequired, onClear: React.PropTypes.func.isRequired, - onFetch: React.PropTypes.func.isRequired, - onReset: React.PropTypes.func.isRequired, + onShow: React.PropTypes.func.isRequired, intl: React.PropTypes.object.isRequired }, mixins: [PureRenderMixin], - onChange (_, { newValue }) { - if (typeof newValue !== 'string') { - return; - } - - this.props.onChange(newValue); + handleChange (e) { + this.props.onChange(e.target.value); }, - onSuggestionsClearRequested () { + handleClear (e) { + e.preventDefault(); this.props.onClear(); }, - @debounce(500) - onSuggestionsFetchRequested ({ value }) { - value = value.replace('#', ''); - this.props.onFetch(value.trim()); + handleKeyDown (e) { + if (e.key === 'Enter') { + e.preventDefault(); + this.props.onSubmit(); + } }, - onSuggestionSelected (_, { suggestion }) { - if (suggestion.type === 'account') { - this.context.router.push(`/accounts/${suggestion.id}`); - } else if(suggestion.type === 'hashtag') { - this.context.router.push(`/timelines/tag/${suggestion.id}`); - } else { - this.context.router.push(`/statuses/${suggestion.id}`); - } + handleFocus () { + this.props.onShow(); }, render () { - const inputProps = { - placeholder: this.props.intl.formatMessage(messages.placeholder), - value: this.props.value, - onChange: this.onChange, - className: 'search__input' - }; + const { intl, value } = this.props; + const hasValue = value.length > 0; return ( -
- + -
+
+ + +
); - }, + } }); diff --git a/app/assets/javascripts/components/features/compose/components/search_results.jsx b/app/assets/javascripts/components/features/compose/components/search_results.jsx new file mode 100644 index 000000000..fd05e7f7e --- /dev/null +++ b/app/assets/javascripts/components/features/compose/components/search_results.jsx @@ -0,0 +1,68 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import AccountContainer from '../../../containers/account_container'; +import StatusContainer from '../../../containers/status_container'; +import { Link } from 'react-router'; + +const SearchResults = React.createClass({ + + propTypes: { + results: ImmutablePropTypes.map.isRequired + }, + + mixins: [PureRenderMixin], + + render () { + const { results } = this.props; + + let accounts, statuses, hashtags; + let count = 0; + + if (results.get('accounts') && results.get('accounts').size > 0) { + count += results.get('accounts').size; + accounts = ( +
+ {results.get('accounts').map(accountId => )} +
+ ); + } + + if (results.get('statuses') && results.get('statuses').size > 0) { + count += results.get('statuses').size; + statuses = ( +
+ {results.get('statuses').map(statusId => )} +
+ ); + } + + if (results.get('hashtags') && results.get('hashtags').size > 0) { + count += results.get('hashtags').size; + hashtags = ( +
+ {results.get('hashtags').map(hashtag => + + #{hashtag} + + )} +
+ ); + } + + return ( +
+
+ +
+ + {accounts} + {statuses} + {hashtags} +
+ ); + } + +}); + +export default SearchResults; diff --git a/app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx b/app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx deleted file mode 100644 index 97cc9487e..000000000 --- a/app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import { FormattedMessage } from 'react-intl'; -import Toggle from 'react-toggle'; -import Collapsable from '../../../components/collapsable'; - -const SensitiveToggle = React.createClass({ - - propTypes: { - hasMedia: React.PropTypes.bool, - isSensitive: React.PropTypes.bool, - onChange: React.PropTypes.func.isRequired - }, - - mixins: [PureRenderMixin], - - render () { - const { hasMedia, isSensitive, onChange } = this.props; - - return ( - - - - ); - } - -}); - -export default SensitiveToggle; diff --git a/app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx b/app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx deleted file mode 100644 index 1c59e4393..000000000 --- a/app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import { FormattedMessage } from 'react-intl'; -import Toggle from 'react-toggle'; - -const SpoilerToggle = React.createClass({ - - propTypes: { - isSpoiler: React.PropTypes.bool, - onChange: React.PropTypes.func.isRequired - }, - - mixins: [PureRenderMixin], - - render () { - const { isSpoiler, onChange } = this.props; - - return ( - - ); - } - -}); - -export default SpoilerToggle; diff --git a/app/assets/javascripts/components/features/compose/containers/search_container.jsx b/app/assets/javascripts/components/features/compose/containers/search_container.jsx index 17a68f2fc..96709215a 100644 --- a/app/assets/javascripts/components/features/compose/containers/search_container.jsx +++ b/app/assets/javascripts/components/features/compose/containers/search_container.jsx @@ -1,14 +1,13 @@ import { connect } from 'react-redux'; import { changeSearch, - clearSearchSuggestions, - fetchSearchSuggestions, - resetSearch + clearSearch, + submitSearch, + showSearch } from '../../../actions/search'; import Search from '../components/search'; const mapStateToProps = state => ({ - suggestions: state.getIn(['search', 'suggestions']), value: state.getIn(['search', 'value']) }); @@ -19,15 +18,15 @@ const mapDispatchToProps = dispatch => ({ }, onClear () { - dispatch(clearSearchSuggestions()); + dispatch(clearSearch()); }, - onFetch (value) { - dispatch(fetchSearchSuggestions(value)); + onSubmit () { + dispatch(submitSearch()); }, - onReset () { - dispatch(resetSearch()); + onShow () { + dispatch(showSearch()); } }); diff --git a/app/assets/javascripts/components/features/compose/containers/search_results_container.jsx b/app/assets/javascripts/components/features/compose/containers/search_results_container.jsx new file mode 100644 index 000000000..e5911fd38 --- /dev/null +++ b/app/assets/javascripts/components/features/compose/containers/search_results_container.jsx @@ -0,0 +1,8 @@ +import { connect } from 'react-redux'; +import SearchResults from '../components/search_results'; + +const mapStateToProps = state => ({ + results: state.getIn(['search', 'results']) +}); + +export default connect(mapStateToProps)(SearchResults); diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx index 15e2c5809..d21e7a9bc 100644 --- a/app/assets/javascripts/components/features/compose/index.jsx +++ b/app/assets/javascripts/components/features/compose/index.jsx @@ -1,17 +1,34 @@ -import Drawer from './components/drawer'; import ComposeFormContainer from './containers/compose_form_container'; import UploadFormContainer from './containers/upload_form_container'; import NavigationContainer from './containers/navigation_container'; import PureRenderMixin from 'react-addons-pure-render-mixin'; -import SearchContainer from './containers/search_container'; import { connect } from 'react-redux'; import { mountCompose, unmountCompose } from '../../actions/compose'; +import { Link } from 'react-router'; +import { injectIntl, defineMessages } from 'react-intl'; +import SearchContainer from './containers/search_container'; +import { Motion, spring } from 'react-motion'; +import SearchResultsContainer from './containers/search_results_container'; + +const messages = defineMessages({ + start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, + public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' }, + community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, + preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, + logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' } +}); + +const mapStateToProps = state => ({ + showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) +}); const Compose = React.createClass({ propTypes: { dispatch: React.PropTypes.func.isRequired, - withHeader: React.PropTypes.bool + withHeader: React.PropTypes.bool, + showSearch: React.PropTypes.bool, + intl: React.PropTypes.object.isRequired }, mixins: [PureRenderMixin], @@ -25,15 +42,46 @@ const Compose = React.createClass({ }, render () { + const { withHeader, showSearch, intl } = this.props; + + let header = ''; + + if (withHeader) { + header = ( +
+ + + + + +
+ ); + } + return ( - +
+ {header} + - - - + +
+
+ + +
+ + + {({ x }) => +
+ +
+ } +
+
+
); } }); -export default connect()(Compose); +export default connect(mapStateToProps)(injectIntl(Compose)); diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx index 6ce41670d..df9440093 100644 --- a/app/assets/javascripts/components/reducers/accounts.jsx +++ b/app/assets/javascripts/components/reducers/accounts.jsx @@ -33,7 +33,7 @@ import { STATUS_FETCH_SUCCESS, CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; -import { SEARCH_SUGGESTIONS_READY } from '../actions/search'; +import { SEARCH_FETCH_SUCCESS } from '../actions/search'; import { NOTIFICATIONS_UPDATE, NOTIFICATIONS_REFRESH_SUCCESS, @@ -97,7 +97,7 @@ export default function accounts(state = initialState, action) { return normalizeAccounts(state, action.accounts); case NOTIFICATIONS_REFRESH_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS: - case SEARCH_SUGGESTIONS_READY: + case SEARCH_FETCH_SUCCESS: return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses); case TIMELINE_REFRESH_SUCCESS: case TIMELINE_EXPAND_SUCCESS: diff --git a/app/assets/javascripts/components/reducers/search.jsx b/app/assets/javascripts/components/reducers/search.jsx index e95f9ed79..b3fe6c7be 100644 --- a/app/assets/javascripts/components/reducers/search.jsx +++ b/app/assets/javascripts/components/reducers/search.jsx @@ -1,14 +1,17 @@ import { SEARCH_CHANGE, - SEARCH_SUGGESTIONS_READY, - SEARCH_RESET + SEARCH_CLEAR, + SEARCH_FETCH_SUCCESS, + SEARCH_SHOW } from '../actions/search'; +import { COMPOSE_MENTION, COMPOSE_REPLY } from '../actions/compose'; import Immutable from 'immutable'; const initialState = Immutable.Map({ value: '', - loaded_value: '', - suggestions: [] + submitted: false, + hidden: false, + results: Immutable.Map() }); const normalizeSuggestions = (state, value, accounts, hashtags, statuses) => { @@ -69,14 +72,24 @@ export default function search(state = initialState, action) { switch(action.type) { case SEARCH_CHANGE: return state.set('value', action.value); - case SEARCH_SUGGESTIONS_READY: - return normalizeSuggestions(state, action.value, action.accounts, action.hashtags, action.statuses); - case SEARCH_RESET: + case SEARCH_CLEAR: return state.withMutations(map => { - map.set('suggestions', []); map.set('value', ''); - map.set('loaded_value', ''); + map.set('results', Immutable.Map()); + map.set('submitted', false); + map.set('hidden', false); }); + case SEARCH_SHOW: + return state.set('hidden', false); + case COMPOSE_REPLY: + case COMPOSE_MENTION: + return state.set('hidden', true); + case SEARCH_FETCH_SUCCESS: + return state.set('results', Immutable.Map({ + accounts: Immutable.List(action.results.accounts.map(item => item.id)), + statuses: Immutable.List(action.results.statuses.map(item => item.id)), + hashtags: Immutable.List(action.results.hashtags) + })).set('submitted', true); default: return state; } diff --git a/app/assets/javascripts/components/reducers/statuses.jsx b/app/assets/javascripts/components/reducers/statuses.jsx index 1669b8c65..ca8fa7a01 100644 --- a/app/assets/javascripts/components/reducers/statuses.jsx +++ b/app/assets/javascripts/components/reducers/statuses.jsx @@ -32,7 +32,7 @@ import { FAVOURITED_STATUSES_FETCH_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS } from '../actions/favourites'; -import { SEARCH_SUGGESTIONS_READY } from '../actions/search'; +import { SEARCH_FETCH_SUCCESS } from '../actions/search'; import Immutable from 'immutable'; const normalizeStatus = (state, status) => { @@ -109,7 +109,7 @@ export default function statuses(state = initialState, action) { case NOTIFICATIONS_EXPAND_SUCCESS: case FAVOURITED_STATUSES_FETCH_SUCCESS: case FAVOURITED_STATUSES_EXPAND_SUCCESS: - case SEARCH_SUGGESTIONS_READY: + case SEARCH_FETCH_SUCCESS: return normalizeStatuses(state, action.statuses); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 1c1e8bffc..9c138e495 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -764,8 +764,19 @@ a.status__content__spoiler-link { } } +.drawer__pager { + box-sizing: border-box; + padding: 0; + flex-grow: 1; + position: relative; + overflow: hidden; + display: flex; +} + .drawer__inner { - //background: linear-gradient(rgba(lighten($color1, 13%), 1), rgba(lighten($color1, 13%), 0.65)); + position: absolute; + top: 0; + left: 0; background: lighten($color1, 13%); box-sizing: border-box; padding: 0; @@ -773,7 +784,12 @@ a.status__content__spoiler-link { flex-direction: column; overflow: hidden; overflow-y: auto; - flex-grow: 1; + width: 100%; + height: 100%; + + &.darker { + background: $color1; + } } .drawer__header { @@ -1224,26 +1240,6 @@ button.active i.fa-retweet { } } -.search { - .fa { - color: $color3; - } -} - -.search__input { - box-sizing: border-box; - display: block; - width: 100%; - border: none; - padding: 10px; - padding-right: 30px; - font-family: inherit; - background: $color1; - color: $color3; - font-size: 14px; - margin: 0; -} - .loading-indicator { color: $color2; } @@ -1723,3 +1719,100 @@ button.active i.fa-retweet { box-shadow: 2px 4px 6px rgba($color8, 0.1); } } + +.search { + position: relative; + margin-bottom: 10px; +} + +.search__input { + padding-right: 30px; + color: $color2; + outline: 0; + box-sizing: border-box; + display: block; + width: 100%; + border: none; + padding: 10px; + padding-right: 30px; + font-family: inherit; + background: $color1; + color: $color3; + font-size: 14px; + margin: 0; + + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, &:focus, &:active { + outline: 0 !important; + } + + &:focus { + background: lighten($color1, 4%); + } +} + +.search__icon { + .fa { + position: absolute; + top: 10px; + right: 10px; + z-index: 2; + display: inline-block; + opacity: 0; + transition: all 100ms linear; + font-size: 18px; + width: 18px; + height: 18px; + color: $color2; + cursor: default; + pointer-events: none; + + &.active { + pointer-events: auto; + opacity: 0.3; + } + } + + .fa-search { + transform: translateZ(0) rotate(90deg); + + &.active { + pointer-events: none; + transform: translateZ(0) rotate(0deg); + } + } + + .fa-times-circle { + top: 11px; + transform: translateZ(0) rotate(0deg); + cursor: pointer; + + &.active { + transform: translateZ(0) rotate(90deg); + } + } +} + +.search-results__header { + color: lighten($color1, 26%); + background: lighten($color1, 2%); + border-bottom: 1px solid darken($color1, 4%); + padding: 15px 10px; + font-size: 14px; + font-weight: 500; +} + +.search-results__hashtag { + display: block; + padding: 10px; + color: $color2; + text-decoration: none; + + &:hover, &:active, &:focus { + color: lighten($color2, 4%); + text-decoration: underline; + } +} -- cgit From d93d6f5124d7e120ad1542a65b72792e31f86b22 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 31 Mar 2017 22:44:12 +0200 Subject: Fix reworked search --- .../javascripts/components/actions/search.jsx | 4 ++++ .../features/compose/components/search.jsx | 5 +++-- .../compose/containers/search_container.jsx | 3 ++- .../components/features/compose/index.jsx | 4 ++-- .../components/features/getting_started/index.jsx | 2 -- .../components/reducers/relationships.jsx | 22 +++++++++++----------- app/assets/stylesheets/components.scss | 9 ++++++--- app/services/search_service.rb | 4 ++-- 8 files changed, 30 insertions(+), 23 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/actions/search.jsx b/app/assets/javascripts/components/actions/search.jsx index 9d28ed11e..df3ae0db1 100644 --- a/app/assets/javascripts/components/actions/search.jsx +++ b/app/assets/javascripts/components/actions/search.jsx @@ -25,6 +25,10 @@ export function submitSearch() { return (dispatch, getState) => { const value = getState().getIn(['search', 'value']); + if (value.length === 0) { + return; + } + dispatch(fetchSearchRequest()); api(getState).get('/api/v1/search', { diff --git a/app/assets/javascripts/components/features/compose/components/search.jsx b/app/assets/javascripts/components/features/compose/components/search.jsx index 8e86f053e..936e003f2 100644 --- a/app/assets/javascripts/components/features/compose/components/search.jsx +++ b/app/assets/javascripts/components/features/compose/components/search.jsx @@ -10,6 +10,7 @@ const Search = React.createClass({ propTypes: { value: React.PropTypes.string.isRequired, + submitted: React.PropTypes.bool, onChange: React.PropTypes.func.isRequired, onSubmit: React.PropTypes.func.isRequired, onClear: React.PropTypes.func.isRequired, @@ -40,8 +41,8 @@ const Search = React.createClass({ }, render () { - const { intl, value } = this.props; - const hasValue = value.length > 0; + const { intl, value, submitted } = this.props; + const hasValue = value.length > 0 || submitted; return (
diff --git a/app/assets/javascripts/components/features/compose/containers/search_container.jsx b/app/assets/javascripts/components/features/compose/containers/search_container.jsx index 96709215a..906c0c28c 100644 --- a/app/assets/javascripts/components/features/compose/containers/search_container.jsx +++ b/app/assets/javascripts/components/features/compose/containers/search_container.jsx @@ -8,7 +8,8 @@ import { import Search from '../components/search'; const mapStateToProps = state => ({ - value: state.getIn(['search', 'value']) + value: state.getIn(['search', 'value']), + submitted: state.getIn(['search', 'submitted']) }); const mapDispatchToProps = dispatch => ({ diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx index d21e7a9bc..d4df259dc 100644 --- a/app/assets/javascripts/components/features/compose/index.jsx +++ b/app/assets/javascripts/components/features/compose/index.jsx @@ -70,9 +70,9 @@ const Compose = React.createClass({
- + {({ x }) => -
+
} diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx index 6f9e988ba..8253ad017 100644 --- a/app/assets/javascripts/components/features/getting_started/index.jsx +++ b/app/assets/javascripts/components/features/getting_started/index.jsx @@ -43,8 +43,6 @@ const GettingStarted = ({ intl, me }) => {
-

-

tootsuite/mastodon, apps: }} />

diff --git a/app/assets/javascripts/components/reducers/relationships.jsx b/app/assets/javascripts/components/reducers/relationships.jsx index 591f8034b..c65c48b43 100644 --- a/app/assets/javascripts/components/reducers/relationships.jsx +++ b/app/assets/javascripts/components/reducers/relationships.jsx @@ -23,16 +23,16 @@ const initialState = Immutable.Map(); export default function relationships(state = initialState, action) { switch(action.type) { - case ACCOUNT_FOLLOW_SUCCESS: - case ACCOUNT_UNFOLLOW_SUCCESS: - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_UNBLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - case ACCOUNT_UNMUTE_SUCCESS: - return normalizeRelationship(state, action.relationship); - case RELATIONSHIPS_FETCH_SUCCESS: - return normalizeRelationships(state, action.relationships); - default: - return state; + case ACCOUNT_FOLLOW_SUCCESS: + case ACCOUNT_UNFOLLOW_SUCCESS: + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_UNBLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + case ACCOUNT_UNMUTE_SUCCESS: + return normalizeRelationship(state, action.relationship); + case RELATIONSHIPS_FETCH_SUCCESS: + return normalizeRelationships(state, action.relationships); + default: + return state; } }; diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 9c138e495..d7589d9b0 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1120,9 +1120,8 @@ a.status__content__spoiler-link { box-sizing: border-box; overflow-y: auto; padding-bottom: 235px; - background: image-url('mastodon-getting-started.png') no-repeat bottom left; - height: auto; - min-height: 100%; + background: image-url('mastodon-getting-started.png') no-repeat 0 100% local; + height: 100%; p { color: $color2; @@ -1793,6 +1792,10 @@ button.active i.fa-retweet { &.active { transform: translateZ(0) rotate(90deg); } + + &:hover { + color: $color5; + } } } diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 159c03713..e9745010b 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -2,10 +2,10 @@ class SearchService < BaseService def call(query, limit, resolve = false, account = nil) - return if query.blank? - results = { accounts: [], hashtags: [], statuses: [] } + return results if query.blank? + if query =~ /\Ahttps?:\/\// resource = FetchRemoteResourceService.new.call(query) -- cgit From 13dfd8d109442ffdd90dbd533a426b04b68e5119 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 1 Apr 2017 15:17:35 +0200 Subject: Improve mobile tabs a little --- .../components/features/ui/components/tabs_bar.jsx | 28 ++++++++++++------- app/assets/stylesheets/components.scss | 31 +++++++++++++++++++++- 2 files changed, 48 insertions(+), 11 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx index 225a6a5fc..6cdb29dbf 100644 --- a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx +++ b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx @@ -1,15 +1,23 @@ import { Link } from 'react-router'; import { FormattedMessage } from 'react-intl'; -const TabsBar = () => { - return ( -
- - - - -
- ); -}; +const TabsBar = React.createClass({ + + render () { + return ( +
+ + + + + + + + +
+ ); + } + +}); export default TabsBar; diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index d7589d9b0..a4dce7f18 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -858,11 +858,25 @@ a.status__content__spoiler-link { font-size:12px; font-weight: 500; border-bottom: 2px solid lighten($color1, 8%); + transition: all 200ms linear; + + .fa { + font-weight: 400; + } &.active { border-bottom: 2px solid $color4; color: $color4; } + + &:hover, &:focus, &:active { + background: lighten($color1, 14%); + transition: all 100ms linear; + } + + span { + display: none; + } } @media screen and (min-width: 360px) { @@ -870,6 +884,22 @@ a.status__content__spoiler-link { margin: 10px; margin-bottom: 0; } + + .search { + margin-bottom: 10px; + } +} + +@media screen and (min-width: 600px) { + .tabs-bar__link { + .fa { + margin-right: 5px; + } + + span { + display: inline; + } + } } @media screen and (min-width: 1025px) { @@ -1721,7 +1751,6 @@ button.active i.fa-retweet { .search { position: relative; - margin-bottom: 10px; } .search__input { -- cgit From f693ab69f3596c689d9df9ea2e749dc614839a8e Mon Sep 17 00:00:00 2001 From: Maxime BORGES Date: Sat, 1 Apr 2017 20:17:28 +0200 Subject: Fix word-break in profile's note on profile page and profile component --- app/assets/stylesheets/accounts.scss | 1 + app/assets/stylesheets/components.scss | 1 + 2 files changed, 2 insertions(+) (limited to 'app') diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss index 7c48c91f3..25e24a95a 100644 --- a/app/assets/stylesheets/accounts.scss +++ b/app/assets/stylesheets/accounts.scss @@ -311,6 +311,7 @@ padding: 10px; padding-top: 15px; color: $color3; + word-wrap: break-word; } } } diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index a4dce7f18..3db9ae852 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -424,6 +424,7 @@ a.status__content__spoiler-link { .account__header__content { word-wrap: break-word; + word-break: break-all; font-weight: 400; overflow: hidden; color: $color3; -- cgit From 60ebfa182f944d5803dd6f3d54aa5e9ef24fc922 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 1 Apr 2017 22:11:28 +0200 Subject: Made modal system more generic --- .../javascripts/components/actions/modal.jsx | 25 +--- .../javascripts/components/components/lightbox.jsx | 82 ---------- .../components/containers/status_container.jsx | 4 +- .../components/features/status/index.jsx | 4 +- .../features/ui/components/media_modal.jsx | 134 +++++++++++++++++ .../features/ui/components/modal_root.jsx | 80 ++++++++++ .../features/ui/containers/modal_container.jsx | 166 +-------------------- .../javascripts/components/features/ui/index.jsx | 4 +- .../javascripts/components/reducers/modal.jsx | 30 +--- app/assets/stylesheets/components.scss | 46 +++++- 10 files changed, 285 insertions(+), 290 deletions(-) delete mode 100644 app/assets/javascripts/components/components/lightbox.jsx create mode 100644 app/assets/javascripts/components/features/ui/components/media_modal.jsx create mode 100644 app/assets/javascripts/components/features/ui/components/modal_root.jsx (limited to 'app') diff --git a/app/assets/javascripts/components/actions/modal.jsx b/app/assets/javascripts/components/actions/modal.jsx index d19218c48..615cd6bfe 100644 --- a/app/assets/javascripts/components/actions/modal.jsx +++ b/app/assets/javascripts/components/actions/modal.jsx @@ -1,14 +1,11 @@ -export const MEDIA_OPEN = 'MEDIA_OPEN'; +export const MODAL_OPEN = 'MODAL_OPEN'; export const MODAL_CLOSE = 'MODAL_CLOSE'; -export const MODAL_INDEX_DECREASE = 'MODAL_INDEX_DECREASE'; -export const MODAL_INDEX_INCREASE = 'MODAL_INDEX_INCREASE'; - -export function openMedia(media, index) { +export function openModal(type, props) { return { - type: MEDIA_OPEN, - media, - index + type: MODAL_OPEN, + modalType: type, + modalProps: props }; }; @@ -17,15 +14,3 @@ export function closeModal() { type: MODAL_CLOSE }; }; - -export function decreaseIndexInModal() { - return { - type: MODAL_INDEX_DECREASE - }; -}; - -export function increaseIndexInModal() { - return { - type: MODAL_INDEX_INCREASE - }; -}; diff --git a/app/assets/javascripts/components/components/lightbox.jsx b/app/assets/javascripts/components/components/lightbox.jsx deleted file mode 100644 index f04ca47ba..000000000 --- a/app/assets/javascripts/components/components/lightbox.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import IconButton from './icon_button'; -import { Motion, spring } from 'react-motion'; -import { injectIntl } from 'react-intl'; - -const overlayStyle = { - position: 'fixed', - top: '0', - left: '0', - width: '100%', - height: '100%', - background: 'rgba(0, 0, 0, 0.5)', - display: 'flex', - justifyContent: 'center', - alignContent: 'center', - flexDirection: 'row', - zIndex: '9999' -}; - -const dialogStyle = { - color: '#282c37', - boxShadow: '0 0 30px rgba(0, 0, 0, 0.8)', - margin: 'auto', - position: 'relative' -}; - -const closeStyle = { - position: 'absolute', - top: '4px', - right: '4px' -}; - -const Lightbox = React.createClass({ - - propTypes: { - isVisible: React.PropTypes.bool, - onOverlayClicked: React.PropTypes.func, - onCloseClicked: React.PropTypes.func, - intl: React.PropTypes.object.isRequired, - children: React.PropTypes.node - }, - - mixins: [PureRenderMixin], - - componentDidMount () { - this._listener = e => { - if (this.props.isVisible && e.key === 'Escape') { - this.props.onCloseClicked(); - } - }; - - window.addEventListener('keyup', this._listener); - }, - - componentWillUnmount () { - window.removeEventListener('keyup', this._listener); - }, - - stopPropagation (e) { - e.stopPropagation(); - }, - - render () { - const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props; - - return ( - - {({ backgroundOpacity, opacity, y }) => -
-
- - {children} -
-
- } -
- ); - } - -}); - -export default injectIntl(Lightbox); diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx index e7543bc39..fd3fbe4c3 100644 --- a/app/assets/javascripts/components/containers/status_container.jsx +++ b/app/assets/javascripts/components/containers/status_container.jsx @@ -17,7 +17,7 @@ import { } from '../actions/accounts'; import { deleteStatus } from '../actions/statuses'; import { initReport } from '../actions/reports'; -import { openMedia } from '../actions/modal'; +import { openModal } from '../actions/modal'; import { createSelector } from 'reselect' import { isMobile } from '../is_mobile' @@ -63,7 +63,7 @@ const mapDispatchToProps = (dispatch) => ({ }, onOpenMedia (media, index) { - dispatch(openMedia(media, index)); + dispatch(openModal('MEDIA', { media, index })); }, onBlock (account) { diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx index 6a7635cc6..f98fe1b01 100644 --- a/app/assets/javascripts/components/features/status/index.jsx +++ b/app/assets/javascripts/components/features/status/index.jsx @@ -28,7 +28,7 @@ import { import { ScrollContainer } from 'react-router-scroll'; import ColumnBackButton from '../../components/column_back_button'; import StatusContainer from '../../containers/status_container'; -import { openMedia } from '../../actions/modal'; +import { openModal } from '../../actions/modal'; import { isMobile } from '../../is_mobile' const makeMapStateToProps = () => { @@ -99,7 +99,7 @@ const Status = React.createClass({ }, handleOpenMedia (media, index) { - this.props.dispatch(openMedia(media, index)); + this.props.dispatch(openModal('MEDIA', { media, index })); }, handleReport (status) { diff --git a/app/assets/javascripts/components/features/ui/components/media_modal.jsx b/app/assets/javascripts/components/features/ui/components/media_modal.jsx new file mode 100644 index 000000000..e8b718094 --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/media_modal.jsx @@ -0,0 +1,134 @@ +import Lightbox from '../../../components/lightbox'; +import LoadingIndicator from '../../../components/loading_indicator'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ExtendedVideoPlayer from '../../../components/extended_video_player'; +import ImageLoader from 'react-imageloader'; +import { defineMessages, injectIntl } from 'react-intl'; +import IconButton from '../../../components/icon_button'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' } +}); + +const leftNavStyle = { + position: 'absolute', + background: 'rgba(0, 0, 0, 0.5)', + padding: '30px 15px', + cursor: 'pointer', + fontSize: '24px', + top: '0', + left: '-61px', + boxSizing: 'border-box', + height: '100%', + display: 'flex', + alignItems: 'center' +}; + +const rightNavStyle = { + position: 'absolute', + background: 'rgba(0, 0, 0, 0.5)', + padding: '30px 15px', + cursor: 'pointer', + fontSize: '24px', + top: '0', + right: '-61px', + boxSizing: 'border-box', + height: '100%', + display: 'flex', + alignItems: 'center' +}; + +const closeStyle = { + position: 'absolute', + top: '4px', + right: '4px' +}; + +const MediaModal = React.createClass({ + + propTypes: { + media: ImmutablePropTypes.list.isRequired, + index: React.PropTypes.number.isRequired, + onClose: React.PropTypes.func.isRequired, + intl: React.PropTypes.object.isRequired + }, + + getInitialState () { + return { + index: null + }; + }, + + mixins: [PureRenderMixin], + + handleNextClick () { + this.setState({ index: (this.getIndex() + 1) % this.props.media.size}); + }, + + handlePrevClick () { + this.setState({ index: (this.getIndex() - 1) % this.props.media.size}); + }, + + handleKeyUp (e) { + switch(e.key) { + case 'ArrowLeft': + this.handlePrevClick(); + break; + case 'ArrowRight': + this.handleNextClick(); + break; + } + }, + + componentDidMount () { + window.addEventListener('keyup', this.handleKeyUp, false); + }, + + componentWillUnmount () { + window.removeEventListener('keyup', this.handleKeyUp); + }, + + getIndex () { + return this.state.index !== null ? this.state.index : this.props.index; + }, + + render () { + const { media, intl, onClose } = this.props; + + const index = this.getIndex(); + const attachment = media.get(index); + const url = attachment.get('url'); + + let leftNav, rightNav, content; + + leftNav = rightNav = content = ''; + + if (media.size > 1) { + leftNav =
; + rightNav =
; + } + + if (attachment.get('type') === 'image') { + content = ; + } else if (attachment.get('type') === 'gifv') { + content = ; + } + + return ( +
+ {leftNav} + +
+ + {content} +
+ + {rightNav} +
+ ); + } + +}); + +export default injectIntl(MediaModal); diff --git a/app/assets/javascripts/components/features/ui/components/modal_root.jsx b/app/assets/javascripts/components/features/ui/components/modal_root.jsx new file mode 100644 index 000000000..d2ae5e145 --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx @@ -0,0 +1,80 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import MediaModal from './media_modal'; +import { TransitionMotion, spring } from 'react-motion'; + +const MODAL_COMPONENTS = { + 'MEDIA': MediaModal +}; + +const ModalRoot = React.createClass({ + + propTypes: { + type: React.PropTypes.string, + props: React.PropTypes.object, + onClose: React.PropTypes.func.isRequired + }, + + mixins: [PureRenderMixin], + + handleKeyUp (e) { + if (e.key === 'Escape' && !!this.props.type) { + this.props.onClose(); + } + }, + + componentDidMount () { + window.addEventListener('keyup', this.handleKeyUp, false); + }, + + componentWillUnmount () { + window.removeEventListener('keyup', this.handleKeyUp); + }, + + willEnter () { + return { opacity: 0, scale: 0.98 }; + }, + + willLeave () { + return { opacity: spring(0), scale: spring(0.98) }; + }, + + render () { + const { type, props, onClose } = this.props; + const items = []; + + if (!!type) { + items.push({ + key: type, + data: { type, props }, + style: { opacity: spring(1), scale: spring(1, { stiffness: 120, damping: 14 }) } + }); + } + + return ( + + {interpolatedStyles => +
+ {interpolatedStyles.map(({ key, data: { type, props }, style }) => { + const SpecificComponent = MODAL_COMPONENTS[type]; + + return ( +
+
+
+ +
+
+ ); + })} +
+ } + + ); + } + +}); + +export default ModalRoot; diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx index e3c4281b9..26d77818c 100644 --- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx +++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx @@ -1,170 +1,16 @@ import { connect } from 'react-redux'; -import { - closeModal, - decreaseIndexInModal, - increaseIndexInModal -} from '../../../actions/modal'; -import Lightbox from '../../../components/lightbox'; -import ImageLoader from 'react-imageloader'; -import LoadingIndicator from '../../../components/loading_indicator'; -import PureRenderMixin from 'react-addons-pure-render-mixin'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ExtendedVideoPlayer from '../../../components/extended_video_player'; +import { closeModal } from '../../../actions/modal'; +import ModalRoot from '../components/modal_root'; const mapStateToProps = state => ({ - media: state.getIn(['modal', 'media']), - index: state.getIn(['modal', 'index']), - isVisible: state.getIn(['modal', 'open']) + type: state.get('modal').modalType, + props: state.get('modal').modalProps }); const mapDispatchToProps = dispatch => ({ - onCloseClicked () { + onClose () { dispatch(closeModal()); }, - - onOverlayClicked () { - dispatch(closeModal()); - }, - - onNextClicked () { - dispatch(increaseIndexInModal()); - }, - - onPrevClicked () { - dispatch(decreaseIndexInModal()); - } -}); - -const imageStyle = { - display: 'block', - maxWidth: '80vw', - maxHeight: '80vh' -}; - -const loadingStyle = { - width: '400px', - paddingBottom: '120px' -}; - -const preloader = () => ( -
- -
-); - -const leftNavStyle = { - position: 'absolute', - background: 'rgba(0, 0, 0, 0.5)', - padding: '30px 15px', - cursor: 'pointer', - fontSize: '24px', - top: '0', - left: '-61px', - boxSizing: 'border-box', - height: '100%', - display: 'flex', - alignItems: 'center' -}; - -const rightNavStyle = { - position: 'absolute', - background: 'rgba(0, 0, 0, 0.5)', - padding: '30px 15px', - cursor: 'pointer', - fontSize: '24px', - top: '0', - right: '-61px', - boxSizing: 'border-box', - height: '100%', - display: 'flex', - alignItems: 'center' -}; - -const Modal = React.createClass({ - - propTypes: { - media: ImmutablePropTypes.list, - index: React.PropTypes.number.isRequired, - isVisible: React.PropTypes.bool, - onCloseClicked: React.PropTypes.func, - onOverlayClicked: React.PropTypes.func, - onNextClicked: React.PropTypes.func, - onPrevClicked: React.PropTypes.func - }, - - mixins: [PureRenderMixin], - - handleNextClick () { - this.props.onNextClicked(); - }, - - handlePrevClick () { - this.props.onPrevClicked(); - }, - - componentDidMount () { - this._listener = e => { - if (!this.props.isVisible) { - return; - } - - switch(e.key) { - case 'ArrowLeft': - this.props.onPrevClicked(); - break; - case 'ArrowRight': - this.props.onNextClicked(); - break; - } - }; - - window.addEventListener('keyup', this._listener); - }, - - componentWillUnmount () { - window.removeEventListener('keyup', this._listener); - }, - - render () { - const { media, index, ...other } = this.props; - - if (!media) { - return null; - } - - const attachment = media.get(index); - const url = attachment.get('url'); - - let leftNav, rightNav, content; - - leftNav = rightNav = content = ''; - - if (media.size > 1) { - leftNav =
; - rightNav =
; - } - - if (attachment.get('type') === 'image') { - content = ( - - ); - } else if (attachment.get('type') === 'gifv') { - content = ; - } - - return ( - - {leftNav} - {content} - {rightNav} - - ); - } - }); -export default connect(mapStateToProps, mapDispatchToProps)(Modal); +export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot); diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index 4b7e4bb46..89fb82568 100644 --- a/app/assets/javascripts/components/features/ui/index.jsx +++ b/app/assets/javascripts/components/features/ui/index.jsx @@ -47,7 +47,9 @@ const UI = React.createClass({ this.dragTargets.push(e.target); } - this.setState({ draggingOver: true }); + if (e.dataTransfer && e.dataTransfer.files.length > 0) { + this.setState({ draggingOver: true }); + } }, handleDragOver (e) { diff --git a/app/assets/javascripts/components/reducers/modal.jsx b/app/assets/javascripts/components/reducers/modal.jsx index 37ffbc62b..3566820ef 100644 --- a/app/assets/javascripts/components/reducers/modal.jsx +++ b/app/assets/javascripts/components/reducers/modal.jsx @@ -1,31 +1,17 @@ -import { - MEDIA_OPEN, - MODAL_CLOSE, - MODAL_INDEX_DECREASE, - MODAL_INDEX_INCREASE -} from '../actions/modal'; +import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal'; import Immutable from 'immutable'; -const initialState = Immutable.Map({ - media: null, - index: 0, - open: false -}); +const initialState = { + modalType: null, + modalProps: {} +}; export default function modal(state = initialState, action) { switch(action.type) { - case MEDIA_OPEN: - return state.withMutations(map => { - map.set('media', action.media); - map.set('index', action.index); - map.set('open', true); - }); + case MODAL_OPEN: + return { modalType: action.modalType, modalProps: action.modalProps }; case MODAL_CLOSE: - return state.set('open', false); - case MODAL_INDEX_DECREASE: - return state.update('index', index => (index - 1) % state.get('media').size); - case MODAL_INDEX_INCREASE: - return state.update('index', index => (index + 1) % state.get('media').size); + return initialState; default: return state; } diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index a4dce7f18..d87e09453 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1311,7 +1311,7 @@ button.active i.fa-retweet { color: $color3; } -.modal-container--nav { +.modal-container__nav { color: $color5; } @@ -1848,3 +1848,47 @@ button.active i.fa-retweet { text-decoration: underline; } } + +.modal-root__overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + opacity: 0; + background: rgba($color8, 0.7); +} + +.modal-root__container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + align-content: space-around; + z-index: 9999; + opacity: 0; + pointer-events: none; + user-select: none; +} + +.modal-root__modal { + pointer-events: auto; + display: flex; +} + +.media-modal { + max-width: 80vw; + max-height: 80vh; + position: relative; + + img, video { + max-width: 80vw; + max-height: 80vh; + } +} -- cgit From 808017ff1843c200bdfcaa69634d41c6e7d47f0a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 1 Apr 2017 22:16:26 +0200 Subject: Paperclip will complain on its own if this variable is missing --- app/models/import.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/import.rb b/app/models/import.rb index 255063c53..5384986d8 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -9,6 +9,6 @@ class Import < ApplicationRecord FILE_TYPES = ['text/plain', 'text/csv'].freeze - has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV.fetch('PAPERCLIP_SECRET') + has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET'] validates_attachment_content_type :data, content_type: FILE_TYPES end -- cgit From a8c2e44fee65484f43dd9c58e9e519c375a76fd5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 1 Apr 2017 22:29:20 +0200 Subject: Fix broken reference --- app/assets/javascripts/components/features/ui/components/media_modal.jsx | 1 - 1 file changed, 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/components/features/ui/components/media_modal.jsx b/app/assets/javascripts/components/features/ui/components/media_modal.jsx index e8b718094..35eb2cb0c 100644 --- a/app/assets/javascripts/components/features/ui/components/media_modal.jsx +++ b/app/assets/javascripts/components/features/ui/components/media_modal.jsx @@ -1,4 +1,3 @@ -import Lightbox from '../../../components/lightbox'; import LoadingIndicator from '../../../components/loading_indicator'; import PureRenderMixin from 'react-addons-pure-render-mixin'; import ImmutablePropTypes from 'react-immutable-proptypes'; -- cgit From ae439784332e044749ce14153069de09fbd1abbd Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Sat, 1 Apr 2017 21:02:30 -0400 Subject: improve video button visibililty --- app/assets/javascripts/components/components/video_player.jsx | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app') diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx index 92597a2ec..1d41c14d4 100644 --- a/app/assets/javascripts/components/components/video_player.jsx +++ b/app/assets/javascripts/components/components/video_player.jsx @@ -23,6 +23,8 @@ const muteStyle = { position: 'absolute', top: '10px', right: '10px', + color: 'white', + boxShadow: '1px 1px 1px #000', opacity: '0.8', zIndex: '5' }; @@ -54,6 +56,8 @@ const spoilerButtonStyle = { position: 'absolute', top: '6px', left: '8px', + color: 'white', + boxShadow: '1px 1px 1px #000', zIndex: '100' }; -- cgit From 433cb198fa930344c7352250dfaae9857f7ba471 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Apr 2017 04:10:22 +0200 Subject: Fix landing page sign up form ignoring username field --- app/controllers/about_controller.rb | 3 +++ app/views/about/index.html.haml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 491036db2..abf4b7df4 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -5,6 +5,9 @@ class AboutController < ApplicationController def index @description = Setting.site_description + + @user = User.new + @user.build_account end def more diff --git a/app/views/about/index.html.haml b/app/views/about/index.html.haml index be5e406c5..fdfb2b916 100644 --- a/app/views/about/index.html.haml +++ b/app/views/about/index.html.haml @@ -24,7 +24,7 @@ .screenshot-with-signup .mascot= image_tag 'fluffy-elephant-friend.png' - = simple_form_for(:user, url: user_registration_path) do |f| + = simple_form_for(@user, url: user_registration_path) do |f| = f.simple_fields_for :account do |ff| = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } -- cgit From 3ed75efc31e6f7d0a9ff19b3a6c0171efbcccf94 Mon Sep 17 00:00:00 2001 From: Damien Erambert Date: Sat, 1 Apr 2017 23:45:53 -0700 Subject: Add fr locale for community_timeline in fr.jsx --- app/assets/javascripts/components/locales/fr.jsx | 1 + 1 file changed, 1 insertion(+) (limited to 'app') diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx index 2f5dd182f..a45b04b0c 100644 --- a/app/assets/javascripts/components/locales/fr.jsx +++ b/app/assets/javascripts/components/locales/fr.jsx @@ -33,6 +33,7 @@ const fr = { "navigation_bar.logout": "Déconnexion", "navigation_bar.preferences": "Préférences", "navigation_bar.public_timeline": "Fil public", + "navigation_bar.local_timeline": "Fil local", "notification.favourite": "{name} a ajouté à ses favoris :", "notification.follow": "{name} vous suit.", "notification.mention": "{name} vous a mentionné⋅e :", -- cgit From e809caa0e1633cede15b2578d1582d9878eae291 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Apr 2017 15:46:31 +0200 Subject: Fix feed regeneration bug --- app/assets/javascripts/components/features/compose/index.jsx | 2 +- app/services/fan_out_on_write_service.rb | 6 +++++- app/services/precompute_feed_service.rb | 8 ++++---- 3 files changed, 10 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx index d4df259dc..9421de3ff 100644 --- a/app/assets/javascripts/components/features/compose/index.jsx +++ b/app/assets/javascripts/components/features/compose/index.jsx @@ -72,7 +72,7 @@ const Compose = React.createClass({ {({ x }) => -
+
} diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 0cacfd7cd..402b84b2f 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -6,7 +6,11 @@ class FanOutOnWriteService < BaseService def call(status) deliver_to_self(status) if status.account.local? - status.direct_visibility? ? deliver_to_mentioned_followers(status) : deliver_to_followers(status) + if status.direct_visibility? + deliver_to_mentioned_followers(status) + else + deliver_to_followers(status) + end return if status.account.silenced? || !status.public_visibility? || status.reblog? diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index 54d11b631..984eb8e86 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -4,10 +4,10 @@ class PrecomputeFeedService < BaseService # Fill up a user's home/mentions feed from DB and return a subset # @param [Symbol] type :home or :mentions # @param [Account] account - def call(type, account) - Status.send("as_#{type}_timeline", account).limit(FeedManager::MAX_ITEMS).each do |status| - next if FeedManager.instance.filter?(type, status, account) - redis.zadd(FeedManager.instance.key(type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) + def call(_, account) + Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS).each do |status| + next if (status.direct_visibility? && !status.permitted?(account)) || FeedManager.instance.filter?(:home, status, account) + redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) end end -- cgit From d6b965cf083fb24fb16a9d2a58e240c357ad4ac9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Apr 2017 15:58:25 +0200 Subject: Fix issue with feed merge-in code as well --- app/lib/feed_manager.rb | 2 +- app/services/precompute_feed_service.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index b0dda1256..cd6ca1291 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -52,7 +52,7 @@ class FeedManager timeline_key = key(:home, into_account.id) from_account.statuses.limit(MAX_ITEMS).each do |status| - next if filter?(:home, status, into_account) + next if status.direct_visibility? || filter?(:home, status, into_account) redis.zadd(timeline_key, status.id, status.id) end diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index 984eb8e86..e1ec56e8d 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -6,7 +6,7 @@ class PrecomputeFeedService < BaseService # @param [Account] account def call(_, account) Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS).each do |status| - next if (status.direct_visibility? && !status.permitted?(account)) || FeedManager.instance.filter?(:home, status, account) + next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account) redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) end end -- cgit From 4b7dca47130b97e2f213cb28def741c6dee46ff1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Apr 2017 16:45:49 +0200 Subject: Fix wording "show reblogs" -> "show boosts", order reports chronologically in admin UI --- .../components/features/home_timeline/components/column_settings.jsx | 4 ++-- app/controllers/admin/reports_controller.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx index 3317210bf..92e700874 100644 --- a/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx +++ b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx @@ -6,7 +6,7 @@ import SettingToggle from '../../notifications/components/setting_toggle'; import SettingText from './setting_text'; const messages = defineMessages({ - filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter by regular expressions' } + filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' } }); const outerStyle = { @@ -44,7 +44,7 @@ const ColumnSettings = React.createClass({
- } /> + } />
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 67d57e4eb..0117a18ee 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -7,7 +7,7 @@ class Admin::ReportsController < ApplicationController layout 'admin' def index - @reports = Report.includes(:account, :target_account).paginate(page: params[:page], per_page: 40) + @reports = Report.includes(:account, :target_account).order('id desc').paginate(page: params[:page], per_page: 40) @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved end -- cgit From f4b5fe9cafe7f6b5a590b45970e48b15c34c262f Mon Sep 17 00:00:00 2001 From: Olivia Mossberg Date: Sun, 2 Apr 2017 16:54:24 +0200 Subject: Fix word-break in account profiles word-break:break-all is a surefire way to break things. It should be set to normal. This merge just set it back to what it should be. Tested on Firefox 52.0.2 and Chrome 56.0.2924.87 with no detected errors. --- app/assets/stylesheets/components.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 422927195..f8003e5fd 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -424,7 +424,7 @@ a.status__content__spoiler-link { .account__header__content { word-wrap: break-word; - word-break: break-all; + word-break: normal; font-weight: 400; overflow: hidden; color: $color3; -- cgit From 5b12624847f6a599e1bbb3b24fc87c3b222e6716 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Apr 2017 19:43:09 +0200 Subject: Add proper error page for request timeouts --- Gemfile | 2 +- app/controllers/application_controller.rb | 8 ++++++++ app/views/errors/503.html.haml | 5 +++++ config/initializers/timeout.rb | 4 +++- 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 app/views/errors/503.html.haml (limited to 'app') diff --git a/Gemfile b/Gemfile index 440f2e87b..764010d5d 100644 --- a/Gemfile +++ b/Gemfile @@ -50,6 +50,7 @@ gem 'rails-settings-cached' gem 'simple-navigation' gem 'statsd-instrument' gem 'ruby-oembed', require: 'oembed' +gem 'rack-timeout' gem 'react-rails' gem 'browserify-rails' @@ -89,5 +90,4 @@ group :production do gem 'rails_12factor' gem 'redis-rails' gem 'lograge' - gem 'rack-timeout' end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ef9364897..abfb5bb8c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base rescue_from ActionController::RoutingError, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity + rescue_from Rack::Timeout::RequestExpiryError, Rack::Timeout::RequestTimeoutError, with: :request_timeout before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :set_locale @@ -69,6 +70,13 @@ class ApplicationController < ActionController::Base end end + def request_timeout + respond_to do |format| + format.any { head 503 } + format.html { render 'errors/503', layout: 'error', status: 503 } + end + end + def current_account @current_account ||= current_user.try(:account) end diff --git a/app/views/errors/503.html.haml b/app/views/errors/503.html.haml new file mode 100644 index 000000000..f88d50d36 --- /dev/null +++ b/app/views/errors/503.html.haml @@ -0,0 +1,5 @@ +- content_for :page_title do + Request timeout + +- content_for :content do + It took too long to process your request. This might be a temporary server issue diff --git a/config/initializers/timeout.rb b/config/initializers/timeout.rb index 06a29492e..de87fd906 100644 --- a/config/initializers/timeout.rb +++ b/config/initializers/timeout.rb @@ -1,4 +1,6 @@ +Rack::Timeout::Logger.disable +Rack::Timeout.service_timeout = false + if Rails.env.production? Rack::Timeout.service_timeout = 90 - Rack::Timeout::Logger.disable end -- cgit From ca21be3e16b77fd1223a974a1a3f7616d63f4a0b Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Sun, 2 Apr 2017 14:54:24 -0400 Subject: remove black border on buttons --- app/assets/javascripts/components/components/video_player.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx index 1d41c14d4..92079387d 100644 --- a/app/assets/javascripts/components/components/video_player.jsx +++ b/app/assets/javascripts/components/components/video_player.jsx @@ -23,8 +23,8 @@ const muteStyle = { position: 'absolute', top: '10px', right: '10px', - color: 'white', - boxShadow: '1px 1px 1px #000', + color: white, + textShadow: "0px 1px 1px black, 1px 0px 1px black", opacity: '0.8', zIndex: '5' }; @@ -56,8 +56,8 @@ const spoilerButtonStyle = { position: 'absolute', top: '6px', left: '8px', - color: 'white', - boxShadow: '1px 1px 1px #000', + color: white, + textShadow: "0px 1px 1px black, 1px 0px 1px black", zIndex: '100' }; -- cgit From f25fc04ea198fd3407232a629f0b7b21967b8c64 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Sun, 2 Apr 2017 14:55:13 -0400 Subject: single-quotes --- app/assets/javascripts/components/components/video_player.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx index 92079387d..ab21ca9cd 100644 --- a/app/assets/javascripts/components/components/video_player.jsx +++ b/app/assets/javascripts/components/components/video_player.jsx @@ -23,7 +23,7 @@ const muteStyle = { position: 'absolute', top: '10px', right: '10px', - color: white, + color: 'white', textShadow: "0px 1px 1px black, 1px 0px 1px black", opacity: '0.8', zIndex: '5' @@ -56,7 +56,7 @@ const spoilerButtonStyle = { position: 'absolute', top: '6px', left: '8px', - color: white, + color: 'white', textShadow: "0px 1px 1px black, 1px 0px 1px black", zIndex: '100' }; -- cgit From 2d07cb57714b7aae96593c8ff293c1c3010a13f9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Apr 2017 21:12:18 +0200 Subject: Catching rack timeout from rails doesn't work --- app/controllers/application_controller.rb | 8 -------- app/views/errors/503.html.haml | 5 ----- 2 files changed, 13 deletions(-) delete mode 100644 app/views/errors/503.html.haml (limited to 'app') diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index abfb5bb8c..ef9364897 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,7 +12,6 @@ class ApplicationController < ActionController::Base rescue_from ActionController::RoutingError, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity - rescue_from Rack::Timeout::RequestExpiryError, Rack::Timeout::RequestTimeoutError, with: :request_timeout before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :set_locale @@ -70,13 +69,6 @@ class ApplicationController < ActionController::Base end end - def request_timeout - respond_to do |format| - format.any { head 503 } - format.html { render 'errors/503', layout: 'error', status: 503 } - end - end - def current_account @current_account ||= current_user.try(:account) end diff --git a/app/views/errors/503.html.haml b/app/views/errors/503.html.haml deleted file mode 100644 index f88d50d36..000000000 --- a/app/views/errors/503.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -- content_for :page_title do - Request timeout - -- content_for :content do - It took too long to process your request. This might be a temporary server issue -- cgit From aaa4d1b0fb896f0d5f607cca3760106399caf41b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Apr 2017 21:44:06 +0200 Subject: Keep track of which timelines are connected live to avoid redundant refreshes on navigation --- .../javascripts/components/actions/timelines.jsx | 22 ++++++++++++++++++++++ .../javascripts/components/containers/mastodon.jsx | 13 ++++++++++++- .../features/community_timeline/index.jsx | 16 +++++++++++++++- .../components/features/public_timeline/index.jsx | 16 +++++++++++++++- .../javascripts/components/reducers/timelines.jsx | 11 ++++++++++- 5 files changed, 74 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx index 3e2d4ff43..6cd1f04b3 100644 --- a/app/assets/javascripts/components/actions/timelines.jsx +++ b/app/assets/javascripts/components/actions/timelines.jsx @@ -14,6 +14,9 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; + export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) { return { type: TIMELINE_REFRESH_SUCCESS, @@ -76,6 +79,11 @@ export function refreshTimeline(timeline, id = null) { let skipLoading = false; if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) { + if (id === null && getState().getIn(['timelines', timeline, 'online'])) { + // Skip refreshing when timeline is live anyway + return; + } + params = { ...params, since_id: newestId }; skipLoading = true; } @@ -162,3 +170,17 @@ export function scrollTopTimeline(timeline, top) { top }; }; + +export function connectTimeline(timeline) { + return { + type: TIMELINE_CONNECT, + timeline + }; +}; + +export function disconnectTimeline(timeline) { + return { + type: TIMELINE_DISCONNECT, + timeline + }; +}; diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 40fbac525..6dc08bb4c 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -4,7 +4,9 @@ import { refreshTimelineSuccess, updateTimeline, deleteFromTimelines, - refreshTimeline + refreshTimeline, + connectTimeline, + disconnectTimeline } from '../actions/timelines'; import { updateNotifications, refreshNotifications } from '../actions/notifications'; import createBrowserHistory from 'history/lib/createBrowserHistory'; @@ -70,6 +72,14 @@ const Mastodon = React.createClass({ this.subscription = createStream(accessToken, 'user', { + connected () { + store.dispatch(connectTimeline('home')); + }, + + disconnected () { + store.dispatch(disconnectTimeline('home')); + }, + received (data) { switch(data.event) { case 'update': @@ -85,6 +95,7 @@ const Mastodon = React.createClass({ }, reconnected () { + store.dispatch(connectTimeline('home')); store.dispatch(refreshTimeline('home')); store.dispatch(refreshNotifications()); } diff --git a/app/assets/javascripts/components/features/community_timeline/index.jsx b/app/assets/javascripts/components/features/community_timeline/index.jsx index 2cfd7b2fe..0957338cf 100644 --- a/app/assets/javascripts/components/features/community_timeline/index.jsx +++ b/app/assets/javascripts/components/features/community_timeline/index.jsx @@ -5,7 +5,9 @@ import Column from '../ui/components/column'; import { refreshTimeline, updateTimeline, - deleteFromTimelines + deleteFromTimelines, + connectTimeline, + disconnectTimeline } from '../../actions/timelines'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; @@ -44,6 +46,18 @@ const CommunityTimeline = React.createClass({ subscription = createStream(accessToken, 'public:local', { + connected () { + dispatch(connectTimeline('community')); + }, + + reconnected () { + dispatch(connectTimeline('community')); + }, + + disconnected () { + dispatch(disconnectTimeline('community')); + }, + received (data) { switch(data.event) { case 'update': diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx index b2342abbd..6d766a83b 100644 --- a/app/assets/javascripts/components/features/public_timeline/index.jsx +++ b/app/assets/javascripts/components/features/public_timeline/index.jsx @@ -5,7 +5,9 @@ import Column from '../ui/components/column'; import { refreshTimeline, updateTimeline, - deleteFromTimelines + deleteFromTimelines, + connectTimeline, + disconnectTimeline } from '../../actions/timelines'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; @@ -44,6 +46,18 @@ const PublicTimeline = React.createClass({ subscription = createStream(accessToken, 'public', { + connected () { + dispatch(connectTimeline('public')); + }, + + reconnected () { + dispatch(connectTimeline('public')); + }, + + disconnected () { + dispatch(disconnectTimeline('public')); + }, + received (data) { switch(data.event) { case 'update': diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index c67d05423..675a52759 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -7,7 +7,9 @@ import { TIMELINE_EXPAND_SUCCESS, TIMELINE_EXPAND_REQUEST, TIMELINE_EXPAND_FAIL, - TIMELINE_SCROLL_TOP + TIMELINE_SCROLL_TOP, + TIMELINE_CONNECT, + TIMELINE_DISCONNECT } from '../actions/timelines'; import { REBLOG_SUCCESS, @@ -35,6 +37,7 @@ const initialState = Immutable.Map({ path: () => '/api/v1/timelines/home', next: null, isLoading: false, + online: false, loaded: false, top: true, unread: 0, @@ -45,6 +48,7 @@ const initialState = Immutable.Map({ path: () => '/api/v1/timelines/public', next: null, isLoading: false, + online: false, loaded: false, top: true, unread: 0, @@ -56,6 +60,7 @@ const initialState = Immutable.Map({ next: null, params: { local: true }, isLoading: false, + online: false, loaded: false, top: true, unread: 0, @@ -300,6 +305,10 @@ export default function timelines(state = initialState, action) { return filterTimelines(state, action.relationship, action.statuses); case TIMELINE_SCROLL_TOP: return updateTop(state, action.timeline, action.top); + case TIMELINE_CONNECT: + return state.setIn([action.timeline, 'online'], true); + case TIMELINE_DISCONNECT: + return state.setIn([action.timeline, 'online'], false); default: return state; } -- cgit From a23e4380b25b5ab2d7446431a046cec2a19b375a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 2 Apr 2017 22:02:38 +0200 Subject: Avoid re-loading already loaded relationships. Also fixes issue where wrong button would be displayed in account lists for unloaded relationships --- app/assets/javascripts/components/actions/accounts.jsx | 11 +++++++---- app/assets/javascripts/components/selectors/index.jsx | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx index 05fa8e68d..37ebb9969 100644 --- a/app/assets/javascripts/components/actions/accounts.jsx +++ b/app/assets/javascripts/components/actions/accounts.jsx @@ -579,15 +579,18 @@ export function expandFollowingFail(id, error) { }; }; -export function fetchRelationships(account_ids) { +export function fetchRelationships(accountIds) { return (dispatch, getState) => { - if (account_ids.length === 0) { + const loadedRelationships = getState().get('relationships'); + const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null); + + if (newAccountIds.length === 0) { return; } - dispatch(fetchRelationshipsRequest(account_ids)); + dispatch(fetchRelationshipsRequest(newAccountIds)); - api(getState).get(`/api/v1/accounts/relationships?${account_ids.map(id => `id[]=${id}`).join('&')}`).then(response => { + api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { dispatch(fetchRelationshipsSuccess(response.data)); }).catch(error => { dispatch(fetchRelationshipsFail(error)); diff --git a/app/assets/javascripts/components/selectors/index.jsx b/app/assets/javascripts/components/selectors/index.jsx index 0e88654a1..01a6cb264 100644 --- a/app/assets/javascripts/components/selectors/index.jsx +++ b/app/assets/javascripts/components/selectors/index.jsx @@ -5,7 +5,7 @@ const getStatuses = state => state.get('statuses'); const getAccounts = state => state.get('accounts'); const getAccountBase = (state, id) => state.getIn(['accounts', id], null); -const getAccountRelationship = (state, id) => state.getIn(['relationships', id]); +const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null); export const makeGetAccount = () => { return createSelector([getAccountBase, getAccountRelationship], (base, relationship) => { -- cgit From 633e5ec6f609e328d2efd4cddff0bb92548dee51 Mon Sep 17 00:00:00 2001 From: Kazhnuz Date: Sun, 2 Apr 2017 23:18:01 +0200 Subject: Update French Translation --- app/assets/javascripts/components/locales/fr.jsx | 110 +++++++++++++---------- 1 file changed, 63 insertions(+), 47 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx index a45b04b0c..9ce98e7cc 100644 --- a/app/assets/javascripts/components/locales/fr.jsx +++ b/app/assets/javascripts/components/locales/fr.jsx @@ -1,69 +1,85 @@ const fr = { - "account.block": "Bloquer", + "column_back_button.label": "Retour", + "lightbox.close": "Fermer", + "loading_indicator.label": "Chargement…", + "status.mention": "Mentionner", + "status.delete": "Effacer", + "status.reply": "Répondre", + "status.reblog": "Partager", + "status.favourite": "Ajouter aux favoris", + "status.reblogged_by": "{name} a partagé :", + "status.sensitive_warning": "Contenu délicat", + "status.sensitive_toggle": "Cliquer pour dévoiler", + "video_player.toggle_sound": "Mettre/Couper le son", + "account.mention": "Mentionner", "account.edit_profile": "Modifier le profil", - "account.followers": "Abonnés", - "account.follows": "Abonnements", + "account.unblock": "Débloquer", + "account.unfollow": "Ne plus suivre", + "account.block": "Bloquer", + "account.mute": "Masquer", + "account.unmute": "Ne plus masquer", "account.follow": "Suivre", - "account.follows_you": "Vous suit", - "account.mention": "Mentionner", "account.posts": "Statuts", + "account.follows": "Abonnements", + "account.followers": "Abonnés", + "account.follows_you": "Vous suit", "account.requested": "Invitation envoyée", - "account.unblock": "Débloquer", - "account.unfollow": "Ne plus suivre", - "column_back_button.label": "Retour", + "account.report": "Signaler", + "account.disclaimer": "Ce compte est situé sur une autre instance. Les nombres peuvent être plus grands.", + "getting_started.heading": "Pour commencer", + "getting_started.about_addressing": "Vous pouvez suivre les statuts de quelqu’un en entrant dans le champs de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.", + "getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, l’identifiant suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.", + "getting_started.about_developer": "Pour suivre le développeur de ce projet, c’est Gargron@mastodon.social", + "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.", "column.home": "Accueil", + "column.community": "Fil public local", + "column.public": "Fil public global", "column.mentions": "Mentions", "column.notifications": "Notifications", - "column.public": "Fil public", + "tabs_bar.compose": "Composer", + "tabs_bar.home": "Accueil", + "tabs_bar.mentions": "Mentions", + "tabs_bar.public": "Fil public global", + "tabs_bar.notifications": "Notifications", "compose_form.placeholder": "Qu’avez-vous en tête ?", - "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ?", - "compose_form.private": "Rendre privé", "compose_form.publish": "Pouet ", "compose_form.sensitive": "Marquer le média comme délicat", - "compose_form.spoiler": "Masque le texte par un avertissement", - "compose_form.unlisted": "Ne pas afficher dans le fil public", - "getting_started.about_addressing": "Vous pouvez vous suivre les statuts de quelqu’un en entrant dans le champs de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.", - "getting_started.about_developer": "Pour suivre le développeur de ce projet, c’est Gargron@mastodon.social", - "getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, l’identifiant suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.", - "getting_started.heading": "Pour commencer", - "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.", - "lightbox.close": "Fermer", - "loading_indicator.label": "Chargement…", + "compose_form.spoiler": "Masquer le texte par un avertissement", + "compose_form.private": "Rendre privé", + "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues", + "compose_form.unlisted": "Ne pas afficher dans les fils publics", + "emoji_button.label": "Insérer un emoji", "navigation_bar.edit_profile": "Modifier le profil", - "navigation_bar.logout": "Déconnexion", "navigation_bar.preferences": "Préférences", - "navigation_bar.public_timeline": "Fil public", - "navigation_bar.local_timeline": "Fil local", - "notification.favourite": "{name} a ajouté à ses favoris :", + "navigation_bar.community_timeline": "Fil public local", + "navigation_bar.public_timeline": "Fil public global", + "navigation_bar.logout": "Déconnexion", + "reply_indicator.cancel": "Annuler", + "search.placeholder": "Chercher", + "search.account": "Compte", + "search.hashtag": "Mot-clé", + "search_results.total": "{count} {count, plural, one {résultat} other {résultats}}", + "upload_button.label": "Joindre un média", + "upload_form.undo": "Annuler", "notification.follow": "{name} vous suit.", - "notification.mention": "{name} vous a mentionné⋅e :", + "notification.favourite": "{name} a ajouté à ses favoris :", "notification.reblog": "{name} a partagé votre statut :", + "notification.mention": "{name} vous a mentionné⋅e :", "notifications.column_settings.alert": "Notifications locales", - "notifications.column_settings.favourite": "Favoris :", + "notifications.column_settings.show": "Afficher dans la colonne", "notifications.column_settings.follow": "Nouveaux abonnés :", + "notifications.column_settings.favourite": "Favoris :", "notifications.column_settings.mention": "Mentions :", "notifications.column_settings.reblog": "Partages :", - "notifications.column_settings.show": "Afficher dans la colonne", - "reply_indicator.cancel": "Annuler", - "search.account": "Compte", - "search.hashtag": "Mot-clé", - "search.placeholder": "Chercher", - "status.delete": "Effacer", - "status.favourite": "Ajouter aux favoris", - "status.mention": "Mentionner", - "status.reblogged_by": "{name} a partagé :", - "status.reblog": "Partager", - "status.reply": "Répondre", - "status.sensitive_toggle": "Cliquer pour dévoiler", - "status.sensitive_warning": "Contenu délicat", - "tabs_bar.compose": "Composer", - "tabs_bar.home": "Accueil", - "tabs_bar.mentions": "Mentions", - "tabs_bar.notifications": "Notifications", - "tabs_bar.public": "Public", - "upload_button.label": "Joindre un média", - "upload_form.undo": "Annuler", - "video_player.toggle_sound": "Mettre/Couper le son", + "privacy.public.short": "Public", + "privacy.public.long": "Afficher dans les fils publics", + "privacy.unlisted.short": "Non-listé", + "privacy.unlisted.long": "Ne pas afficher dans les fils publics", + "privacy.private.short": "Privé" + "privacy.private.long": "N’afficher que pour vos abonné⋅e⋅s", + "privacy.direct.short": "Direct", + "privacy.direct.long": "N’afficher que pour les personnes mentionné⋅e⋅s", + "privacy.change": "Ajuster la confidentialité du message", }; export default fr; -- cgit From 4f7cce25acdcb5e105d479b68f04b4d9b2ffea00 Mon Sep 17 00:00:00 2001 From: Damien Erambert Date: Sun, 2 Apr 2017 14:23:40 -0700 Subject: Add more lcoales in fr.jsx --- app/assets/javascripts/components/locales/fr.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx index a45b04b0c..b52aef0fb 100644 --- a/app/assets/javascripts/components/locales/fr.jsx +++ b/app/assets/javascripts/components/locales/fr.jsx @@ -15,6 +15,8 @@ const fr = { "column.mentions": "Mentions", "column.notifications": "Notifications", "column.public": "Fil public", + "column.blocks": "Utilisateurs bloqués", + "column.favourites": "Favoris", "compose_form.placeholder": "Qu’avez-vous en tête ?", "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ?", "compose_form.private": "Rendre privé", @@ -33,7 +35,10 @@ const fr = { "navigation_bar.logout": "Déconnexion", "navigation_bar.preferences": "Préférences", "navigation_bar.public_timeline": "Fil public", - "navigation_bar.local_timeline": "Fil local", + "navigation_bar.community_timeline": "Fil local", + "navigation_bar.blocks": "Utilisateurs bloqués", + "navigation_bar.favourites": "Favoris", + "navigation_bar.info": "Plus d'informations", "notification.favourite": "{name} a ajouté à ses favoris :", "notification.follow": "{name} vous suit.", "notification.mention": "{name} vous a mentionné⋅e :", -- cgit From c76d20c2a0572f1da7d2fafcd6c7a90aa6caf7f1 Mon Sep 17 00:00:00 2001 From: Kazhnuz Date: Sun, 2 Apr 2017 23:39:41 +0200 Subject: Add forgotten comma --- app/assets/javascripts/components/locales/fr.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx index 9ce98e7cc..982aea5e2 100644 --- a/app/assets/javascripts/components/locales/fr.jsx +++ b/app/assets/javascripts/components/locales/fr.jsx @@ -75,7 +75,7 @@ const fr = { "privacy.public.long": "Afficher dans les fils publics", "privacy.unlisted.short": "Non-listé", "privacy.unlisted.long": "Ne pas afficher dans les fils publics", - "privacy.private.short": "Privé" + "privacy.private.short": "Privé", "privacy.private.long": "N’afficher que pour vos abonné⋅e⋅s", "privacy.direct.short": "Direct", "privacy.direct.long": "N’afficher que pour les personnes mentionné⋅e⋅s", -- cgit From 2d384850cb4b677010e6ba435193f3d8217d1117 Mon Sep 17 00:00:00 2001 From: Jessica Stokes Date: Mon, 3 Apr 2017 13:16:14 +1000 Subject: Fix the position of the Mastodon mascot in the UI The Mastodon mascot was previously anchored to the bottom, and that was since broken. This restores that behaviour! It also disables the double-scrollbar behaviour that was caused by this area allowing overflow-y in addition to its parent doing so. --- app/assets/stylesheets/components.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index f8003e5fd..d233b3471 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1149,10 +1149,9 @@ a.status__content__spoiler-link { .getting-started { box-sizing: border-box; - overflow-y: auto; padding-bottom: 235px; background: image-url('mastodon-getting-started.png') no-repeat 0 100% local; - height: 100%; + flex: 1 0 auto; p { color: $color2; -- cgit From 1236a12cae3a002755465ce16deb2455f66c871c Mon Sep 17 00:00:00 2001 From: Marvin Kopf Date: Mon, 3 Apr 2017 09:56:01 +0200 Subject: add mute option in status dropdown --- app/assets/javascripts/components/components/status_action_bar.jsx | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'app') diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx index 234cd396a..4ebb76ea7 100644 --- a/app/assets/javascripts/components/components/status_action_bar.jsx +++ b/app/assets/javascripts/components/components/status_action_bar.jsx @@ -7,6 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' }, reply: { id: 'status.reply', defaultMessage: 'Reply' }, reblog: { id: 'status.reblog', defaultMessage: 'Reblog' }, @@ -28,6 +29,7 @@ const StatusActionBar = React.createClass({ onReblog: React.PropTypes.func, onDelete: React.PropTypes.func, onMention: React.PropTypes.func, + onMute: React.PropTypes.func, onBlock: React.PropTypes.func, onReport: React.PropTypes.func, me: React.PropTypes.number.isRequired, @@ -56,6 +58,10 @@ const StatusActionBar = React.createClass({ this.props.onMention(this.props.status.get('account'), this.context.router); }, + handleMuteClick () { + this.props.onMute(this.props.status.get('account')); + }, + handleBlockClick () { this.props.onBlock(this.props.status.get('account')); }, @@ -81,6 +87,7 @@ const StatusActionBar = React.createClass({ } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push(null); + menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); } -- cgit From 69fc95a2f5e7cf90fcf86c24f8e7eabd6cfd67d6 Mon Sep 17 00:00:00 2001 From: JantsoP Date: Mon, 3 Apr 2017 12:09:33 +0200 Subject: Create Finnish translation for Mastodon Create Finnish translation for Mastodon --- app/assets/javascripts/components/locales/fi.jsx | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 app/assets/javascripts/components/locales/fi.jsx (limited to 'app') diff --git a/app/assets/javascripts/components/locales/fi.jsx b/app/assets/javascripts/components/locales/fi.jsx new file mode 100644 index 000000000..5bef99923 --- /dev/null +++ b/app/assets/javascripts/components/locales/fi.jsx @@ -0,0 +1,68 @@ +const fi = { + "column_back_button.label": "Takaisin", + "lightbox.close": "Sulje", + "loading_indicator.label": "Ladataan...", + "status.mention": "Mainitse @{name}", + "status.delete": "Poista", + "status.reply": "Vastaa", + "status.reblog": "Boostaa", + "status.favourite": "Tykkää", + "status.reblogged_by": "{name} boostattu", + "status.sensitive_warning": "Arkaluontoista sisältöä", + "status.sensitive_toggle": "Klikkaa nähdäksesi", + "video_player.toggle_sound": "Äänet päälle/pois", + "account.mention": "Mainitse @{name}", + "account.edit_profile": "Muokkaa", + "account.unblock": "Salli @{name}", + "account.unfollow": "Lopeta seuraaminen", + "account.block": "Estä @{name}", + "account.follow": "Seuraa", + "account.posts": "Postit", + "account.follows": "Seuraa", + "account.followers": "Seuraajia", + "account.follows_you": "Seuraa sinua", + "account.requested": "Odottaa hyväksyntää", + "getting_started.heading": "Päästä alkuun", + "getting_started.about_addressing": "Voit seurata ihmisiä jos tiedät heidän käyttäjänimensä ja domainin missä he ovat syöttämällä e-mail-esque osoitteen Etsi kenttään.", + "getting_started.about_shortcuts": "Jos etsimäsi henkilö on samassa domainissa kuin sinä, pelkkä käyttäjänimi kelpaa. Sama pätee kun mainitset ihmisiä statuksessasi", + "getting_started.open_source_notice": "Mastodon Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia githubissa {github}. {apps}.", + "column.home": "Koti", + "column.community": "Paikallinen aikajana", + "column.public": "Yhdistetty aikajana", + "column.notifications": "Ilmoitukset", + "tabs_bar.compose": "Luo", + "tabs_bar.home": "Koti", + "tabs_bar.mentions": "Maininnat", + "tabs_bar.public": "Yleinen aikajana", + "tabs_bar.notifications": "Ilmoitukset", + "compose_form.placeholder": "Mitä sinulla on mielessä?", + "compose_form.publish": "Toot", + "compose_form.sensitive": "Merkitse media herkäksi", + "compose_form.spoiler": "Piiloita teksti varoituksen taakse", + "compose_form.private": "Merkitse yksityiseksi", + "compose_form.privacy_disclaimer": "Sinun yksityinen status toimitetaan mainitsemallesi käyttäjille domaineissa {domains}. Luotatko {domainsCount, plural, one {tähän palvelimeen} other {näihin palvelimiin}}? Postauksen yksityisyys toimii van Mastodon palvelimilla. Jos {domains} {domainsCount, plural, one {ei ole Mastodon palvelin} other {eivät ole Mastodon palvelin}}, viestiin ei tule Yksityinen-merkintää, ja sitä voidaan boostata tai muuten tehdä näkyväksi muille vastaanottajille.", + "compose_form.unlisted": "Älä näytä julkisilla aikajanoilla", + "navigation_bar.edit_profile": "Muokkaa profiilia", + "navigation_bar.preferences": "Ominaisuudet", + "navigation_bar.community_timeline": "Paikallinen aikajana", + "navigation_bar.public_timeline": "Yleinen aikajana", + "navigation_bar.logout": "Kirjaudu ulos", + "reply_indicator.cancel": "Peruuta", + "search.placeholder": "Hae", + "search.account": "Tili", + "search.hashtag": "Hashtag", + "upload_button.label": "Lisää mediaa", + "upload_form.undo": "Peru", + "notification.follow": "{name} seurasi sinua", + "notification.favourite": "{name} tykkäsi statuksestasi", + "notification.reblog": "{name} boostasi statustasi", + "notification.mention": "{name} mainitsi sinut", + "notifications.column_settings.alert": "Työpöytä ilmoitukset", + "notifications.column_settings.show": "Näytä sarakkeessa", + "notifications.column_settings.follow": "Uusia seuraajia:", + "notifications.column_settings.favourite": "Tykkäyksiä:", + "notifications.column_settings.mention": "Mainintoja:", + "notifications.column_settings.reblog": "Boosteja:", +}; + +export default fi; -- cgit From eabb86b1247429b016cef8711ab78983def07ae9 Mon Sep 17 00:00:00 2001 From: JantsoP Date: Mon, 3 Apr 2017 13:32:10 +0200 Subject: add finnish language add finnish language --- app/assets/javascripts/components/locales/index.jsx | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app') diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx index 203929d66..fef317887 100644 --- a/app/assets/javascripts/components/locales/index.jsx +++ b/app/assets/javascripts/components/locales/index.jsx @@ -5,6 +5,7 @@ import hu from './hu'; import fr from './fr'; import pt from './pt'; import uk from './uk'; +import fi from './fi'; const locales = { en, @@ -14,6 +15,7 @@ const locales = { fr, pt, uk + fi }; export default function getMessagesForLocale (locale) { -- cgit From 22f88b845ad3238d2970222d276d952135e26884 Mon Sep 17 00:00:00 2001 From: JantsoP Date: Mon, 3 Apr 2017 13:33:43 +0200 Subject: add finnish translation add finnish translation --- app/assets/javascripts/components/containers/mastodon.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 6dc08bb4c..cbb7b85bc 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -46,6 +46,7 @@ import fr from 'react-intl/locale-data/fr'; import pt from 'react-intl/locale-data/pt'; import hu from 'react-intl/locale-data/hu'; import uk from 'react-intl/locale-data/uk'; +import fi from 'react-intl/locale-data/fi'; import getMessagesForLocale from '../locales'; import { hydrateStore } from '../actions/store'; import createStream from '../stream'; @@ -58,7 +59,7 @@ const browserHistory = useRouterHistory(createBrowserHistory)({ basename: '/web' }); -addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk]); +addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi]); const Mastodon = React.createClass({ -- cgit From ae95f35fe604a840f3c3573516c740dc84d8dee6 Mon Sep 17 00:00:00 2001 From: JantsoP Date: Mon, 3 Apr 2017 13:34:26 +0200 Subject: add finnish translation add finnish translation --- app/helpers/settings_helper.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'app') diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 74215e8df..e01f7d0cc 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -10,6 +10,7 @@ module SettingsHelper hu: 'Magyar', uk: 'Українська', 'zh-CN': '简体中文', + fi: 'Suomi', }.freeze def human_locale(locale) -- cgit From a229840ffed572e8b6ae33969c934103499ed855 Mon Sep 17 00:00:00 2001 From: JantsoP Date: Mon, 3 Apr 2017 14:16:03 +0200 Subject: fixed typo --- app/assets/javascripts/components/locales/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx index fef317887..72b8a5df5 100644 --- a/app/assets/javascripts/components/locales/index.jsx +++ b/app/assets/javascripts/components/locales/index.jsx @@ -14,7 +14,7 @@ const locales = { hu, fr, pt, - uk + uk, fi }; -- cgit From 5652f00d81aa18dd4fa6046c22282c000635e032 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Apr 2017 11:44:11 -0400 Subject: GitHub should be capitalized --- app/assets/javascripts/components/features/getting_started/index.jsx | 2 +- app/assets/javascripts/components/locales/en.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx index 8253ad017..d7a78d9cc 100644 --- a/app/assets/javascripts/components/features/getting_started/index.jsx +++ b/app/assets/javascripts/components/features/getting_started/index.jsx @@ -43,7 +43,7 @@ const GettingStarted = ({ intl, me }) => {
-

tootsuite/mastodon, apps: }} />

+

tootsuite/mastodon, apps: }} />

diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index 2d3360b6b..53e2898eb 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -25,7 +25,7 @@ const en = { "getting_started.heading": "Getting started", "getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.", "getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.", - "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on github at {github}. {apps}.", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.", "column.home": "Home", "column.community": "Local timeline", "column.public": "Federated timeline", -- cgit From b7c1b12367b307d07303ce99f2c27bf255ecd56a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 3 Apr 2017 18:55:06 +0200 Subject: Make default admin UI page reports. Add admin UI for creating a domain block --- app/controllers/admin/domain_blocks_controller.rb | 18 +++ app/services/block_domain_service.rb | 10 +- app/views/admin/domain_blocks/index.html.haml | 1 + app/views/admin/domain_blocks/new.html.haml | 18 +++ app/workers/domain_block_worker.rb | 11 ++ config/locales/devise.no.yml | 62 +------- config/locales/doorkeeper.no.yml | 114 +-------------- config/locales/no.yml | 165 +--------------------- config/locales/simple_form.no.yml | 47 +----- config/navigation.rb | 4 +- config/routes.rb | 2 +- 11 files changed, 59 insertions(+), 393 deletions(-) create mode 100644 app/views/admin/domain_blocks/new.html.haml create mode 100644 app/workers/domain_block_worker.rb (limited to 'app') diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index e362957e7..1f4432847 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -9,6 +9,24 @@ class Admin::DomainBlocksController < ApplicationController @blocks = DomainBlock.paginate(page: params[:page], per_page: 40) end + def new + @domain_block = DomainBlock.new + end + def create + @domain_block = DomainBlock.new(resource_params) + + if @domain_block.save + DomainBlockWorker.perform_async(@domain_block.id) + redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed' + else + render action: :new + end + end + + private + + def resource_params + params.require(:domain_block).permit(:domain, :severity) end end diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index 9518b1fcf..6c131bd34 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true class BlockDomainService < BaseService - def call(domain, severity) - DomainBlock.where(domain: domain).first_or_create!(domain: domain, severity: severity) - - if severity == :silence - Account.where(domain: domain).update_all(silenced: true) + def call(domain_block) + if domain_block.silence? + Account.where(domain: domain_block.domain).update_all(silenced: true) else - Account.where(domain: domain).find_each do |account| + Account.where(domain: domain_block.domain).find_each do |account| account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed? SuspendAccountService.new.call(account) end diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml index dbaeb4716..eb7894b86 100644 --- a/app/views/admin/domain_blocks/index.html.haml +++ b/app/views/admin/domain_blocks/index.html.haml @@ -14,3 +14,4 @@ %td= block.severity = will_paginate @blocks, pagination_options += link_to 'Add new', new_admin_domain_block_path, class: 'button' diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml new file mode 100644 index 000000000..fbd39d6cf --- /dev/null +++ b/app/views/admin/domain_blocks/new.html.haml @@ -0,0 +1,18 @@ +- content_for :page_title do + New domain block + += simple_form_for @domain_block, url: admin_domain_blocks_path do |f| + = render 'shared/error_messages', object: @domain_block + + %p.hint The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts. + + = f.input :domain, placeholder: 'Domain' + = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false + + %p.hint + %strong Silence + will make the account's posts invisible to anyone who isn't following them. + %strong Suspend + will remove all of the account's content, media, and profile data. + .actions + = f.button :button, 'Create block', type: :submit diff --git a/app/workers/domain_block_worker.rb b/app/workers/domain_block_worker.rb new file mode 100644 index 000000000..884477829 --- /dev/null +++ b/app/workers/domain_block_worker.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class DomainBlockWorker + include Sidekiq::Worker + + def perform(domain_block_id) + BlockDomainService.new.call(DomainBlock.find(domain_block_id)) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/config/locales/devise.no.yml b/config/locales/devise.no.yml index 7c665f0da..2fbf0ffd7 100644 --- a/config/locales/devise.no.yml +++ b/config/locales/devise.no.yml @@ -1,61 +1 @@ ---- -no: - devise: - confirmations: - confirmed: Epostaddressen din er blitt bekreftet. - send_instructions: Du vil motta en epost med instruksjoner for hvordan bekrefte din epostaddresse om noen få minutter. - send_paranoid_instructions: Hvis din epostaddresse finnes i vår database vil du motta en epost med instruksjoner for hvordan bekrefte din epost om noen få minutter. - failure: - already_authenticated: Du er allerede innlogget. - inactive: Din konto er ikke blitt aktivert ennå. - invalid: Ugyldig %{authentication_keys} eller passord. - last_attempt: Du har ett forsøk igjen før kontoen din bli låst. - locked: Din konto er låst. - not_found_in_database: Ugyldig %{authentication_keys} eller passord. - timeout: Sesjonen din løp ut på tid. Logg inn på nytt for å fortsette. - unauthenticated: Du må logge inn eller registrere deg før du kan fortsette. - unconfirmed: Du må bekrefte epostadressen din før du kan fortsette. - mailer: - confirmation_instructions: - subject: 'Mastodon: Instruksjoner for å bekrefte epostadresse' - password_change: - subject: 'Mastodon: Passord endret' - reset_password_instructions: - subject: 'Mastodon: Hvordan nullstille passord?' - unlock_instructions: - subject: 'Mastodon: Instruksjoner for å gjenåpne konto' - omniauth_callbacks: - failure: Kunne ikke autentisere deg fra %{kind} fordi "%{reason}". - success: Vellykket autentisering fra %{kind}. - passwords: - no_token: Du har ingen tilgang til denne siden så lenge du ikke kommer fra en epost om nullstilling av passord. Hvis du kommer fra en passordnullstilling epost, dobbelsjekk at du brukte hele URLen. - send_instructions: Du vil motta en epost med instruksjoner for å nullstille passordet ditt om noen få minutter. - send_paranoid_instructions: Hvis epostadressen din finnes i databasen vår vil du motta en instruksjonsmail om passord nullstilling om noen få minutter. - updated: Passordet ditt har blitt endret. Du er nå logget inn. - updated_not_active: Passordet ditt har blitt endret. - registrations: - destroyed: Adjø! Kontoen din har blitt avsluttet. Vi håper at vi ser deg igjen snart. - signed_up: Velkommen! Registrasjonen var vellykket. - signed_up_but_inactive: Registrasjonen var vellykket. Vi kunne dessverre ikke logge deg inn fordi kontoen din ennå ikke har blitt aktivert. - signed_up_but_locked: Registrasjonen var vellykket. Vi kunne dessverre ikke logge deg inn fordi kontoen din har blitt låst. - signed_up_but_unconfirmed: En epostmelding med en bekreftelseslink har blitt sendt til din adresse. Klikk på linken i eposten for å aktivere kontoen din. - update_needs_confirmation: Du har oppdatert kontoen din, men vi må bekrefte din nye epostadresse. Sjekk eposten din og følg bekreftelseslinken for å bekrefte din nye epostadresse. - updated: Kontoen din ble oppdatert. - sessions: - already_signed_out: Logget ut. - signed_in: Logget inn. - signed_out: Logget ut. - unlocks: - send_instructions: Du vil motta en epost med instruksjoner for å åpne kontoen din om noen få minutter. - send_paranoid_instructions: Hvis kontoen din eksisterer vil du motta en epost med instruksjoner for å åpne kontoen din om noen få minutter. - unlocked: Kontoen din ble åpnet uten problemer. Logg på for å fortsette. - errors: - messages: - already_confirmed: har allerede blitt bekreftet, prøv å logg på istedet. - confirmation_period_expired: må bekreftes innen %{period}. Spør om en ny bekreftelsesmail istedet. - expired: har utløpt, spør om en ny en istedet - not_found: ikke funnet - not_locked: var ikke låst - not_saved: - one: '1 feil hindret denne %{resource} fra å bli lagret:' - other: "%{count} feil hindret denne %{resource} fra å bli lagret:" +--- {} diff --git a/config/locales/doorkeeper.no.yml b/config/locales/doorkeeper.no.yml index 7b51289aa..2fbf0ffd7 100644 --- a/config/locales/doorkeeper.no.yml +++ b/config/locales/doorkeeper.no.yml @@ -1,113 +1 @@ ---- -no: - activerecord: - attributes: - doorkeeper/application: - name: Navn - redirect_uri: Omdirigerings-URI - errors: - models: - doorkeeper/application: - attributes: - redirect_uri: - fragment_present: kan ikke inneholde ett fragment. - invalid_uri: må være en gyldig URI. - relative_uri: må være en absolutt URI. - secured_uri: må være en HTTPS/SSL URI. - doorkeeper: - applications: - buttons: - authorize: Autoriser - cancel: Avbryt - destroy: Ødelegg - edit: Endre - submit: Send inn - confirmations: - destroy: Er du sikker? - edit: - title: Endre applikasjon - form: - error: Whoops! Sjekk skjemaet ditt for mulige feil - help: - native_redirect_uri: Bruk %{native_redirect_uri} for lokale tester - redirect_uri: Bruk en linje per URI - scopes: Adskill omfang med mellomrom. La det være blankt for å bruke standard omfang. - index: - callback_url: Callback URL - name: Navn - new: Ny Applikasjon - title: Dine applikasjoner - new: - title: Ny Applikasjoner - show: - actions: Operasjoner - application_id: Applikasjon Id - callback_urls: Callback urls - scopes: Omfang - secret: Hemmelighet - title: 'Applikasjon: %{name}' - authorizations: - buttons: - authorize: Autoriser - deny: Avvis - error: - title: En feil oppsto - new: - able_to: Den vil ha mulighet til - prompt: Applikasjon %{client_name} spør om tilgang til din konto - title: Autorisasjon påkrevd - show: - title: Autoriserings kode - authorized_applications: - buttons: - revoke: Opphev - confirmations: - revoke: Opphev? - index: - application: Applikasjon - created_at: Autorisert - date_format: "%Y-%m-%d %H:%M:%S" - scopes: Omfang - title: Dine autoriserte applikasjoner - errors: - messages: - access_denied: Ressurseieren eller autoriserings tjeneren avviste forespørslen. - credential_flow_not_configured: Ressurseiers passord flyt feilet på grunn av at Doorkeeper.configure.resource_owner_from_credentials ikke var konfigurert. - invalid_client: Klient autentisering feilet på grunn av ukjent klient, ingen autentisering inkludert eller autentiserings metode som ikke er støttet. - invalid_grant: Autoriseringen er ugyldig, utløpt, opphevet, stemmer ikke overens med omdirigerings-URIen eller var utstedt til en annen klient. - invalid_redirect_uri: redirect urien som var inkludert er ikke gyldig. - invalid_request: Forespørslen mangler ett eller flere parametere, inkluderte ett parameter som ikke støttes eller har feil struktur. - invalid_resource_owner: Ressurseierens detaljer er ikke gyldig, eller så kan ikke eieren finnes. - invalid_scope: Det etterspurte omfanget er ugyldig, ukjent eller har feil struktur. - invalid_token: - expired: Tilgangsbeviset har utløpt - revoked: Tilgangsbeviset har blitt opphevet - unknown: Tilgangsbeviset er ugyldig - resource_owner_authenticator_not_configured: Ressurseier kunne ikke finnes fordi Doorkeeper.configure.resource_owner_authenticator ikke er konfigurert. - server_error: Autoriserings tjeneren støtte på en uventet hendelse som hindret den i å svare på forespørslen. - temporarily_unavailable: Autoriserings tjeneren kan ikke håndtere forespørslen grunnet en midlertidig overbelastning eller tjenervedlikehold. - unauthorized_client: Klienten har ikke autorisasjon for å utføre denne forespørslen med denne metoden. - unsupported_grant_type: Autorisasjons tildelings typen er ikke støttet av denne autoriserings tjeneren. - unsupported_response_type: Autorisasjons serveren støtter ikke denne typen av forespørsler. - flash: - applications: - create: - notice: Applikasjon opprettet. - destroy: - notice: Applikasjon slettet. - update: - notice: Applikasjon oppdatert. - authorized_applications: - destroy: - notice: Applikasjon opphevet. - layouts: - admin: - nav: - applications: Applikasjoner - oauth2_provider: OAuth2 tilbyder - application: - title: OAuth autorisering påkrevet - scopes: - follow: følg, blokker, avblokker, avfølg kontoer - read: lese dine data - write: poste på dine vegne +--- {} diff --git a/config/locales/no.yml b/config/locales/no.yml index d4514d5e4..2fbf0ffd7 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -1,164 +1 @@ ---- -no: - about: - about_mastodon: Mastodon er et gratis, åpen kildekode sosialt nettverk. Et desentralisert alternativ til kommersielle plattformer. Slik kan det unngå risikoene ved å ha et enkelt selskap med monopol på din kommunikasjon. Velg en tjener du stoler på — uansett hvilken du velger så kan du interagere med alle andre. Alle kan kjøre sin egen Mastodon og delta sømløst i det sosiale nettverket. - about_this: Om denne instansen - apps: Applikasjoner - business_email: 'Bedriftsepost:' - contact: Kontakt - description_headline: Hva er %{domain}? - domain_count_after: andre instanser - domain_count_before: Koblet til - features: - api: Åpent api for applikasjoner og tjenester - blocks: Rikholdige blokkerings verktøy - characters: 500 tegn per post - chronology: Tidslinjer er kronologiske - ethics: 'Etisk design: Ingen reklame, ingen sporing' - gifv: GIFV sett og korte videoer - privacy: Finmaskete personvernsinnstillinger - public: Offentlige tidslinjer - features_headline: Hva skiller Mastodon fra andre sosiale nettverk - get_started: Kom i gang - links: Lenker - other_instances: Andre instanser - source_code: Kildekode - status_count_after: statuser - status_count_before: Hvem skrev - terms: Betingelser - user_count_after: brukere - user_count_before: Hjem til - accounts: - follow: Følg - followers: Følgere - following: Følger - nothing_here: Det er ingenting her! - people_followed_by: Folk som %{name} følger - people_who_follow: Folk som følger %{name} - posts: Poster - remote_follow: Følg fra andre instanser - unfollow: Avfølg - application_mailer: - settings: 'Endre foretrukne epost innstillinger: %{link}' - signature: Mastodon notiser fra %{instance} - view: 'Se:' - applications: - invalid_url: Den oppgitte URLen er ugyldig - auth: - change_password: Brukerdetaljer - didnt_get_confirmation: Fikk du ikke bekreftelsesmailen din? - forgot_password: Har du glemt passordet ditt? - login: Innlogging - logout: Logg ut - register: Bli med - resend_confirmation: Send bekreftelsesinstruksjoner på nytt - reset_password: Nullstill passord - set_new_password: Sett nytt passord - authorize_follow: - error: Uheldigvis så skjedde det en feil når vi prøvde å få tak i en konto fra en annen instans. - follow: Følg - prompt_html: 'Du (%{self}) har spurt om å følge:' - title: Følg %{acct} - datetime: - distance_in_words: - about_x_hours: "%{count}t" - about_x_months: "%{count}m" - about_x_years: "%{count}å" - almost_x_years: "%{count}å" - half_a_minute: Nylig - less_than_x_minutes: "%{count}min" - less_than_x_seconds: Nylig - over_x_years: "%{count}å" - x_days: "%{count}d" - x_minutes: "%{count}min" - x_months: "%{count}mo" - x_seconds: "%{count}s" - exports: - blocks: Du blokkerer - csv: CSV - follows: Du følger - storage: Media lagring - generic: - changes_saved_msg: Vellykket lagring av endringer! - powered_by: drevet av %{link} - save_changes: Lagre endringer - validation_errors: - one: Noe er ikke helt riktig ennå. Vær snill å se etter en gang til - other: Noe er ikke helt riktig ennå. Det er ennå %{count} feil å rette på - imports: - preface: Du kan importere data om mennesker du følger eller blokkerer inn til kontoen din på denne instansen, fra filer opprettet av eksporter fra andre instanser. - success: Din data ble mottatt og vil bli prosessert så fort som mulig. - types: - blocking: Blokkeringsliste - following: Følgeliste - upload: Opplastning - landing_strip_html: %{name} er en bruker på %{domain}. Du kan følge dem eller interagere med dem hvis du har en konto hvor som helst i fediverset. Hvis du ikke har en konto så kan du registrere deg her. - notification_mailer: - digest: - body: 'Her er en kort oppsummering av hva du har gått glipp av på %{instance} siden du logget deg inn sist den %{since}:' - mention: "%{name} nevnte deg i:" - new_followers_summary: - one: Du har fått en ny følger. Jippi! - other: Du har fått %{count} nye følgere! Imponerende! - subject: - one: "1 ny hendelse siden ditt siste besøk \U0001F418" - other: "%{count} nye hendelser siden ditt siste besøk \U0001F418" - favourite: - body: 'Din status ble satt som favoritt av %{name}' - subject: "%{name} satte din status som favoritt." - follow: - body: "%{name} følger deg!" - subject: "%{name} følger deg" - follow_request: - body: "%{name} har spurt om å få lov til å følge deg" - subject: 'Ventende følger: %{name}' - mention: - body: 'Du ble nevnt av %{name} i:' - subject: Du ble nevnt av %{name} - reblog: - body: 'Din status fikk en boost av %{name}:' - subject: "%{name} ga din status en boost" - pagination: - next: Neste - prev: Forrige - remote_follow: - acct: Tast inn brukernavn@domene som du vil følge fra - missing_resource: Kunne ikke finne URLen for din konto - proceed: Fortsett med følging - prompt: 'Du kommer til å følge:' - settings: - authorized_apps: Autoriserte applikasjoner - back: Tilbake til Mastodon - edit_profile: Endre profil - export: Data eksport - import: Importer - preferences: Foretrukne valg - settings: Innstillinger - two_factor_auth: To-faktor autentisering - statuses: - open_in_web: Åpne i nettleser - over_character_limit: tegngrense på %{max} overskredet - show_more: Vis mer - visibilities: - private: Vis kun til følgere - public: Offentlig - unlisted: Offentlig, men vis ikke på offentlig tidslinje - stream_entries: - click_to_show: Klikk for å vise - reblogged: boostet - sensitive_content: Sensitivt innhold - time: - formats: - default: "%d, %b %Y, %H:%M" - two_factor_auth: - description_html: Hvis du skru på tofaktor autentisering vil innlogging kreve at du har telefonen din, som vil generere koder som du må taste inn. - disable: Skru av - enable: Skru på - instructions_html: "Scan denne QR-koden i Google Authenticator eller en lignende app på telefonen din. Fra nå av så vil denne applikasjonen generere koder for deg som skal brukes under innlogging" - plaintext_secret_html: 'Plain-text secret: %{secret}' - warning: Hvis du ikke kan konfigurere en autentikatorapp nå, så bør du trykke "Skru av"; ellers vil du ikke kunne logge inn. - users: - invalid_email: E-post addressen er ugyldig - invalid_otp_token: Ugyldig two-faktor kode - will_paginate: - page_gap: "…" +--- {} diff --git a/config/locales/simple_form.no.yml b/config/locales/simple_form.no.yml index 6829e6a24..2fbf0ffd7 100644 --- a/config/locales/simple_form.no.yml +++ b/config/locales/simple_form.no.yml @@ -1,46 +1 @@ ---- -no: - simple_form: - hints: - defaults: - avatar: PNG, GIF eller JPG. Maksimalt 2MB. Vil bli nedskalert til 120x120px - display_name: Maksimalt 30 tegn - header: PNG, GIF eller JPG. Maksimalt 2MB. Vil bli nedskalert til 700x335px - locked: Krever at du manuelt godkjenner følgere og setter standard beskyttelse av poster til kun-følgere - note: Maksimalt 160 tegn - imports: - data: CSV fil eksportert fra en annen Mastodon instans - labels: - defaults: - avatar: Avatar - confirm_new_password: Bekreft nytt passord - confirm_password: Bekreft passord - current_password: Nåværende passord - data: Data - display_name: Visningsnavn - email: E-post adresse - header: Header - locale: Språk - locked: Endre konto til privat - new_password: Nytt passord - note: Biografi - otp_attempt: To-faktor kode - password: Passord - setting_default_privacy: Leserettigheter for poster - type: Importeringstype - username: Brukernavn - interactions: - must_be_follower: Blokker meldinger fra ikke-følgere - must_be_following: Blokker meldinger fra folk du ikke følger - notification_emails: - digest: Send oppsummerings eposter - favourite: Send e-post når noen setter din status som favoritt - follow: Send e-post når noen følger deg - follow_request: Send e-post når noen spør om å få følge deg - mention: Send e-post når noen nevner deg - reblog: Send e-post når noen reblogger din status - 'no': 'Nei' - required: - mark: "*" - text: påkrevd - 'yes': 'Ja' +--- {} diff --git a/config/navigation.rb b/config/navigation.rb index 77556e5aa..c6b7b9767 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -14,11 +14,11 @@ SimpleNavigation::Configuration.run do |navigation| settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url end - primary.item :admin, safe_join([fa_icon('cogs fw'), 'Administration']), admin_accounts_url, if: proc { current_user.admin? } do |admin| + primary.item :admin, safe_join([fa_icon('cogs fw'), 'Administration']), admin_reports_url, if: proc { current_user.admin? } do |admin| admin.item :reports, safe_join([fa_icon('flag fw'), 'Reports']), admin_reports_url, highlights_on: %r{/admin/reports} admin.item :accounts, safe_join([fa_icon('users fw'), 'Accounts']), admin_accounts_url, highlights_on: %r{/admin/accounts} admin.item :pubsubhubbubs, safe_join([fa_icon('paper-plane-o fw'), 'PubSubHubbub']), admin_pubsubhubbub_index_url - admin.item :domain_blocks, safe_join([fa_icon('lock fw'), 'Domain Blocks']), admin_domain_blocks_url + admin.item :domain_blocks, safe_join([fa_icon('lock fw'), 'Domain Blocks']), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks} admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url admin.item :settings, safe_join([fa_icon('cogs fw'), 'Site Settings']), admin_settings_url diff --git a/config/routes.rb b/config/routes.rb index bfca5c734..ca77191f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -77,7 +77,7 @@ Rails.application.routes.draw do namespace :admin do resources :pubsubhubbub, only: [:index] - resources :domain_blocks, only: [:index, :create] + resources :domain_blocks, only: [:index, :new, :create] resources :settings, only: [:index, :update] resources :reports, only: [:index, :show] do -- cgit From 71458dc6df368801b32b55bb63baa94375019a83 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 3 Apr 2017 19:17:56 +0200 Subject: When taking action on a report (silence/suspend), it dismisses all other reports for that user automatically --- app/controllers/admin/reports_controller.rb | 4 ++-- app/views/admin/reports/index.html.haml | 35 ++++++++++++++++------------- 2 files changed, 22 insertions(+), 17 deletions(-) (limited to 'app') diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 0117a18ee..bb3f028d9 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -22,13 +22,13 @@ class Admin::ReportsController < ApplicationController def suspend Admin::SuspensionWorker.perform_async(@report.target_account.id) - @report.update(action_taken: true) + Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true) redirect_to admin_report_path(@report) end def silence @report.target_account.update(silenced: true) - @report.update(action_taken: true) + Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true) redirect_to admin_report_path(@report) end diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 8a5414cef..839259dc2 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -8,20 +8,25 @@ %li= filter_link_to 'Unresolved', action_taken: nil %li= filter_link_to 'Resolved', action_taken: '1' -%table.table - %thead - %tr - %th ID - %th Target - %th Reported by - %th Comment - %th - %tbody - - @reports.each do |report| += form_tag do + + %table.table + %thead %tr - %td= "##{report.id}" - %td= link_to report.target_account.acct, admin_account_path(report.target_account.id) - %td= link_to report.account.acct, admin_account_path(report.account.id) - %td= truncate(report.comment, length: 30, separator: ' ') - %td= table_link_to 'circle', 'View', admin_report_path(report) + %th + %th ID + %th Target + %th Reported by + %th Comment + %th + %tbody + - @reports.each do |report| + %tr + %td= check_box_tag 'select', report.id + %td= "##{report.id}" + %td= link_to report.target_account.acct, admin_account_path(report.target_account.id) + %td= link_to report.account.acct, admin_account_path(report.account.id) + %td= truncate(report.comment, length: 30, separator: ' ') + %td= table_link_to 'circle', 'View', admin_report_path(report) + = will_paginate @reports, pagination_options -- cgit From 68f829e11c058c55a6695b5812aa0577b5b1eea1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 3 Apr 2017 19:27:30 +0200 Subject: Add basic logging of who resolved report --- app/controllers/admin/reports_controller.rb | 6 +++--- app/models/report.rb | 1 + app/views/admin/reports/show.html.haml | 8 +++++++- ...3172249_add_action_taken_by_account_id_to_reports.rb | 5 +++++ db/schema.rb | 17 +++++++++-------- spec/services/block_domain_service_spec.rb | 2 +- 6 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 db/migrate/20170403172249_add_action_taken_by_account_id_to_reports.rb (limited to 'app') diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index bb3f028d9..2b3b1809f 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -16,19 +16,19 @@ class Admin::ReportsController < ApplicationController end def resolve - @report.update(action_taken: true) + @report.update(action_taken: true, action_taken_by_account_id: current_account.id) redirect_to admin_report_path(@report) end def suspend Admin::SuspensionWorker.perform_async(@report.target_account.id) - Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true) + Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id) redirect_to admin_report_path(@report) end def silence @report.target_account.update(silenced: true) - Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true) + Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id) redirect_to admin_report_path(@report) end diff --git a/app/models/report.rb b/app/models/report.rb index 05dc8cff1..fd8e46aac 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -3,6 +3,7 @@ class Report < ApplicationRecord belongs_to :account belongs_to :target_account, class_name: 'Account' + belongs_to :action_taken_by_account, class_name: 'Account' scope :unresolved, -> { where(action_taken: false) } scope :resolved, -> { where(action_taken: true) } diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 74cac016d..caa8415df 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -27,7 +27,7 @@ = link_to remove_admin_report_path(@report, status_id: status.id), method: :post, class: 'icon-button', style: 'font-size: 24px; width: 24px; height: 24px', title: 'Delete' do = fa_icon 'trash' -- unless @report.action_taken? +- if !@report.action_taken? %hr/ %div{ style: 'overflow: hidden' } @@ -36,3 +36,9 @@ = link_to 'Suspend account', suspend_admin_report_path(@report), method: :post, class: 'button' %div{ style: 'float: left' } = link_to 'Mark as resolved', resolve_admin_report_path(@report), method: :post, class: 'button' +- elsif !@report.action_taken_by_account.nil? + %hr/ + + %p + %strong Action taken by: + = @report.action_taken_by_account.acct diff --git a/db/migrate/20170403172249_add_action_taken_by_account_id_to_reports.rb b/db/migrate/20170403172249_add_action_taken_by_account_id_to_reports.rb new file mode 100644 index 000000000..2d4e12198 --- /dev/null +++ b/db/migrate/20170403172249_add_action_taken_by_account_id_to_reports.rb @@ -0,0 +1,5 @@ +class AddActionTakenByAccountIdToReports < ActiveRecord::Migration[5.0] + def change + add_column :reports, :action_taken_by_account_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 5a9ca1426..3aaa3e3ad 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: 20170330164118) do +ActiveRecord::Schema.define(version: 20170403172249) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -201,13 +201,14 @@ ActiveRecord::Schema.define(version: 20170330164118) do end create_table "reports", force: :cascade do |t| - t.integer "account_id", null: false - t.integer "target_account_id", null: false - t.bigint "status_ids", default: [], null: false, array: true - t.text "comment", default: "", null: false - t.boolean "action_taken", default: false, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "account_id", null: false + t.integer "target_account_id", null: false + t.bigint "status_ids", default: [], null: false, array: true + t.text "comment", default: "", null: false + t.boolean "action_taken", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "action_taken_by_account_id" end create_table "settings", force: :cascade do |t| diff --git a/spec/services/block_domain_service_spec.rb b/spec/services/block_domain_service_spec.rb index d88b3b55c..8e71d4542 100644 --- a/spec/services/block_domain_service_spec.rb +++ b/spec/services/block_domain_service_spec.rb @@ -14,7 +14,7 @@ RSpec.describe BlockDomainService do bad_status2 bad_attachment - subject.call('evil.org', :suspend) + subject.call(DomainBlock.create!(domain: 'evil.org', severity: :suspend)) end it 'creates a domain block' do -- cgit From 8232f76c482d3046055bd7bf224ef7835d0fa399 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 3 Apr 2017 22:54:46 +0200 Subject: Add check for visibility.nil? even though it can't ever be, to check for race conditions --- app/lib/exceptions.rb | 1 + app/services/fan_out_on_write_service.rb | 2 ++ 2 files changed, 3 insertions(+) (limited to 'app') diff --git a/app/lib/exceptions.rb b/app/lib/exceptions.rb index 200da9fe1..9bc802c12 100644 --- a/app/lib/exceptions.rb +++ b/app/lib/exceptions.rb @@ -4,4 +4,5 @@ module Mastodon class Error < StandardError; end class NotPermittedError < Error; end class ValidationError < Error; end + class RaceConditionError < Error; end end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 402b84b2f..df404cbef 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -4,6 +4,8 @@ class FanOutOnWriteService < BaseService # Push a status into home and mentions feeds # @param [Status] status def call(status) + raise Mastodon::RaceConditionError if status.visibility.nil? + deliver_to_self(status) if status.account.local? if status.direct_visibility? -- cgit From f722bd2387df9163760014e9555928ec487ae95f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 4 Apr 2017 00:53:20 +0200 Subject: Separate background jobs into different queues. ATTENTION: new queue "pull" must be added to the Sidekiq invokation in your systemd file The pull queue will handle link crawling, thread resolving, and OStatus processing. Such tasks are more likely to hang for a longer time (due to network requests) so it is more sensible to not make the "in-house" tasks wait for them. --- app/workers/after_remote_follow_request_worker.rb | 2 +- app/workers/after_remote_follow_worker.rb | 2 +- app/workers/import_worker.rb | 2 +- app/workers/link_crawl_worker.rb | 2 +- app/workers/merge_worker.rb | 2 ++ app/workers/notification_worker.rb | 2 +- app/workers/processing_worker.rb | 2 +- app/workers/regeneration_worker.rb | 2 ++ app/workers/salmon_worker.rb | 2 +- app/workers/thread_resolve_worker.rb | 2 +- app/workers/unmerge_worker.rb | 2 ++ docker-compose.yml | 2 +- docs/Running-Mastodon/Production-guide.md | 2 +- 13 files changed, 16 insertions(+), 10 deletions(-) (limited to 'app') diff --git a/app/workers/after_remote_follow_request_worker.rb b/app/workers/after_remote_follow_request_worker.rb index f1d6869cc..1f2db3061 100644 --- a/app/workers/after_remote_follow_request_worker.rb +++ b/app/workers/after_remote_follow_request_worker.rb @@ -3,7 +3,7 @@ class AfterRemoteFollowRequestWorker include Sidekiq::Worker - sidekiq_options retry: 5 + sidekiq_options queue: 'pull', retry: 5 def perform(follow_request_id) follow_request = FollowRequest.find(follow_request_id) diff --git a/app/workers/after_remote_follow_worker.rb b/app/workers/after_remote_follow_worker.rb index 0d04456a9..bdd2c2a91 100644 --- a/app/workers/after_remote_follow_worker.rb +++ b/app/workers/after_remote_follow_worker.rb @@ -3,7 +3,7 @@ class AfterRemoteFollowWorker include Sidekiq::Worker - sidekiq_options retry: 5 + sidekiq_options queue: 'pull', retry: 5 def perform(follow_id) follow = Follow.find(follow_id) diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb index a3ae2a85a..7cf29fb53 100644 --- a/app/workers/import_worker.rb +++ b/app/workers/import_worker.rb @@ -5,7 +5,7 @@ require 'csv' class ImportWorker include Sidekiq::Worker - sidekiq_options retry: false + sidekiq_options queue: 'pull', retry: false def perform(import_id) import = Import.find(import_id) diff --git a/app/workers/link_crawl_worker.rb b/app/workers/link_crawl_worker.rb index af3394b8b..834b0088b 100644 --- a/app/workers/link_crawl_worker.rb +++ b/app/workers/link_crawl_worker.rb @@ -3,7 +3,7 @@ class LinkCrawlWorker include Sidekiq::Worker - sidekiq_options retry: false + sidekiq_options queue: 'pull', retry: false def perform(status_id) FetchLinkCardService.new.call(Status.find(status_id)) diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index 0f288f43f..d745cb99c 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -3,6 +3,8 @@ class MergeWorker include Sidekiq::Worker + sidekiq_options queue: 'pull' + def perform(from_account_id, into_account_id) FeedManager.instance.merge_into_timeline(Account.find(from_account_id), Account.find(into_account_id)) end diff --git a/app/workers/notification_worker.rb b/app/workers/notification_worker.rb index 1a2faefd8..da1d6ab45 100644 --- a/app/workers/notification_worker.rb +++ b/app/workers/notification_worker.rb @@ -3,7 +3,7 @@ class NotificationWorker include Sidekiq::Worker - sidekiq_options retry: 5 + sidekiq_options queue: 'push', retry: 5 def perform(xml, source_account_id, target_account_id) SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id)) diff --git a/app/workers/processing_worker.rb b/app/workers/processing_worker.rb index 5df404bcc..4a467d924 100644 --- a/app/workers/processing_worker.rb +++ b/app/workers/processing_worker.rb @@ -3,7 +3,7 @@ class ProcessingWorker include Sidekiq::Worker - sidekiq_options backtrace: true + sidekiq_options queue: 'pull', backtrace: true def perform(account_id, body) ProcessFeedService.new.call(body, Account.find(account_id)) diff --git a/app/workers/regeneration_worker.rb b/app/workers/regeneration_worker.rb index 3aece0ba2..289b63d84 100644 --- a/app/workers/regeneration_worker.rb +++ b/app/workers/regeneration_worker.rb @@ -3,6 +3,8 @@ class RegenerationWorker include Sidekiq::Worker + sidekiq_options queue: 'pull', backtrace: true + def perform(account_id, timeline_type) PrecomputeFeedService.new.call(timeline_type, Account.find(account_id)) end diff --git a/app/workers/salmon_worker.rb b/app/workers/salmon_worker.rb index fc95ce47f..2888b574b 100644 --- a/app/workers/salmon_worker.rb +++ b/app/workers/salmon_worker.rb @@ -3,7 +3,7 @@ class SalmonWorker include Sidekiq::Worker - sidekiq_options backtrace: true + sidekiq_options queue: 'pull', backtrace: true def perform(account_id, body) ProcessInteractionService.new.call(body, Account.find(account_id)) diff --git a/app/workers/thread_resolve_worker.rb b/app/workers/thread_resolve_worker.rb index 593edd032..38287e8e6 100644 --- a/app/workers/thread_resolve_worker.rb +++ b/app/workers/thread_resolve_worker.rb @@ -3,7 +3,7 @@ class ThreadResolveWorker include Sidekiq::Worker - sidekiq_options retry: false + sidekiq_options queue: 'pull', retry: false def perform(child_status_id, parent_url) child_status = Status.find(child_status_id) diff --git a/app/workers/unmerge_worker.rb b/app/workers/unmerge_worker.rb index dbf7243de..ea6aacebf 100644 --- a/app/workers/unmerge_worker.rb +++ b/app/workers/unmerge_worker.rb @@ -3,6 +3,8 @@ class UnmergeWorker include Sidekiq::Worker + sidekiq_options queue: 'pull' + def perform(from_account_id, into_account_id) FeedManager.instance.unmerge_from_timeline(Account.find(from_account_id), Account.find(into_account_id)) end diff --git a/docker-compose.yml b/docker-compose.yml index 68c8ef960..d6ba66dde 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,7 +33,7 @@ services: restart: always build: . env_file: .env.production - command: bundle exec sidekiq -q default -q mailers -q push + command: bundle exec sidekiq -q default -q mailers -q pull -q push depends_on: - db - redis diff --git a/docs/Running-Mastodon/Production-guide.md b/docs/Running-Mastodon/Production-guide.md index f0dd7bd2b..469fefa94 100644 --- a/docs/Running-Mastodon/Production-guide.md +++ b/docs/Running-Mastodon/Production-guide.md @@ -180,7 +180,7 @@ User=mastodon WorkingDirectory=/home/mastodon/live Environment="RAILS_ENV=production" Environment="DB_POOL=5" -ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 5 -q default -q mailers -q push +ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push TimeoutSec=15 Restart=always -- cgit From 4c53af64f0b10bc11473df5e3fd1cd7a11b755f6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 4 Apr 2017 01:33:34 +0200 Subject: Fix ActionController::Parameters in API issue --- app/controllers/api/v1/apps_controller.rb | 8 +++++++- app/controllers/api/v1/follows_controller.rb | 8 ++++++-- app/controllers/api/v1/media_controller.rb | 8 +++++++- app/controllers/api/v1/reports_controller.rb | 12 +++++++++--- app/controllers/api/v1/statuses_controller.rb | 14 +++++++++----- app/models/status.rb | 2 +- 6 files changed, 39 insertions(+), 13 deletions(-) (limited to 'app') diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb index ca9dd0b7e..2ec7280af 100644 --- a/app/controllers/api/v1/apps_controller.rb +++ b/app/controllers/api/v1/apps_controller.rb @@ -4,6 +4,12 @@ class Api::V1::AppsController < ApiController respond_to :json def create - @app = Doorkeeper::Application.create!(name: params[:client_name], redirect_uri: params[:redirect_uris], scopes: (params[:scopes] || Doorkeeper.configuration.default_scopes), website: params[:website]) + @app = Doorkeeper::Application.create!(name: app_params[:client_name], redirect_uri: app_params[:redirect_uris], scopes: (app_params[:scopes] || Doorkeeper.configuration.default_scopes), website: app_params[:website]) + end + + private + + def app_params + params.permit(:client_name, :redirect_uris, :scopes, :website) end end diff --git a/app/controllers/api/v1/follows_controller.rb b/app/controllers/api/v1/follows_controller.rb index c22dacbaa..7c0f44f03 100644 --- a/app/controllers/api/v1/follows_controller.rb +++ b/app/controllers/api/v1/follows_controller.rb @@ -7,7 +7,7 @@ class Api::V1::FollowsController < ApiController respond_to :json def create - raise ActiveRecord::RecordNotFound if params[:uri].blank? + raise ActiveRecord::RecordNotFound if follow_params[:uri].blank? @account = FollowService.new.call(current_user.account, target_uri).try(:target_account) render action: :show @@ -16,6 +16,10 @@ class Api::V1::FollowsController < ApiController private def target_uri - params[:uri].strip.gsub(/\A@/, '') + follow_params[:uri].strip.gsub(/\A@/, '') + end + + def follow_params + params.permit(:uri) end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index f8139ade7..aed3578d7 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -10,10 +10,16 @@ class Api::V1::MediaController < ApiController respond_to :json def create - @media = MediaAttachment.create!(account: current_user.account, file: params[:file]) + @media = MediaAttachment.create!(account: current_user.account, file: media_params[:file]) rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: { error: 'File type of uploaded media could not be verified' }, status: 422 rescue Paperclip::Error render json: { error: 'Error processing thumbnail for uploaded media' }, status: 500 end + + private + + def media_params + params.permit(:file) + end end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 46bdddbc1..f83c573cb 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -12,13 +12,19 @@ class Api::V1::ReportsController < ApiController end def create - status_ids = params[:status_ids].is_a?(Enumerable) ? params[:status_ids] : [params[:status_ids]] + status_ids = report_params[:status_ids].is_a?(Enumerable) ? report_params[:status_ids] : [report_params[:status_ids]] @report = Report.create!(account: current_account, - target_account: Account.find(params[:account_id]), + target_account: Account.find(report_params[:account_id]), status_ids: Status.find(status_ids).pluck(:id), - comment: params[:comment]) + comment: report_params[:comment]) render :show end + + private + + def report_params + params.permit(:account_id, :comment, status_ids: []) + end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 024258c0e..4ece7e702 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -62,11 +62,11 @@ class Api::V1::StatusesController < ApiController end def create - @status = PostStatusService.new.call(current_user.account, params[:status], params[:in_reply_to_id].blank? ? nil : Status.find(params[:in_reply_to_id]), media_ids: params[:media_ids], - sensitive: params[:sensitive], - spoiler_text: params[:spoiler_text], - visibility: params[:visibility], - application: doorkeeper_token.application) + @status = PostStatusService.new.call(current_user.account, status_params[:status], status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]), media_ids: status_params[:media_ids], + sensitive: status_params[:sensitive], + spoiler_text: status_params[:spoiler_text], + visibility: status_params[:visibility], + application: doorkeeper_token.application) render action: :show end @@ -111,4 +111,8 @@ class Api::V1::StatusesController < ApiController @status = Status.find(params[:id]) raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account) end + + def status_params + params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, media_ids: []) + end end diff --git a/app/models/status.rb b/app/models/status.rb index 81b26fd14..daf128572 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -188,7 +188,7 @@ class Status < ApplicationRecord end before_validation do - text.strip! + text&.strip! spoiler_text&.strip! self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply -- cgit From b510a56c0c3d8c1a48bb295a85b688af94466723 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 4 Apr 2017 02:00:10 +0200 Subject: Only call regeneration worker after first login after a 14 day break --- app/controllers/application_controller.rb | 9 ++++++++- app/controllers/oauth/authorizations_controller.rb | 7 +++++++ app/models/feed.rb | 12 ++---------- app/workers/regeneration_worker.rb | 4 ++-- 4 files changed, 19 insertions(+), 13 deletions(-) (limited to 'app') diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ef9364897..c06142fd4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -39,7 +39,14 @@ class ApplicationController < ActionController::Base end def set_user_activity - current_user.touch(:current_sign_in_at) if !current_user.nil? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < 24.hours.ago) + return unless !current_user.nil? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < 24.hours.ago) + + # Mark user as signed-in today + current_user.update_tracked_fields(request) + + # If the sign in is after a two week break, we need to regenerate their feed + RegenerationWorker.perform_async(current_user.account_id) if current_user.last_sign_in_at < 14.days.ago + return end def check_suspension diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index feaad04f6..7c25266d8 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -3,6 +3,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController skip_before_action :authenticate_resource_owner! + before_action :set_locale before_action :store_current_location before_action :authenticate_resource_owner! @@ -11,4 +12,10 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController def store_current_location store_location_for(:user, request.url) end + + def set_locale + I18n.locale = current_user.try(:locale) || I18n.default_locale + rescue I18n::InvalidLocale + I18n.locale = I18n.default_locale + end end diff --git a/app/models/feed.rb b/app/models/feed.rb index 5e1905e15..3cbc160a0 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -10,17 +10,9 @@ class Feed max_id = '+inf' if max_id.blank? since_id = '-inf' if since_id.blank? unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i) + status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h - # If we're after most recent items and none are there, we need to precompute the feed - if unhydrated.empty? && max_id == '+inf' && since_id == '-inf' - RegenerationWorker.perform_async(@account.id, @type) - @statuses = Status.send("as_#{@type}_timeline", @account).cache_ids.paginate_by_max_id(limit, nil, nil) - else - status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h - @statuses = unhydrated.map { |id| status_map[id] }.compact - end - - @statuses + unhydrated.map { |id| status_map[id] }.compact end private diff --git a/app/workers/regeneration_worker.rb b/app/workers/regeneration_worker.rb index 289b63d84..82665b581 100644 --- a/app/workers/regeneration_worker.rb +++ b/app/workers/regeneration_worker.rb @@ -5,7 +5,7 @@ class RegenerationWorker sidekiq_options queue: 'pull', backtrace: true - def perform(account_id, timeline_type) - PrecomputeFeedService.new.call(timeline_type, Account.find(account_id)) + def perform(account_id, _ = :home) + PrecomputeFeedService.new.call(:home, Account.find(account_id)) end end -- cgit From eb023beb4975a019d6a3b3091483c91c2c837bbd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 4 Apr 2017 02:03:16 +0200 Subject: Fix #808 - smaller elephant friend PNG for frontpage --- app/assets/images/fluffy-elephant-friend.png | Bin 1101408 -> 60667 bytes 1 file changed, 0 insertions(+), 0 deletions(-) (limited to 'app') diff --git a/app/assets/images/fluffy-elephant-friend.png b/app/assets/images/fluffy-elephant-friend.png index 11787e936..f0df29927 100644 Binary files a/app/assets/images/fluffy-elephant-friend.png and b/app/assets/images/fluffy-elephant-friend.png differ -- cgit