From db7c7d1af154a17808f3379a357c511b669be51c Mon Sep 17 00:00:00 2001 From: Jessica Stokes Date: Tue, 11 Apr 2017 00:27:30 +1000 Subject: [PATCH 01/25] Improve scrolling behaviour (#1415) * Replace column margin with padding This improves horizontal scrolling behaviour significantly; scrolled flex elements are... a little weird. * Move clear column button styling to css --- .../components/clear_column_button.jsx | 12 +- app/assets/stylesheets/components.scss | 141 ++++++++++-------- 2 files changed, 78 insertions(+), 75 deletions(-) diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx index 71877fb2bcc..debbfd01f1f 100644 --- a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx +++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx @@ -4,16 +4,6 @@ const messages = defineMessages({ clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' } }); -const iconStyle = { - fontSize: '16px', - padding: '15px', - position: 'absolute', - right: '48px', - top: '0', - cursor: 'pointer', - zIndex: '2' -}; - const ClearColumnButton = React.createClass({ propTypes: { @@ -25,7 +15,7 @@ const ClearColumnButton = React.createClass({ const { intl } = this.props; return ( -
+
); diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index f10fc690230..8775ce6fdcb 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -49,6 +49,22 @@ } } +.column-icon-clear { + font-size: 16px; + padding: 15px; + position: absolute; + right: 48px; + top: 0; + cursor: pointer; + z-index: 2; +} + +@media screen and (min-width: 1024px) { + .column-icon-clear { + top: 10px; + } +} + .icon-button { display: inline-block; padding: 0; @@ -714,15 +730,7 @@ a.status__content__spoiler-link { @media screen and (min-width: 360px) { .columns-area { - margin: 0; - } - - .column:first-child, .drawer:first-child { - margin-left: 0; - } - - .column:last-child, .drawer:last-child { - margin-right: 0; + padding: 10px; } } @@ -730,9 +738,12 @@ a.status__content__spoiler-link { width: 330px; position: relative; box-sizing: border-box; - background: $color1; display: flex; flex-direction: column; + + > .scrollable { + background: $color1; + } } .ui { @@ -764,6 +775,58 @@ a.status__content__spoiler-link { border-bottom: 2px solid transparent; } +.column, .drawer { + flex: 1 1 100%; + overflow: hidden; +} + +@media screen and (min-width: 360px) { + .tabs-bar { + margin: 10px; + margin-bottom: 0; + } + + .search { + margin-bottom: 10px; + } +} + +@media screen and (max-width: 1024px) { + .column, .drawer { + width: 100%; + padding: 0; + } + + .columns-area { + flex-direction: column; + } + + .search__input, .autosuggest-textarea__textarea { + font-size: 16px; + } +} + +@media screen and (min-width: 1024px) { + .columns-area { + padding: 0; + } + + .column, .drawer { + flex: 0 0 auto; + padding: 10px; + padding-left: 5px; + padding-right: 5px; + + &:first-child { + padding-left: 10px; + } + + &:last-child { + padding-right: 10px; + } + } +} + @media screen and (min-width: 2560px) { .columns-area { justify-content: center; @@ -823,38 +886,6 @@ a.status__content__spoiler-link { } } -.column, .drawer { - margin: 10px; - margin-left: 5px; - margin-right: 5px; - flex: 0 0 auto; - overflow: hidden; -} - -.column:first-child, .drawer:first-child { - margin-left: 10px; -} - -.column:last-child, .drawer:last-child { - margin-right: 10px; -} - -@media screen and (max-width: 1024px) { - .column, .drawer { - width: 100%; - margin: 0; - flex: 1 1 100%; - } - - .columns-area { - flex-direction: column; - } - - .search__input, .autosuggest-textarea__textarea { - font-size: 16px; - } -} - .tabs-bar { display: flex; background: lighten($color1, 8%); @@ -895,27 +926,6 @@ a.status__content__spoiler-link { } } -@media screen and (min-width: 360px) { - .columns-area { - margin: 10px; - } - - .tabs-bar { - margin: 10px; - margin-bottom: 0; - } - - .search { - margin-bottom: 10px; - } -} - -@media screen and (min-width: 1024px) { - .columns-area { - margin: 0; - } -} - @media screen and (min-width: 600px) { .tabs-bar__link { span { @@ -1379,12 +1389,15 @@ button.active i.fa-retweet { .empty-column-indicator { color: lighten($color1, 20%); + background: $color1; text-align: center; padding: 20px; - padding-top: 100px; font-size: 15px; font-weight: 400; cursor: default; + display: flex; + flex: 1 1 auto; + align-items: center; a { color: $color4; From 087ca3009b83f2ae2299cce8d5e4c106e38c2a29 Mon Sep 17 00:00:00 2001 From: Corey Dutson Date: Mon, 10 Apr 2017 14:56:14 -0400 Subject: [PATCH 02/25] Adjust background of emoji panel (#1461) Addresses #1451 which notes the emoji picker is too light. I agree, so I submit this adjustment. Changes: Changed the background to a darkened version of another system color --- app/assets/stylesheets/components.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 8775ce6fdcb..95e432cb659 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1425,7 +1425,7 @@ button.active i.fa-retweet { .emoji-dialog { width: 280px; height: 220px; - background: $color2; + background: darken($color3, 10%); box-sizing: border-box; border-radius: 2px; overflow: hidden; From 1be6aa0c7fdac51e81ff7ee0c2b9184ed29ca3de Mon Sep 17 00:00:00 2001 From: Alexsander Akers Date: Mon, 10 Apr 2017 21:11:59 +0200 Subject: [PATCH 03/25] Fix references to "v1" API (#1460) References to `vi` API version replaced with `v1` --- docs/Using-the-API/API.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Using-the-API/API.md b/docs/Using-the-API/API.md index bd7915a3570..fee1fde942e 100644 --- a/docs/Using-the-API/API.md +++ b/docs/Using-the-API/API.md @@ -362,15 +362,15 @@ Returns an empty object. #### Reblogging/unreblogging a status: - POST /api/vi/statuses/:id/reblog - POST /api/vi/statuses/:id/unreblog + POST /api/v1/statuses/:id/reblog + POST /api/v1/statuses/:id/unreblog Returns the target [Status](#status). #### Favouriting/unfavouriting a status: - POST /api/vi/statuses/:id/favourite - POST /api/vi/statuses/:id/unfavourite + POST /api/v1/statuses/:id/favourite + POST /api/v1/statuses/:id/unfavourite Returns the target [Status](#status). From dbe9f33fdc9a995b07ff3b1dcd93ad02cd336649 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 10 Apr 2017 15:27:03 -0400 Subject: [PATCH 04/25] Admin base controller (#1465) * Add Admin::BaseController to wrap admin area Extracts the setting of the `admin` layout and verifying that users are admins to a common base class for the admin/ controllers. * Add basic coverage for admin/reports and admin/settings controllers --- app/controllers/admin/accounts_controller.rb | 73 +++++++++---------- app/controllers/admin/base_controller.rb | 9 +++ .../admin/domain_blocks_controller.rb | 42 +++++------ .../admin/pubsubhubbub_controller.rb | 12 ++- app/controllers/admin/reports_controller.rb | 65 ++++++++--------- app/controllers/admin/settings_controller.rb | 50 ++++++------- .../admin/reports_controller_spec.rb | 14 ++++ .../admin/settings_controller_spec.rb | 14 ++++ 8 files changed, 154 insertions(+), 125 deletions(-) create mode 100644 app/controllers/admin/base_controller.rb create mode 100644 spec/controllers/admin/reports_controller_spec.rb create mode 100644 spec/controllers/admin/settings_controller_spec.rb diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index df2c7bebfcc..60b631ece49 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -1,51 +1,50 @@ # frozen_string_literal: true -class Admin::AccountsController < ApplicationController - before_action :require_admin! - before_action :set_account, except: :index +module Admin + class AccountsController < BaseController + before_action :set_account, except: :index - layout 'admin' + def index + @accounts = Account.alphabetic.paginate(page: params[:page], per_page: 40) - def index - @accounts = Account.alphabetic.paginate(page: params[:page], per_page: 40) + @accounts = @accounts.local if params[:local].present? + @accounts = @accounts.remote if params[:remote].present? + @accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present? + @accounts = @accounts.silenced if params[:silenced].present? + @accounts = @accounts.recent if params[:recent].present? + @accounts = @accounts.suspended if params[:suspended].present? + end - @accounts = @accounts.local if params[:local].present? - @accounts = @accounts.remote if params[:remote].present? - @accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present? - @accounts = @accounts.silenced if params[:silenced].present? - @accounts = @accounts.recent if params[:recent].present? - @accounts = @accounts.suspended if params[:suspended].present? - end + def show; end - def show; end + def suspend + Admin::SuspensionWorker.perform_async(@account.id) + redirect_to admin_accounts_path + end - def suspend - Admin::SuspensionWorker.perform_async(@account.id) - redirect_to admin_accounts_path - end + def unsuspend + @account.update(suspended: false) + redirect_to admin_accounts_path + end - def unsuspend - @account.update(suspended: false) - redirect_to admin_accounts_path - end + def silence + @account.update(silenced: true) + redirect_to admin_accounts_path + end - def silence - @account.update(silenced: true) - redirect_to admin_accounts_path - end + def unsilence + @account.update(silenced: false) + redirect_to admin_accounts_path + end - def unsilence - @account.update(silenced: false) - redirect_to admin_accounts_path - end + private - private + def set_account + @account = Account.find(params[:id]) + end - def set_account - @account = Account.find(params[:id]) - end - - def account_params - params.require(:account).permit(:silenced, :suspended) + def account_params + params.require(:account).permit(:silenced, :suspended) + end end end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb new file mode 100644 index 00000000000..11fe326bce1 --- /dev/null +++ b/app/controllers/admin/base_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Admin + class BaseController < ApplicationController + before_action :require_admin! + + layout 'admin' + end +end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 1f443284784..58f1efa5b4b 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -1,32 +1,30 @@ # frozen_string_literal: true -class Admin::DomainBlocksController < ApplicationController - before_action :require_admin! +module Admin + class DomainBlocksController < BaseController + def index + @blocks = DomainBlock.paginate(page: params[:page], per_page: 40) + end - layout 'admin' + def new + @domain_block = DomainBlock.new + end - def index - @blocks = DomainBlock.paginate(page: params[:page], per_page: 40) - end + def create + @domain_block = DomainBlock.new(resource_params) - def new - @domain_block = DomainBlock.new - end + 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 - def create - @domain_block = DomainBlock.new(resource_params) + private - 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 + def resource_params + params.require(:domain_block).permit(:domain, :severity) end end - - private - - def resource_params - params.require(:domain_block).permit(:domain, :severity) - end end diff --git a/app/controllers/admin/pubsubhubbub_controller.rb b/app/controllers/admin/pubsubhubbub_controller.rb index b9e840ffe48..95f79c52096 100644 --- a/app/controllers/admin/pubsubhubbub_controller.rb +++ b/app/controllers/admin/pubsubhubbub_controller.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -class Admin::PubsubhubbubController < ApplicationController - before_action :require_admin! - - layout 'admin' - - def index - @subscriptions = Subscription.order('id desc').includes(:account).paginate(page: params[:page], per_page: 40) +module Admin + class PubsubhubbubController < BaseController + def index + @subscriptions = Subscription.order('id desc').includes(:account).paginate(page: params[:page], per_page: 40) + end end end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 2b3b1809fdd..5a37d8e6e9d 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -1,45 +1,44 @@ # frozen_string_literal: true -class Admin::ReportsController < ApplicationController - before_action :require_admin! - before_action :set_report, except: [:index] +module Admin + class ReportsController < BaseController + before_action :set_report, except: [:index] - layout 'admin' + def index + @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 - def index - @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 + def show + @statuses = Status.where(id: @report.status_ids) + end - def show - @statuses = Status.where(id: @report.status_ids) - end + def resolve + @report.update(action_taken: true, action_taken_by_account_id: current_account.id) + redirect_to admin_report_path(@report) + end - def resolve - @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, 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, 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, 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, action_taken_by_account_id: current_account.id) - redirect_to admin_report_path(@report) - end + def remove + RemovalWorker.perform_async(params[:status_id]) + redirect_to admin_report_path(@report) + end - def remove - RemovalWorker.perform_async(params[:status_id]) - redirect_to admin_report_path(@report) - end + private - private - - def set_report - @report = Report.find(params[:id]) + def set_report + @report = Report.find(params[:id]) + end end end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 7615c781d5f..6cca5c3e3fb 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -1,35 +1,33 @@ # frozen_string_literal: true -class Admin::SettingsController < ApplicationController - before_action :require_admin! - - layout 'admin' - - def index - @settings = Setting.all_as_records - end - - def update - @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id]) - value = settings_params[:value] - - # Special cases - value = value == 'true' if @setting.var == 'open_registrations' - - if @setting.value != value - @setting.value = value - @setting.save +module Admin + class SettingsController < BaseController + def index + @settings = Setting.all_as_records end - respond_to do |format| - format.html { redirect_to admin_settings_path } - format.json { respond_with_bip(@setting) } + def update + @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id]) + value = settings_params[:value] + + # Special cases + value = value == 'true' if @setting.var == 'open_registrations' + + if @setting.value != value + @setting.value = value + @setting.save + end + + respond_to do |format| + format.html { redirect_to admin_settings_path } + format.json { respond_with_bip(@setting) } + end end - end - private + private - def settings_params - params.require(:setting).permit(:value) + def settings_params + params.require(:setting).permit(:value) + end end end diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb new file mode 100644 index 00000000000..622ea87c1c1 --- /dev/null +++ b/spec/controllers/admin/reports_controller_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe Admin::ReportsController, type: :controller do + describe 'GET #index' do + before do + sign_in Fabricate(:user, admin: true), scope: :user + end + + it 'returns http success' do + get :index + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb new file mode 100644 index 00000000000..c126b645bec --- /dev/null +++ b/spec/controllers/admin/settings_controller_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe Admin::SettingsController, type: :controller do + describe 'GET #index' do + before do + sign_in Fabricate(:user, admin: true), scope: :user + end + + it 'returns http success' do + get :index + expect(response).to have_http_status(:success) + end + end +end From d2f6d9b9fbe59575846141ca094324da9cd7de4d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 10 Apr 2017 15:27:52 -0400 Subject: [PATCH 05/25] Fix issue with missing emojify class in views (#1455) * Add missing emojify class to landing strip * Add missing emojify class to simple_status partial --- app/views/shared/_landing_strip.html.haml | 5 ++++- app/views/stream_entries/_status.html.haml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml index bb081e54457..3536c5ca8ce 100644 --- a/app/views/shared/_landing_strip.html.haml +++ b/app/views/shared/_landing_strip.html.haml @@ -1,2 +1,5 @@ .landing-strip - = t('landing_strip_html', name: display_name(account), domain: Rails.configuration.x.local_domain, sign_up_path: new_user_registration_path) + = t('landing_strip_html', + name: content_tag(:span, display_name(account), class: :emojify), + domain: Rails.configuration.x.local_domain, + sign_up_path: new_user_registration_path) diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml index 434c5c8da2d..1333d4d82a5 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/stream_entries/_status.html.haml @@ -13,7 +13,7 @@ = fa_icon('retweet fw') %span = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do - %strong= display_name(status.account) + %strong.emojify= display_name(status.account) = t('stream_entries.reblogged') = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper } From 8a6096a3de66f0d6bd0b7133b5873d35ae10edea Mon Sep 17 00:00:00 2001 From: Jessica Stokes Date: Tue, 11 Apr 2017 05:30:58 +1000 Subject: [PATCH 06/25] Allow typing a toot while an image uploads (#1429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch stops disabling the toot text field when an image is uploading. Instead, you can type to your heart's content and when the image uploads it'll append the image URL, and restore the position of your cursor to wherever it was in the toot text just prior. Effectively, the image URL is appended to the toot, and typing is not interrupted at all! ✨ --- .../compose/components/compose_form.jsx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx index b016d3f28ea..cb4b62f6cf1 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -83,11 +83,23 @@ const ComposeForm = React.createClass({ this.props.onChangeSpoilerText(e.target.value); }, + componentWillReceiveProps (nextProps) { + // If this is the update where we've finished uploading, + // save the last caret position so we can restore it below! + if (!nextProps.is_uploading && this.props.is_uploading) { + this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart; + } + }, + componentDidUpdate (prevProps) { - if (this.props.focusDate !== prevProps.focusDate) { - // If replying to zero or one users, places the cursor at the end of the textbox. - // If replying to more than one user, selects any usernames past the first; - // this provides a convenient shortcut to drop everyone else from the conversation. + // This statement does several things: + // - If we're beginning a reply, and, + // - Replying to zero or one users, places the cursor at the end of the textbox. + // - Replying to more than one user, selects any usernames past the first; + // this provides a convenient shortcut to drop everyone else from the conversation. + // - If we've just finished uploading an image, and have a saved caret position, + // restores the cursor to that position after the text changes! + if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) { let selectionEnd, selectionStart; if (this.props.preselectDate !== prevProps.preselectDate) { @@ -118,7 +130,7 @@ const ComposeForm = React.createClass({ render () { const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props; - const disabled = this.props.is_submitting || this.props.is_uploading; + const disabled = this.props.is_submitting; let publishText = ''; let privacyWarning = ''; From 553170b77acea8867cb67deca7c24ce9e16c9c48 Mon Sep 17 00:00:00 2001 From: Stephen Burgess Date: Mon, 10 Apr 2017 14:31:26 -0500 Subject: [PATCH 07/25] Fix #1097 When onClick is falsy, do not make status content clickable (#1434) --- .../components/components/status_content.jsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index 6c25afdea53..52826ace0f6 100644 --- a/app/assets/javascripts/components/components/status_content.jsx +++ b/app/assets/javascripts/components/components/status_content.jsx @@ -125,7 +125,7 @@ const StatusContent = React.createClass({ ); - } else { + } else if (this.props.onClick) { return (
); + } else { + return ( +
+ ); } }, From f690320fb93ea1a884a292f9e099eb1bc0834b6d Mon Sep 17 00:00:00 2001 From: Rachel H Date: Mon, 10 Apr 2017 12:32:45 -0700 Subject: [PATCH 08/25] Keep newlines in xml (#1397) --- app/assets/javascripts/components/components/status_content.jsx | 2 +- app/lib/formatter.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index 52826ace0f6..e0cca6113c5 100644 --- a/app/assets/javascripts/components/components/status_content.jsx +++ b/app/assets/javascripts/components/components/status_content.jsx @@ -91,7 +91,7 @@ const StatusContent = React.createClass({ const { status } = this.props; const { hidden } = this.state; - const content = { __html: emojify(status.get('content')) }; + const content = { __html: emojify(status.get('content')).replace(/\n/g, '') }; const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) }; const directionStyle = { direction: 'ltr' }; diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index da7ad202751..c3f331ff7d0 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -15,7 +15,6 @@ class Formatter html = status.text html = encode(html) html = simple_format(html, {}, sanitize: false) - html = html.gsub(/\n/, '') html = link_urls(html) html = link_mentions(html, status.mentions) html = link_hashtags(html) From 0dbbc16c69652afe63d0b3dd49c06939351e3bc6 Mon Sep 17 00:00:00 2001 From: Alexander Mankuta Date: Mon, 10 Apr 2017 22:48:30 +0300 Subject: [PATCH 09/25] More SMTP customization (#1372) * Allow SMTP auth method customization * Add SMTP openssl_verify_mode option support Allows one use self-signed certs with their SMTP server. * Add SMTP enable_starttls_auto option support --- .env.production.sample | 4 ++++ app.json | 12 ++++++++++++ config/environments/production.rb | 4 +++- scalingo.json | 12 ++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.env.production.sample b/.env.production.sample index fbb28470789..97bba5e3f07 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -35,6 +35,10 @@ SMTP_PORT=587 SMTP_LOGIN= SMTP_PASSWORD= SMTP_FROM_ADDRESS=notifications@example.com +#SMTP_AUTH_METHOD=plain +#SMTP_OPENSSL_VERIFY_MODE=peer +#SMTP_ENABLE_STARTTLS_AUTO=true + # Optional asset host for multi-server setups # CDN_HOST=assets.example.com diff --git a/app.json b/app.json index 29c1f9f9cfb..6c4294c79e5 100644 --- a/app.json +++ b/app.json @@ -79,6 +79,18 @@ "SMTP_FROM_ADDRESS": { "description": "Address to send emails from", "required": false + }, + "SMTP_AUTH_METHOD": { + "description": "Authentication method to use with SMTP server. Default is 'plain'.", + "required": false + }, + "SMTP_OPENSSL_VERIFY_MODE": { + "description": "SMTP server certificate verification mode. Defaults is 'peer'.", + "required": false + }, + "SMTP_ENABLE_STARTTLS_AUTO": { + "description": "Enable STARTTLS if SMTP server supports it? Default is true.", + "required": false } }, "buildpacks": [ diff --git a/config/environments/production.rb b/config/environments/production.rb index d299e4f4ca4..05cced67b55 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -99,7 +99,9 @@ Rails.application.configure do :user_name => ENV['SMTP_LOGIN'], :password => ENV['SMTP_PASSWORD'], :domain => ENV['SMTP_DOMAIN'] || config.x.local_domain, - :authentication => :plain, + :authentication => ENV['SMTP_AUTH_METHOD'] || :plain, + :openssl_verify_mode => ENV['SMTP_OPENSSL_VERIFY_MODE'] || 'peer', + :enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] || true, } config.action_mailer.delivery_method = :smtp diff --git a/scalingo.json b/scalingo.json index d60f1529ce8..4afaa6b4e01 100644 --- a/scalingo.json +++ b/scalingo.json @@ -71,6 +71,18 @@ "description": "Address to send emails from", "required": false }, + "SMTP_AUTH_METHOD": { + "description": "Authentication method to use with SMTP server. Default is 'plain'.", + "required": false + }, + "SMTP_OPENSSL_VERIFY_MODE": { + "description": "SMTP server certificate verification mode. Defaults is 'peer'.", + "required": false + }, + "SMTP_ENABLE_STARTTLS_AUTO": { + "description": "Enable STARTTLS if SMTP server supports it? Default is true.", + "required": false + }, "BUILDPACK_URL": { "description": "Internal scalingo configuration", "required": true, From ae57b3a8c5b8ab0bdc11fac9c5e8d9277fb1f95d Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Mon, 10 Apr 2017 16:41:52 -0400 Subject: [PATCH 10/25] Add more specific class names to notification divs (#1120) --- .../features/notifications/components/notification.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx index 0de4df52ebe..c9279b20e82 100644 --- a/app/assets/javascripts/components/features/notifications/components/notification.jsx +++ b/app/assets/javascripts/components/features/notifications/components/notification.jsx @@ -21,7 +21,7 @@ const Notification = React.createClass({ renderFollow (account, link) { return ( -
+
@@ -41,7 +41,7 @@ const Notification = React.createClass({ renderFavourite (notification, link) { return ( -
+
@@ -57,7 +57,7 @@ const Notification = React.createClass({ renderReblog (notification, link) { return ( -
+
From 64dbde0dbf8c5d3ce820f780644d46a6b18e6743 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 10 Apr 2017 16:47:41 -0400 Subject: [PATCH 11/25] Version bumps for ruby and misc gems (#1159) * Update rspec-rails to version 3.5.2 * Update addressable to version 2.5.1 * Update autoprefixer-rails to version 6.7.7.1 * Update bullet to version 5.5.1 * Update domain_name to version 0.5.20170404 * Update letter_opener_web to version 1.3.1 * Upate redis-rails to version 5.0.2 * Update active_record_query_trace to version 1.5.4 * Update capistrano-rails to version 1.2.3 * Update dotenv-rails to version 2.2.0 * Update pg to version 0.20.0 * Update tilt to version 2.0.7 * Update warden to version 1.2.7 * Update tins to version 1.13.2 * Update terminal-table to version 1.7.3 * Update oj to version 2.18.5 * Update simplecov to version 0.14.1 * Update uglifier to version 3.1.13 * Update hashdiff to version 0.3.2 * Update webmock to version 2.3.2 * Update devise to version 4.2.1 * Use ruby version 2.4.1 * Update sass to version 3.4.23 * Update puma to version 3.8.2 * Update will_paginate to version 3.1.5 * Update font-awesome-rails to version 4.7.0.1 * Update fuubar to version 2.2.0 * Update pry-rails to version 0.3.6 * Update simple-navigation to version 4.0.5 * Update rubocop to version 0.48.1 * Update doorkeeper to version 4.2.5 * Update faker to version 1.7.3 * Update aws-sdk to version 2.9.5 * Update fabrication to version 2.16.1 * Update hamlit-rails to version 0.2.0 * Update http to version 2.2.1 * Update httplog to version 0.99.2 * Update sidekiq to version 4.2.10 * Update rspec-sidekiq to version 3.0.0 * Update pghero to version 1.6.4 * Update rack-cors to version 0.4.1 * Update i18n-tasks to version 0.9.13 * Update ruby-oembed to version 0.12.0 * Update jquery-rails to version 4.3.1 * Update simple_form to version 3.4.0 * Update react-rails to version 1.11.0 * Update aws-sdk to version 2.9.6 * Update sidekiq-unique-jobs to version 5.0.0 * Update uglifier to version 3.2.0 --- .ruby-version | 2 +- .travis.yml | 2 +- Dockerfile | 2 +- Gemfile | 2 +- Gemfile.lock | 188 ++++++++++------------ config/initializers/httplog.rb | 8 +- docs/Running-Mastodon/Production-guide.md | 2 +- 7 files changed, 99 insertions(+), 107 deletions(-) diff --git a/.ruby-version b/.ruby-version index 2bf1c1ccf36..005119baaa0 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.1 +2.4.1 diff --git a/.travis.yml b/.travis.yml index b1b0c2bcda2..a9824ccf7d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ addons: postgresql: 9.4 rvm: - - 2.3.1 + - 2.4.1 services: - redis-server diff --git a/Dockerfile b/Dockerfile index 57a8f34e97e..a05525b33a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.3.1-alpine +FROM ruby:2.4.1-alpine LABEL maintainer="https://github.com/tootsuite/mastodon" \ description="A GNU Social-compatible microblogging server" diff --git a/Gemfile b/Gemfile index 7895a345c1a..8810c83defb 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '2.3.1' +ruby '2.4.1' gem 'rails', '~> 5.0.2' gem 'sass-rails', '~> 5.0' diff --git a/Gemfile.lock b/Gemfile.lock index aa048aace0f..6e38655272d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,7 +24,7 @@ GEM erubis (~> 2.7.0) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - active_record_query_trace (1.5.3) + active_record_query_trace (1.5.4) activejob (5.0.2) activesupport (= 5.0.2) globalid (>= 0.3.6) @@ -39,7 +39,7 @@ GEM i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.5.0) + addressable (2.5.1) public_suffix (~> 2.0, >= 2.0.2) airbrussh (1.1.2) sshkit (>= 1.6.1, != 1.7.0) @@ -47,17 +47,17 @@ GEM ast (2.3.0) attr_encrypted (3.0.3) encryptor (~> 3.0.0) - autoprefixer-rails (6.5.0.2) + autoprefixer-rails (6.7.7.1) execjs av (0.9.0) cocaine (~> 0.5.3) - aws-sdk (2.6.28) - aws-sdk-resources (= 2.6.28) - aws-sdk-core (2.6.28) + aws-sdk (2.9.6) + aws-sdk-resources (= 2.9.6) + aws-sdk-core (2.9.6) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-resources (2.6.28) - aws-sdk-core (= 2.6.28) + aws-sdk-resources (2.9.6) + aws-sdk-core (= 2.9.6) aws-sigv4 (1.0.0) babel-source (5.8.35) babel-transpiler (0.7.0) @@ -78,12 +78,11 @@ GEM railties (>= 4.0.0, < 5.1) sprockets (>= 3.6.0) builder (3.2.3) - bullet (5.3.0) + bullet (5.5.1) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) - capistrano (3.7.2) + capistrano (3.8.0) airbrussh (>= 1.0.0) - capistrano-harrow i18n rake (>= 10.0.0) sshkit (>= 1.9.0) @@ -92,8 +91,7 @@ GEM sshkit (~> 1.2) capistrano-faster-assets (1.0.2) capistrano (>= 3.1) - capistrano-harrow (0.5.3) - capistrano-rails (1.2.2) + capistrano-rails (1.2.3) capistrano (~> 3.1) capistrano-bundler (~> 1.1) capistrano-rbenv (2.1.0) @@ -119,7 +117,7 @@ GEM crack (0.4.3) safe_yaml (~> 1.0.0) debug_inspector (0.0.2) - devise (4.2.0) + devise (4.2.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0, < 5.1) @@ -131,16 +129,16 @@ GEM devise (~> 4.0) railties rotp (~> 2.0) - diff-lcs (1.2.5) + diff-lcs (1.3) docile (1.1.5) - domain_name (0.5.20161129) + domain_name (0.5.20170404) unf (>= 0.0.5, < 1.0.0) - doorkeeper (4.2.0) + doorkeeper (4.2.5) railties (>= 4.2) - dotenv (2.1.1) - dotenv-rails (2.1.1) - dotenv (= 2.1.1) - railties (>= 4.0, < 5.1) + dotenv (2.2.0) + dotenv-rails (2.2.0) + dotenv (= 2.2.0) + railties (>= 3.2, < 5.1) easy_translate (0.5.0) json thread @@ -148,14 +146,14 @@ GEM encryptor (3.0.0) erubis (2.7.0) execjs (2.7.0) - fabrication (2.15.2) - faker (1.6.6) + fabrication (2.16.1) + faker (1.7.3) i18n (~> 0.5) fast_blank (1.0.0) - font-awesome-rails (4.6.3.1) + font-awesome-rails (4.7.0.1) railties (>= 3.2, < 5.1) - fuubar (2.1.1) - rspec (~> 3.0) + fuubar (2.2.0) + rspec-core (~> 3.0) ruby-progressbar (~> 1.4) globalid (0.3.7) activesupport (>= 4.1.0) @@ -163,20 +161,20 @@ GEM addressable (~> 2.4) http (~> 2.0) nokogiri (~> 1.6) - hamlit (2.7.2) - temple (~> 0.7.6) + hamlit (2.8.1) + temple (>= 0.8.0) thor tilt - hamlit-rails (0.1.0) + hamlit-rails (0.2.0) actionpack (>= 4.0.1) activesupport (>= 4.0.1) hamlit (>= 1.2.0) railties (>= 4.0.1) - hashdiff (0.3.0) + hashdiff (0.3.2) highline (1.7.8) hiredis (0.6.1) htmlentities (4.3.4) - http (2.1.0) + http (2.2.1) addressable (~> 2.3) http-cookie (~> 1.0) http-form_data (~> 1.0.1) @@ -186,10 +184,10 @@ GEM http-form_data (1.0.1) http_accept_language (2.1.0) http_parser.rb (0.6.0) - httplog (0.3.2) + httplog (0.99.2) colorize i18n (0.8.1) - i18n-tasks (0.9.6) + i18n-tasks (0.9.13) activesupport (>= 4.0.2) ast (>= 2.1.0) easy_translate (>= 0.5.0) @@ -197,19 +195,19 @@ GEM highline (>= 1.7.3) i18n parser (>= 2.2.3.0) - term-ansicolor (>= 1.3.2) + rainbow (~> 2.2) terminal-table (>= 1.5.1) jmespath (1.3.1) - jquery-rails (4.1.1) + jquery-rails (4.3.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (1.8.3) + json (2.0.3) launchy (2.4.3) addressable (~> 2.3) letter_opener (1.4.1) launchy (~> 2.2) - letter_opener_web (1.3.0) + letter_opener_web (1.3.1) actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) @@ -231,11 +229,11 @@ GEM minitest (5.10.1) net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (4.0.1) + net-ssh (4.1.0) nio4r (2.0.0) nokogiri (1.7.1) mini_portile2 (~> 2.1.0) - oj (2.17.3) + oj (2.18.5) orm_adapter (0.5.0) ostatus2 (1.0.2) addressable (~> 2.4) @@ -251,26 +249,26 @@ GEM paperclip-av-transcoder (0.6.4) av (~> 0.9.0) paperclip (>= 2.5.2) - parser (2.3.1.2) + parser (2.4.0.0) ast (~> 2.2) - pg (0.18.4) - pghero (1.6.2) + pg (0.20.0) + pghero (1.6.4) activerecord powerpack (0.1.1) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - pry-rails (0.3.4) - pry (>= 0.9.10) - public_suffix (2.0.4) - puma (3.6.0) + pry-rails (0.3.6) + pry (>= 0.10.4) + public_suffix (2.0.5) + puma (3.8.2) rabl (0.13.1) activesupport (>= 2.3.14) rack (2.0.1) rack-attack (5.0.1) rack - rack-cors (0.4.0) + rack-cors (0.4.1) rack-protection (1.5.3) rack rack-test (0.6.3) @@ -306,42 +304,37 @@ GEM method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.1.0) + rainbow (2.2.1) rake (12.0.0) - react-rails (1.10.0) + react-rails (1.11.0) babel-transpiler (>= 0.7.0) - coffee-script-source (~> 1.8) connection_pool execjs railties (>= 3.2) tilt - redis (3.3.2) - redis-actionpack (5.0.0) - actionpack (>= 4.0.0, < 6) - redis-rack (~> 2.0.0.pre) - redis-store (~> 1.2.0.pre) - redis-activesupport (5.0.1) + redis (3.3.3) + redis-actionpack (5.0.1) + actionpack (>= 4.0, < 6) + redis-rack (>= 1, < 3) + redis-store (>= 1.1.0, < 1.4.0) + redis-activesupport (5.0.2) activesupport (>= 3, < 6) - redis-store (~> 1.2.0) - redis-rack (2.0.0) - rack (~> 2.0) - redis-store (~> 1.2.0) - redis-rails (5.0.1) - redis-actionpack (~> 5.0.0) - redis-activesupport (~> 5.0.0) - redis-store (~> 1.2.0) - redis-store (1.2.0) + redis-store (~> 1.3.0) + redis-rack (2.0.1) + rack (>= 2.0, < 3) + redis-store (>= 1.2, < 1.4) + redis-rails (5.0.2) + redis-actionpack (>= 5.0, < 6) + redis-activesupport (>= 5.0, < 6) + redis-store (>= 1.2, < 2) + redis-store (1.3.0) redis (>= 2.2) responders (2.3.0) railties (>= 4.2.0, < 5.1) rotp (2.1.2) rqrcode (0.10.1) chunky_png (~> 1.0) - rspec (3.5.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-core (3.5.2) + rspec-core (3.5.4) rspec-support (~> 3.5.0) rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) @@ -349,7 +342,7 @@ GEM rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.5.0) - rspec-rails (3.5.1) + rspec-rails (3.5.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) @@ -357,40 +350,40 @@ GEM rspec-expectations (~> 3.5.0) rspec-mocks (~> 3.5.0) rspec-support (~> 3.5.0) - rspec-sidekiq (2.2.0) - rspec (~> 3.0, >= 3.0.0) + rspec-sidekiq (3.0.0) + rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.5.0) - rubocop (0.42.0) - parser (>= 2.3.1.1, < 3.0) + rubocop (0.48.1) + parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - ruby-oembed (0.10.1) + ruby-oembed (0.12.0) ruby-progressbar (1.8.1) safe_yaml (1.0.4) - sass (3.4.22) + sass (3.4.23) sass-rails (5.0.6) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sidekiq (4.2.7) + sidekiq (4.2.10) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) redis (~> 3.2, >= 3.2.1) - sidekiq-unique-jobs (4.0.18) - sidekiq (>= 2.6) + sidekiq-unique-jobs (5.0.0) + sidekiq (>= 4.0) thor - simple-navigation (4.0.3) + simple-navigation (4.0.5) activesupport (>= 2.3.2) - simple_form (3.2.1) + simple_form (3.4.0) actionpack (> 4, < 5.1) activemodel (> 4, < 5.1) - simplecov (0.12.0) + simplecov (0.14.1) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) @@ -403,43 +396,40 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.11.5) + sshkit (1.13.1) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) statsd-instrument (2.1.2) - temple (0.7.7) - term-ansicolor (1.4.0) - tins (~> 1.0) - terminal-table (1.7.0) - unicode-display_width (~> 1.1) + temple (0.8.0) + terminal-table (1.7.3) + unicode-display_width (~> 1.1.1) thor (0.19.4) thread (0.2.2) thread_safe (0.3.6) - tilt (2.0.6) - tins (1.12.0) + tilt (2.0.7) twitter-text (1.14.5) unf (~> 0.1.0) - tzinfo (1.2.2) + tzinfo (1.2.3) thread_safe (~> 0.1) tzinfo-data (1.2017.2) tzinfo (>= 1.0.0) - uglifier (3.0.1) + uglifier (3.2.0) execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext unf_ext (0.0.7.2) - unicode-display_width (1.1.0) + unicode-display_width (1.1.3) uniform_notifier (1.10.0) - warden (1.2.6) + warden (1.2.7) rack (>= 1.0) - webmock (2.1.0) + webmock (2.3.2) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) - will_paginate (3.1.0) + will_paginate (3.1.5) PLATFORMS ruby @@ -521,7 +511,7 @@ DEPENDENCIES will_paginate RUBY VERSION - ruby 2.3.1p112 + ruby 2.4.1p111 BUNDLED WITH - 1.14.5 + 1.14.6 diff --git a/config/initializers/httplog.rb b/config/initializers/httplog.rb index 37f113d5dbc..5cfc16a8b5f 100644 --- a/config/initializers/httplog.rb +++ b/config/initializers/httplog.rb @@ -1,3 +1,5 @@ -HttpLog.options[:logger] = Rails.logger -HttpLog.options[:color] = { color: :yellow } -HttpLog.options[:compact_log] = true +HttpLog.configure do |config| + config.logger = Rails.logger + config.color = { color: :yellow } + config.compact_log = true +end diff --git a/docs/Running-Mastodon/Production-guide.md b/docs/Running-Mastodon/Production-guide.md index 785826acac9..ec67a452c17 100644 --- a/docs/Running-Mastodon/Production-guide.md +++ b/docs/Running-Mastodon/Production-guide.md @@ -121,7 +121,7 @@ It is recommended to use rbenv (exclusively from the `mastodon` user) to install [2]: https://github.com/rbenv/ruby-build#installation [3]: https://github.com/rbenv/ruby-build/wiki#suggested-build-environment -Then once `rbenv` is ready, run `rbenv install 2.3.1` to install the Ruby version for Mastodon. +Then once `rbenv` is ready, run `rbenv install 2.4.1` to install the Ruby version for Mastodon. ## Git From 0687ab8ae3c2573ba2aa1d37f62e3583d0c7ab01 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 10 Apr 2017 16:58:06 -0400 Subject: [PATCH 12/25] Clean up generation of account webfinger string (#1477) * Consolidate webfinger string creation under Account#to_webfinger_s * Introduce Account#local_username_and_domain for consolidation --- app/controllers/accounts_controller.rb | 2 +- app/controllers/remote_follow_controller.rb | 2 +- .../settings/exports_controller.rb | 2 +- app/controllers/xrd_controller.rb | 2 +- app/helpers/atom_builder_helper.rb | 2 +- app/lib/atom_serializer.rb | 2 +- app/models/account.rb | 8 +++++++ spec/controllers/xrd_controller_spec.rb | 2 +- spec/models/account_spec.rb | 24 +++++++++++++++++++ 9 files changed, 39 insertions(+), 7 deletions(-) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 619c04be26e..34103de0e8f 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -53,7 +53,7 @@ class AccountsController < ApplicationController end def webfinger_account_url - webfinger_url(resource: "acct:#{@account.acct}@#{Rails.configuration.x.local_domain}") + webfinger_url(resource: @account.to_webfinger_s) end def check_account_suspension diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb index 1e3f786ec8b..22e376836e2 100644 --- a/app/controllers/remote_follow_controller.rb +++ b/app/controllers/remote_follow_controller.rb @@ -25,7 +25,7 @@ class RemoteFollowController < ApplicationController session[:remote_follow] = @remote_follow.acct - redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s + redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: @account.to_webfinger_s).to_s else render :new end diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 4fcec532250..ff688978cc3 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -39,7 +39,7 @@ class Settings::ExportsController < ApplicationController def accounts_list_to_csv(list) CSV.generate do |csv| list.each do |account| - csv << [(account.local? ? "#{account.username}@#{Rails.configuration.x.local_domain}" : account.acct)] + csv << [(account.local? ? account.local_username_and_domain : account.acct)] end end end diff --git a/app/controllers/xrd_controller.rb b/app/controllers/xrd_controller.rb index 6db87cefc49..5964172e9a3 100644 --- a/app/controllers/xrd_controller.rb +++ b/app/controllers/xrd_controller.rb @@ -14,7 +14,7 @@ class XrdController < ApplicationController def webfinger @account = Account.find_local!(username_from_resource) - @canonical_account_uri = "acct:#{@account.username}@#{Rails.configuration.x.local_domain}" + @canonical_account_uri = @account.to_webfinger_s @magic_key = pem_to_magic_key(@account.keypair.public_key) respond_to do |format| diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb index b750eeb0790..185388ec93c 100644 --- a/app/helpers/atom_builder_helper.rb +++ b/app/helpers/atom_builder_helper.rb @@ -160,7 +160,7 @@ module AtomBuilderHelper object_type xml, :person uri xml, TagManager.instance.uri_for(account) name xml, account.username - email xml, account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct + email xml, account.local? ? account.local_username_and_domain : account.acct summary xml, account.note link_alternate xml, TagManager.instance.url_for(account) link_avatar xml, account diff --git a/app/lib/atom_serializer.rb b/app/lib/atom_serializer.rb index 845d38c9254..68d2fce6887 100644 --- a/app/lib/atom_serializer.rb +++ b/app/lib/atom_serializer.rb @@ -20,7 +20,7 @@ class AtomSerializer append_element(author, 'activity:object-type', TagManager::TYPES[:person]) append_element(author, 'uri', uri) append_element(author, 'name', account.username) - append_element(author, 'email', account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct) + append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct) append_element(author, 'summary', account.note) append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account)) append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original))) diff --git a/app/models/account.rb b/app/models/account.rb index c59c760095a..a482fc8e6e1 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -120,6 +120,14 @@ class Account < ApplicationRecord local? ? username : "#{username}@#{domain}" end + def local_username_and_domain + "#{username}@#{Rails.configuration.x.local_domain}" + end + + def to_webfinger_s + "acct:#{local_username_and_domain}" + end + def subscribed? !subscription_expires_at.blank? end diff --git a/spec/controllers/xrd_controller_spec.rb b/spec/controllers/xrd_controller_spec.rb index e687cf9e0c0..b56c68f5c3f 100644 --- a/spec/controllers/xrd_controller_spec.rb +++ b/spec/controllers/xrd_controller_spec.rb @@ -14,7 +14,7 @@ RSpec.describe XrdController, type: :controller do let(:alice) { Fabricate(:account, username: 'alice') } it 'returns http success when account can be found' do - get :webfinger, params: { resource: "acct:#{alice.username}@#{Rails.configuration.x.local_domain}" } + get :webfinger, params: { resource: alice.to_webfinger_s } expect(response).to have_http_status(:success) end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 0c3b2b04282..0906bb0ae15 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -54,6 +54,30 @@ RSpec.describe Account, type: :model do end end + describe 'Local domain user methods' do + around do |example| + before = Rails.configuration.x.local_domain + example.run + Rails.configuration.x.local_domain = before + end + + describe '#to_webfinger_s' do + it 'returns a webfinger string for the account' do + Rails.configuration.x.local_domain = 'example.com' + + expect(subject.to_webfinger_s).to eq 'acct:alice@example.com' + end + end + + describe '#local_username_and_domain' do + it 'returns the username and local domain for the account' do + Rails.configuration.x.local_domain = 'example.com' + + expect(subject.local_username_and_domain).to eq 'alice@example.com' + end + end + end + describe '#acct' do it 'returns username for local users' do expect(subject.acct).to eql 'alice' From 2810013b933bceb2a7c1d1b8b10d2714c39d1e15 Mon Sep 17 00:00:00 2001 From: Eugen Date: Mon, 10 Apr 2017 23:45:29 +0200 Subject: [PATCH 13/25] API param to exclude notification types from response (#1341) * Add exclude_types param to /api/v1/notifications * Exclude notification types in web UI through exclude_types in the API --- Gemfile | 1 + Gemfile.lock | 5 ++ .../components/actions/notifications.jsx | 12 ++-- .../api/v1/notifications_controller.rb | 10 ++- app/models/notification.rb | 27 +++++--- .../api/v1/notifications_controller_spec.rb | 62 ++++++++++++++++++- 6 files changed, 100 insertions(+), 17 deletions(-) diff --git a/Gemfile b/Gemfile index 8810c83defb..078ac5806d2 100644 --- a/Gemfile +++ b/Gemfile @@ -68,6 +68,7 @@ end group :test do gem 'faker' + gem 'rails-controller-testing' gem 'rspec-sidekiq' gem 'simplecov', require: false gem 'webmock' diff --git a/Gemfile.lock b/Gemfile.lock index 6e38655272d..4fe8aa07600 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -286,6 +286,10 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 5.0.2) sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.1) + actionpack (~> 5.x) + actionview (~> 5.x) + activesupport (~> 5.x) rails-dom-testing (2.0.2) activesupport (>= 4.2.0, < 6.0) nokogiri (~> 1.6) @@ -487,6 +491,7 @@ DEPENDENCIES rack-cors rack-timeout rails (~> 5.0.2) + rails-controller-testing rails-settings-cached rails_12factor react-rails diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx index 980b7d63ea7..11e814e1fee 100644 --- a/app/assets/javascripts/components/actions/notifications.jsx +++ b/app/assets/javascripts/components/actions/notifications.jsx @@ -61,6 +61,8 @@ export function refreshNotifications() { params.since_id = ids.first().get('id'); } + params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); + api(getState).get('/api/v1/notifications', { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); @@ -105,11 +107,11 @@ export function expandNotifications() { dispatch(expandNotificationsRequest()); - api(getState).get(url, { - params: { - limit: 5 - } - }).then(response => { + const params = {}; + + params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); + + api(getState).get(url, params).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 71c054334e9..3cff299820e 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -9,7 +9,7 @@ class Api::V1::NotificationsController < ApiController DEFAULT_NOTIFICATIONS_LIMIT = 15 def index - @notifications = Notification.where(account: current_account).browserable.paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id]) + @notifications = Notification.where(account: current_account).browserable(exclude_types).paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id]) @notifications = cache_collection(@notifications, Notification) statuses = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status) @@ -32,7 +32,13 @@ class Api::V1::NotificationsController < ApiController private + def exclude_types + val = params.permit(exclude_types: [])[:exclude_types] || [] + val = [val] unless val.is_a?(Enumerable) + val + end + def pagination_params(core_params) - params.permit(:limit).merge(core_params) + params.permit(:limit, exclude_types: []).merge(core_params) end end diff --git a/app/models/notification.rb b/app/models/notification.rb index b7b474869b9..302d4382dca 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -16,10 +16,17 @@ class Notification < ApplicationRecord validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] } + TYPE_CLASS_MAP = { + mention: 'Mention', + reblog: 'Status', + follow: 'Follow', + follow_request: 'FollowRequest', + favourite: 'Favourite', + }.freeze + STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze scope :cache_ids, -> { select(:id, :updated_at, :activity_type, :activity_id) } - scope :browserable, -> { where.not(activity_type: ['FollowRequest']) } cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account @@ -28,12 +35,7 @@ class Notification < ApplicationRecord end def type - case activity_type - when 'Status' - :reblog - else - activity_type.underscore.to_sym - end + @type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym end def target_status @@ -50,6 +52,11 @@ class Notification < ApplicationRecord end class << self + def browserable(types = []) + types.concat([:follow_request]) + where.not(activity_type: activity_types_from_types(types)) + end + def reload_stale_associations!(cached_items) account_ids = cached_items.map(&:from_account_id).uniq accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h @@ -58,6 +65,12 @@ class Notification < ApplicationRecord item.from_account = accounts[item.from_account_id] end end + + private + + def activity_types_from_types(types) + types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact + end end after_initialize :set_from_account diff --git a/spec/controllers/api/v1/notifications_controller_spec.rb b/spec/controllers/api/v1/notifications_controller_spec.rb index e5f7eec73d0..c390d4f0143 100644 --- a/spec/controllers/api/v1/notifications_controller_spec.rb +++ b/spec/controllers/api/v1/notifications_controller_spec.rb @@ -5,15 +5,71 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:token) { double acceptable?: true, resource_owner_id: user.id } + let(:other) { Fabricate(:user, account: Fabricate(:account, username: 'bob')) } before do allow(controller).to receive(:doorkeeper_token) { token } end describe 'GET #index' do - it 'returns http success' do - get :index - expect(response).to have_http_status(:success) + before do + status = PostStatusService.new.call(user.account, 'Test') + @reblog = ReblogService.new.call(other.account, status) + @mention = PostStatusService.new.call(other.account, 'Hello @alice') + @favourite = FavouriteService.new.call(other.account, status) + @follow = FollowService.new.call(other.account, 'alice') + end + + describe 'with no options' do + before do + get :index + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'includes reblog' do + expect(assigns(:notifications).map(&:activity_id)).to include(@reblog.id) + end + + it 'includes mention' do + expect(assigns(:notifications).map(&:activity_id)).to include(@mention.mentions.first.id) + end + + it 'includes favourite' do + expect(assigns(:notifications).map(&:activity_id)).to include(@favourite.id) + end + + it 'includes follow' do + expect(assigns(:notifications).map(&:activity_id)).to include(@follow.id) + end + end + + describe 'with excluded mentions' do + before do + get :index, params: { exclude_types: ['mention'] } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'includes reblog' do + expect(assigns(:notifications).map(&:activity_id)).to include(@reblog.id) + end + + it 'excludes mention' do + expect(assigns(:notifications).map(&:activity_id)).to_not include(@mention.mentions.first.id) + end + + it 'includes favourite' do + expect(assigns(:notifications).map(&:activity_id)).to include(@favourite.id) + end + + it 'includes follow' do + expect(assigns(:notifications).map(&:activity_id)).to include(@follow.id) + end end end end From d439855a6d04ff04a187b4a1ded03c6ee2bc062e Mon Sep 17 00:00:00 2001 From: Ash Furrow Date: Mon, 10 Apr 2017 18:13:08 -0400 Subject: [PATCH 14/25] Adds error message to mastodon:confirm_email task. (#1476) --- lib/tasks/mastodon.rake | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 5dc7f15678f..037a133986f 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -13,10 +13,13 @@ namespace :mastodon do desc 'Manually confirms a user with associated user email address stored in USER_EMAIL environment variable.' task confirm_email: :environment do email = ENV.fetch('USER_EMAIL') - user = User.where(email: email) - user.update(confirmed_at: Time.now.utc) - - puts "User #{email} confirmed." + user = User.where(email: email).first + if user + user.update(confirmed_at: Time.now.utc) + puts "User #{email} confirmed." + else + abort "User #{email} not found." + end end namespace :media do From 3fd5385e7b038bce4da566bcd193f0fd1e2c5383 Mon Sep 17 00:00:00 2001 From: Matthias Jouan Date: Tue, 11 Apr 2017 00:35:35 +0200 Subject: [PATCH 15/25] Add username as a title for mentions (#1385) Add a title attribute on mention links for both notifications and mentions in statuses. Related to #1350 --- app/assets/javascripts/components/components/status_content.jsx | 1 + .../features/notifications/components/notification.jsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index e0cca6113c5..9cf03bb3256 100644 --- a/app/assets/javascripts/components/components/status_content.jsx +++ b/app/assets/javascripts/components/components/status_content.jsx @@ -36,6 +36,7 @@ const StatusContent = React.createClass({ if (mention) { link.addEventListener('click', this.onMentionClick.bind(this, mention), false); + link.setAttribute('title', mention.get('acct')); } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); } else if (media) { diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx index c9279b20e82..0607466d093 100644 --- a/app/assets/javascripts/components/features/notifications/components/notification.jsx +++ b/app/assets/javascripts/components/features/notifications/components/notification.jsx @@ -76,7 +76,7 @@ const Notification = React.createClass({ const account = notification.get('account'); const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - const link = ; + const link = ; switch(notification.get('type')) { case 'follow': From 3672a799d4bf9011bc665b6d8f55e58031513527 Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Mon, 10 Apr 2017 15:36:03 -0700 Subject: [PATCH 16/25] Dev Tooling fixes (eslint/editorconfig) (#1398) * Add eslint to dev dependancies so it gets installed for the repo yarn add --dev eslint babel-eslint eslint-plugin-reac project specific version of eslint, you can globally install eslint-cli if you want the global runtime, or add .bin to your path * fix eslint errors about inconsitent returns * eslint ignore the same as git ignore. allows for eslint . * Add editorconfig file so everyones editor will be setup to follow the same standards --- .editorconfig | 12 ++ .eslintignore | 30 +++ package.json | 5 + streaming/index.js | 15 +- yarn.lock | 486 +++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 529 insertions(+), 19 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintignore diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..5f8702cf892 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000000..6d540c41387 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,30 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-journal + +# Ignore all logfiles and tempfiles. +/log/* +!/log/.keep +/tmp +coverage +public/system +public/assets +.env +.env.production +node_modules/ +neo4j/ + +# Ignore Vagrant files +.vagrant/ + +# Ignore Capistrano customizations +config/deploy/* diff --git a/package.json b/package.json index 14c8abe7945..fee78dd695a 100644 --- a/package.json +++ b/package.json @@ -72,5 +72,10 @@ "webpack": "^2.2.1", "websocket.js": "^0.1.7", "ws": "^2.1.0" + }, + "devDependencies": { + "babel-eslint": "^7.2.1", + "eslint": "^3.19.0", + "eslint-plugin-react": "^6.10.3" } } diff --git a/streaming/index.js b/streaming/index.js index 7edf6203fb0..a1e7eaca772 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -87,21 +87,24 @@ const setRequestId = (req, res, next) => { const accountFromToken = (token, req, next) => { pgPool.connect((err, client, done) => { if (err) { - return next(err) + next(err) + return } client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 LIMIT 1', [token], (err, result) => { done() if (err) { - return next(err) + next(err) + return } if (result.rows.length === 0) { err = new Error('Invalid access token') err.statusCode = 401 - return next(err) + next(err) + return } req.accountId = result.rows[0].account_id @@ -113,7 +116,8 @@ const accountFromToken = (token, req, next) => { const authenticationMiddleware = (req, res, next) => { if (req.method === 'OPTIONS') { - return next() + next() + return } const authorization = req.get('Authorization') @@ -122,7 +126,8 @@ const authenticationMiddleware = (req, res, next) => { const err = new Error('Missing access token') err.statusCode = 401 - return next(err) + next(err) + return } const token = authorization.replace(/^Bearer /, '') diff --git a/yarn.lock b/yarn.lock index 6a3a36270fa..b83924ad930 100644 --- a/yarn.lock +++ b/yarn.lock @@ -140,6 +140,12 @@ acorn-globals@^3.1.0: dependencies: acorn "^4.0.4" +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + acorn@^1.0.3: version "1.2.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-1.2.2.tgz#c8ce27de0acc76d896d2b1fad3df588d9e82f014" @@ -148,7 +154,7 @@ acorn@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7" -acorn@^3.0.0: +acorn@^3.0.0, acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" @@ -156,6 +162,10 @@ acorn@^4.0.3, acorn@^4.0.4: version "4.0.11" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0" +acorn@^5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d" + airbnb-js-shims@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/airbnb-js-shims/-/airbnb-js-shims-1.0.1.tgz#7d5a7d772c8c6fdeb624ea3cef62506091b180b5" @@ -169,7 +179,7 @@ airbnb-js-shims@^1.0.1: string.prototype.padend "^3.0.0" string.prototype.padstart "^3.0.0" -ajv-keywords@^1.1.1: +ajv-keywords@^1.0.0, ajv-keywords@^1.1.1: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" @@ -196,6 +206,10 @@ amdefine@>=0.0.4: version "1.0.0" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.0.tgz#fd17474700cb5cc9c2b709f0be9d23ce3c198c33" +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -284,10 +298,27 @@ array-reduce@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" +array.prototype.find@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.0.4.tgz#556a5c5362c08648323ddaeb9de9d14bc1864c90" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + arrify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -424,7 +455,7 @@ babel-code-frame@^6.11.0: esutils "^2.0.2" js-tokens "^2.0.0" -babel-code-frame@^6.22.0: +babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" dependencies: @@ -480,6 +511,15 @@ babel-core@^6.11.4: slash "^1.0.0" source-map "^0.5.0" +babel-eslint@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.1.tgz#079422eb73ba811e3ca0865ce87af29327f8c52f" + dependencies: + babel-code-frame "^6.22.0" + babel-traverse "^6.23.1" + babel-types "^6.23.0" + babylon "^6.16.1" + babel-generator@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.22.0.tgz#d642bf4961911a8adc7c692b0c9297f325cda805" @@ -1302,6 +1342,10 @@ babylon@^6.15.0: version "6.15.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" +babylon@^6.16.1: + version "6.16.1" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3" + babylon@~5.8.3: version "5.8.38" resolved "https://registry.yarnpkg.com/babylon/-/babylon-5.8.38.tgz#ec9b120b11bf6ccd4173a18bf217e60b79859ffd" @@ -1586,6 +1630,16 @@ cached-path-relative@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7" +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + camelcase-keys@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" @@ -1639,7 +1693,7 @@ chai@^3.5.0: deep-eql "^0.1.3" type-detect "^1.0.0" -chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -1695,6 +1749,10 @@ cipher-base@^1.0.0, cipher-base@^1.0.1: dependencies: inherits "^2.0.1" +circular-json@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" + clap@^1.0.9: version "1.1.1" resolved "https://registry.yarnpkg.com/clap/-/clap-1.1.1.tgz#a8a93e0bfb7581ac199c4f001a5525a724ce696d" @@ -1705,6 +1763,16 @@ classnames@^2.1.2, classnames@^2.2.3, classnames@~2.2: version "2.2.5" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-width@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" + cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -1824,7 +1892,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.4.7, concat-stream@~1.5.0, concat-stream@~1.5.1: +concat-stream@^1.4.7, concat-stream@^1.5.2, concat-stream@~1.5.0, concat-stream@~1.5.1: version "1.5.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" dependencies: @@ -2085,6 +2153,12 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + d@^0.1.1, d@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" @@ -2140,6 +2214,18 @@ defined@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -2197,6 +2283,13 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +doctrine@^1.2.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + doctrine@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" @@ -2377,6 +2470,15 @@ es-abstract@^1.3.2, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1: is-callable "^1.1.3" is-regex "^1.0.3" +es-abstract@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.0" + is-callable "^1.1.3" + is-regex "^1.0.3" + es-to-primitive@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" @@ -2385,6 +2487,13 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.1" +es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.15" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.15.tgz#c330a5934c1ee21284a7c081a86e5fd937c91ea6" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + es5-ext@^0.10.7, es5-ext@~0.10.11, es5-ext@~0.10.2: version "0.10.12" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047" @@ -2404,10 +2513,39 @@ es6-iterator@2: es5-ext "^0.10.7" es6-symbol "3" +es6-iterator@^2.0.1, es6-iterator@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-symbol "^3.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + es6-promise@^3.2.1: version "3.3.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + es6-shim@^0.35.1: version "0.35.1" resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.1.tgz#a23524009005b031ab4a352ac196dfdfd1144ab7" @@ -2419,6 +2557,22 @@ es6-symbol@3, es6-symbol@^3.0.2, es6-symbol@~3.1: d "~0.1.1" es5-ext "~0.10.11" +es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -2438,6 +2592,72 @@ escodegen@^1.6.1: optionalDependencies: source-map "~0.2.0" +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-plugin-react@^6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-6.10.3.tgz#c5435beb06774e12c7db2f6abaddcbf900cd3f78" + dependencies: + array.prototype.find "^2.0.1" + doctrine "^1.2.2" + has "^1.0.1" + jsx-ast-utils "^1.3.4" + object.assign "^4.0.4" + +eslint@^3.19.0: + version "3.19.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" + dependencies: + babel-code-frame "^6.16.0" + chalk "^1.1.3" + concat-stream "^1.5.2" + debug "^2.1.1" + doctrine "^2.0.0" + escope "^3.6.0" + espree "^3.4.0" + esquery "^1.0.0" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + glob "^7.0.3" + globals "^9.14.0" + ignore "^3.2.0" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.7.5" + strip-bom "^3.0.0" + strip-json-comments "~2.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.1.tgz#28a83ab4aaed71ed8fe0f5efe61b76a05c13c4d2" + dependencies: + acorn "^5.0.1" + acorn-jsx "^3.0.0" + esprima@^2.6.0, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -2446,10 +2666,31 @@ esprima@~3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" +esquery@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + dependencies: + estraverse "~4.1.0" + object-assign "^4.0.1" + estraverse@^1.9.1: version "1.9.3" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" +estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +estraverse@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" + esutils@^2.0.0, esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -2458,6 +2699,13 @@ etag@~1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + events@^1.0.0, events@^1.1.1, events@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -2478,6 +2726,10 @@ exenv@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.0.tgz#3835f127abf075bfe082d0aed4484057c78e3c89" +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + expand-brackets@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" @@ -2559,6 +2811,20 @@ fbjs@^0.8.1, fbjs@^0.8.4: promise "^7.1.1" ua-parser-js "^0.7.9" +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + file-loader@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.9.0.tgz#1d2daddd424ce6d1b07cfe3f79731bed3617ab42" @@ -2604,6 +2870,15 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" +flat-cache@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + flatten@1.0.2, flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" @@ -2815,10 +3090,21 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@~7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^9.0.0: +globals@^9.0.0, globals@^9.14.0: version "9.14.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + globule@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/globule/-/globule-1.1.0.tgz#c49352e4dc183d85893ee825385eb994bb6df45f" @@ -2986,6 +3272,10 @@ ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" +ignore@^3.2.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd" + immutable@^3.7.6, immutable@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2" @@ -3037,6 +3327,24 @@ inline-source-map@~0.6.0: dependencies: source-map "~0.5.3" +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + insert-module-globals@^7.0.0: version "7.0.1" resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.1.tgz#c03bf4e01cb086d5b5e5ace8ad0afe7889d638c3" @@ -3162,13 +3470,17 @@ is-fullwidth-code-point@^1.0.0: dependencies: number-is-nan "^1.0.0" +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + is-glob@^2.0.0, is-glob@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" dependencies: is-extglob "^1.0.0" -is-my-json-valid@^2.12.4: +is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: version "2.15.0" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" dependencies: @@ -3187,6 +3499,22 @@ is-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + dependencies: + path-is-inside "^1.0.1" + is-plain-obj@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -3217,6 +3545,12 @@ is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" +is-resolvable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" + dependencies: + tryit "^1.0.1" + is-stream@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -3298,7 +3632,7 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" -js-yaml@^3.4.3, js-yaml@~3.6.1: +js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@~3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.6.1.tgz#6e5fe67d8b205ce4d22fad05b7781e8dadcc4b30" dependencies: @@ -3349,7 +3683,7 @@ json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" -json-stable-stringify@^1.0.1: +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" dependencies: @@ -3397,6 +3731,12 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.3.6" +jsx-ast-utils@^1.3.4: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.0.tgz#5afe38868f56bc8cc7aeaef0100ba8c75bd12591" + dependencies: + object-assign "^4.1.0" + keycode@^2.1.1: version "2.1.7" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.7.tgz#7b9255919f6cff562b09a064d222dca70b020f5c" @@ -3435,7 +3775,7 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" -levn@~0.3.0: +levn@^0.3.0, levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" dependencies: @@ -3634,7 +3974,7 @@ lodash.tail@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" -lodash@4.x.x, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.1: +lodash@4.x.x, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.1, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -3865,10 +4205,18 @@ ms@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + nan@^2.3.0, nan@^2.3.2, nan@~2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" @@ -4156,6 +4504,10 @@ once@~1.3.0, once@~1.3.3: dependencies: wrappy "1" +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + optimist@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -4163,7 +4515,7 @@ optimist@~0.6.0: minimist "~0.0.1" wordwrap "~0.0.2" -optionator@^0.8.1: +optionator@^0.8.1, optionator@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" dependencies: @@ -4284,6 +4636,10 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + path-platform@~0.11.15: version "0.11.15" resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" @@ -4373,6 +4729,10 @@ pkg-dir@^1.0.0: dependencies: find-up "^1.0.0" +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + podda@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/podda/-/podda-1.2.2.tgz#15b0edbd334ade145813343f5ecf9c10a71cf500" @@ -4718,6 +5078,10 @@ process@^0.11.0, process@~0.11.0: version "0.11.9" resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1" +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + promise@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf" @@ -5140,6 +5504,14 @@ readdirp@^2.0.0: readable-stream "^2.0.2" set-immediate-shim "^1.0.1" +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + recast@^0.11.5: version "0.11.22" resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.22.tgz#dedeb18fb001a2bbc6ac34475fda53dfe3d47dfa" @@ -5341,6 +5713,13 @@ require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" +require-uncached@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + requires-port@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -5349,17 +5728,28 @@ reselect@^2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/reselect/-/reselect-2.5.4.tgz#b7d23fdf00b83fa7ad0279546f8dbbbd765c7047" +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + resolve@1.1.7, resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" dependencies: align-text "^0.1.1" -rimraf@2, rimraf@~2.5.0, rimraf@~2.5.1: +rimraf@2, rimraf@^2.2.8, rimraf@~2.5.0, rimraf@~2.5.1: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" dependencies: @@ -5373,6 +5763,16 @@ ripemd160@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e" +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + samsam@1.1.2, samsam@~1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" @@ -5523,6 +5923,14 @@ shelljs@^0.7.4: interpret "^1.0.0" rechoir "^0.6.2" +shelljs@^0.7.5: + version "0.7.7" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + signal-exit@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.1.tgz#5a4c884992b63a7acd9badb7894c3ee9cfccad81" @@ -5548,6 +5956,10 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + slide@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" @@ -5692,6 +6104,13 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +string-width@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^3.0.0" + string.prototype.padend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0" @@ -5735,6 +6154,10 @@ strip-bom@^2.0.0: dependencies: is-utf8 "^0.2.0" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -5745,6 +6168,10 @@ strip-json-comments@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + style-loader@0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.13.1.tgz#468280efbc0473023cd3a6cd56e33b5a1d7fc3a9" @@ -5809,6 +6236,17 @@ syntax-error@^1.1.1: dependencies: acorn "^2.7.0" +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + tapable@^0.1.8, tapable@~0.1.8: version "0.1.10" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" @@ -5856,6 +6294,10 @@ tar@^2.0.0, tar@~2.2.0, tar@~2.2.1: fstream "^1.0.2" inherits "2" +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + through2@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.1.tgz#384e75314d49f32de12eebb8136b8eb6b5d59da9" @@ -5863,7 +6305,7 @@ through2@^2.0.0: readable-stream "~2.0.0" xtend "~4.0.0" -through@2, "through@>=2.2.7 <3": +through@2, "through@>=2.2.7 <3", through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -5909,6 +6351,10 @@ trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +tryit@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" + tty-browserify@0.0.0, tty-browserify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -6022,6 +6468,12 @@ user-home@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + utf-8-validate@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-3.0.1.tgz#5d2b8656b4ddcfded47217b647a98941b63cf213" @@ -6280,6 +6732,12 @@ write-file-atomic@^1.1.2: imurmurhash "^0.1.4" slide "^1.1.5" +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + ws@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ws/-/ws-2.1.0.tgz#b24eaed9609f8632dd51e3f7698619a90fddcc92" From b57eed4584fbaa3bf83964bda804f27495b6f1fc Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 10 Apr 2017 18:38:34 -0400 Subject: [PATCH 17/25] Remove order prior to .find_in_batches (#1470) The `Status` class has a default order on it, so when this query gets built and gets all the way to `find_in_batches` there is an order already there. When `find_in_batches` is run it discards any existing order on the query, and emits a warning to the logs if there is one there. This change removes the order prior calling `find_in_batches`, which will stop the logged warning from occurring as well. --- app/lib/feed_manager.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 58d9fb1fc28..339a5c78bde 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -66,7 +66,7 @@ class FeedManager timeline_key = key(:home, into_account.id) oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0 - from_account.statuses.select('id').where('id > ?', oldest_home_score).find_in_batches do |statuses| + from_account.statuses.select('id').where('id > ?', oldest_home_score).reorder(nil).find_in_batches do |statuses| redis.pipelined do statuses.each do |status| redis.zrem(timeline_key, status.id) From 12f72e1740cd91929419c82c6b782393e306994c Mon Sep 17 00:00:00 2001 From: Eugen Date: Tue, 11 Apr 2017 00:38:58 +0200 Subject: [PATCH 18/25] When avatar/header are GIF, generate static versions (#1428) * When avatar/header are GIF, generate static versions. Account API returns "avatar"/"avatar_static", "header"/"header_static" Static version is the same as original for other cases Web UI de-animates avatars in toots, lists of users Fix #441, fix #596, prerequisite for #1064 * Fix JS test * Add rake task to generate static avatars/headers from GIF ones, add test --- .../components/components/account.jsx | 2 +- .../components/components/avatar.jsx | 135 +++--------------- .../components/components/status.jsx | 2 +- .../components/autosuggest_account.jsx | 2 +- .../compose/components/navigation_bar.jsx | 2 +- .../compose/components/reply_indicator.jsx | 2 +- .../components/account_authorize.jsx | 2 +- .../status/components/detailed_status.jsx | 2 +- app/assets/stylesheets/components.scss | 10 +- app/models/account.rb | 32 ++++- app/views/api/v1/accounts/show.rabl | 11 +- lib/tasks/mastodon.rake | 12 ++ spec/fixtures/files/avatar.gif | Bin 0 -> 85810 bytes spec/javascript/components/avatar.test.jsx | 12 +- spec/models/account_spec.rb | 20 +++ 15 files changed, 108 insertions(+), 138 deletions(-) create mode 100644 spec/fixtures/files/avatar.gif diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx index 7a1c9f5ce01..782cf382dfb 100644 --- a/app/assets/javascripts/components/components/account.jsx +++ b/app/assets/javascripts/components/components/account.jsx @@ -65,7 +65,7 @@ const Account = React.createClass({
-
+
diff --git a/app/assets/javascripts/components/components/avatar.jsx b/app/assets/javascripts/components/components/avatar.jsx index 0237a190465..673b1a247ec 100644 --- a/app/assets/javascripts/components/components/avatar.jsx +++ b/app/assets/javascripts/components/components/avatar.jsx @@ -1,103 +1,18 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; -// From: http://stackoverflow.com/a/18320662 -const resample = (canvas, width, height, resize_canvas) => { - let width_source = canvas.width; - let height_source = canvas.height; - width = Math.round(width); - height = Math.round(height); - - let ratio_w = width_source / width; - let ratio_h = height_source / height; - let ratio_w_half = Math.ceil(ratio_w / 2); - let ratio_h_half = Math.ceil(ratio_h / 2); - - let ctx = canvas.getContext("2d"); - let img = ctx.getImageData(0, 0, width_source, height_source); - let img2 = ctx.createImageData(width, height); - let data = img.data; - let data2 = img2.data; - - for (let j = 0; j < height; j++) { - for (let i = 0; i < width; i++) { - let x2 = (i + j * width) * 4; - let weight = 0; - let weights = 0; - let weights_alpha = 0; - let gx_r = 0; - let gx_g = 0; - let gx_b = 0; - let gx_a = 0; - let center_y = (j + 0.5) * ratio_h; - let yy_start = Math.floor(j * ratio_h); - let yy_stop = Math.ceil((j + 1) * ratio_h); - - for (let yy = yy_start; yy < yy_stop; yy++) { - let dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half; - let center_x = (i + 0.5) * ratio_w; - let w0 = dy * dy; //pre-calc part of w - let xx_start = Math.floor(i * ratio_w); - let xx_stop = Math.ceil((i + 1) * ratio_w); - - for (let xx = xx_start; xx < xx_stop; xx++) { - let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half; - let w = Math.sqrt(w0 + dx * dx); - - if (w >= 1) { - // pixel too far - continue; - } - - // hermite filter - weight = 2 * w * w * w - 3 * w * w + 1; - let pos_x = 4 * (xx + yy * width_source); - - // alpha - gx_a += weight * data[pos_x + 3]; - weights_alpha += weight; - - // colors - if (data[pos_x + 3] < 255) - weight = weight * data[pos_x + 3] / 250; - - gx_r += weight * data[pos_x]; - gx_g += weight * data[pos_x + 1]; - gx_b += weight * data[pos_x + 2]; - weights += weight; - } - } - - data2[x2] = gx_r / weights; - data2[x2 + 1] = gx_g / weights; - data2[x2 + 2] = gx_b / weights; - data2[x2 + 3] = gx_a / weights_alpha; - } - } - - // clear and resize canvas - if (resize_canvas === true) { - canvas.width = width; - canvas.height = height; - } else { - ctx.clearRect(0, 0, width_source, height_source); - } - - // draw - ctx.putImageData(img2, 0, 0); -}; - const Avatar = React.createClass({ propTypes: { src: React.PropTypes.string.isRequired, + staticSrc: React.PropTypes.string, size: React.PropTypes.number.isRequired, style: React.PropTypes.object, - animated: React.PropTypes.bool + animate: React.PropTypes.bool }, getDefaultProps () { return { - animated: true + animate: false }; }, @@ -117,38 +32,30 @@ const Avatar = React.createClass({ this.setState({ hovering: false }); }, - handleLoad () { - this.canvas.width = this.image.naturalWidth; - this.canvas.height = this.image.naturalHeight; - this.canvas.getContext('2d').drawImage(this.image, 0, 0); - - resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true); - }, - - setImageRef (c) { - this.image = c; - }, - - setCanvasRef (c) { - this.canvas = c; - }, - render () { + const { src, size, staticSrc, animate } = this.props; const { hovering } = this.state; - if (this.props.animated) { - return ( -
- -
- ); + const style = { + ...this.props.style, + width: `${size}px`, + height: `${size}px`, + backgroundSize: `${size}px ${size}px` + }; + + if (hovering || animate) { + style.backgroundImage = `url(${src})`; + } else { + style.backgroundImage = `url(${staticSrc})`; } return ( -
- - -
+
); } diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx index 110d26c6d70..65db8f79bbb 100644 --- a/app/assets/javascripts/components/components/status.jsx +++ b/app/assets/javascripts/components/components/status.jsx @@ -90,7 +90,7 @@ const Status = React.createClass({
- +
diff --git a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx index 5591b45cfbb..9e05193fbde 100644 --- a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx +++ b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx @@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; const AutosuggestAccount = ({ account }) => (
-
+
); diff --git a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx index 076ac7cbbde..1a748a23c04 100644 --- a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx +++ b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx @@ -17,7 +17,7 @@ const NavigationBar = React.createClass({ render () { return (
- + diff --git a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx index 1766655c20f..9c713287c12 100644 --- a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx +++ b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx @@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
-
+
diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx index caa46ff3c54..2da57252ee2 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -54,7 +54,7 @@ const DetailedStatus = React.createClass({ return (
-
+
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 95e432cb659..8c76ddf999f 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1,7 +1,7 @@ @import 'variables'; .app-body{ - -ms-overflow-style: -ms-autohiding-scrollbar; + -ms-overflow-style: -ms-autohiding-scrollbar; } .button { @@ -165,6 +165,14 @@ } } +.avatar { + border-radius: 4px; + background: transparent no-repeat; + background-position: 50%; + background-clip: padding-box; + position: relative; +} + .lightbox .icon-button { color: $color1; } diff --git a/app/models/account.rb b/app/models/account.rb index a482fc8e6e1..8ceda7f974c 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -12,12 +12,12 @@ class Account < ApplicationRecord validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?' # Avatar upload - has_attached_file :avatar, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' } + has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-quality 80 -strip' } validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES validates_attachment_size :avatar, less_than: 2.megabytes # Header upload - has_attached_file :header, styles: { original: '700x335#' }, convert_options: { all: '-quality 80 -strip' } + has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-quality 80 -strip' } validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES validates_attachment_size :header, less_than: 2.megabytes @@ -158,6 +158,22 @@ class Account < ApplicationRecord save! end + def avatar_original_url + avatar.url(:original) + end + + def avatar_static_url + avatar_content_type == 'image/gif' ? avatar.url(:static) : avatar_original_url + end + + def header_original_url + header.url(:original) + end + + def header_static_url + header_content_type == 'image/gif' ? header.url(:static) : header_original_url + end + def avatar_remote_url=(url) parsed_url = URI.parse(url) @@ -292,6 +308,18 @@ class Account < ApplicationRecord def follow_mapping(query, field) query.pluck(field).inject({}) { |mapping, id| mapping[id] = true; mapping } end + + def avatar_styles(file) + styles = { original: '120x120#' } + styles[:static] = { format: 'png' } if file.content_type == 'image/gif' + styles + end + + def header_styles(file) + styles = { original: '700x335#' } + styles[:static] = { format: 'png' } if file.content_type == 'image/gif' + styles + end end before_create do diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl index 32df0457a75..8826aa22da4 100644 --- a/app/views/api/v1/accounts/show.rabl +++ b/app/views/api/v1/accounts/show.rabl @@ -4,8 +4,9 @@ attributes :id, :username, :acct, :display_name, :locked, :created_at 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.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 } +node(:avatar) { |account| full_asset_url(account.avatar_original_url) } +node(:avatar_static) { |account| full_asset_url(account.avatar_static_url) } +node(:header) { |account| full_asset_url(account.header_original_url) } +node(:header_static) { |account| full_asset_url(account.header_static_url) } + +attributes :followers_count, :following_count, :statuses_count diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 037a133986f..a8fb58b7fec 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -92,5 +92,17 @@ namespace :mastodon do Rails.logger.debug 'Done!' end + + desc 'Generate static versions of GIF avatars/headers' + task add_static_avatars: :environment do + Rails.logger.debug 'Generating static avatars/headers for GIF ones...' + + Account.unscoped.where(avatar_content_type: 'image/gif').or(Account.unscoped.where(header_content_type: 'image/gif')).find_each do |account| + account.avatar.reprocess! + account.header.reprocess! + end + + Rails.logger.debug 'Done!' + end end end diff --git a/spec/fixtures/files/avatar.gif b/spec/fixtures/files/avatar.gif new file mode 100644 index 0000000000000000000000000000000000000000..d929801e5cf6205cb95d03de0d70844c22c3529e GIT binary patch literal 85810 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 BB5! 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 thFjiQwvb}', () => { const src = '/path/to/image.jpg'; const size = 100; - const wrapper = render(); + const wrapper = render(); - it('renders an img element with the given src', () => { - expect(wrapper.find('img')).to.have.attr('src', `${src}`); - }); - - it('renders an img element of the given size', () => { - ['width', 'height'].map((attr) => { - expect(wrapper.find('img')).to.have.attr(attr, `${size}`); - }); + it('renders a div element with the given src as background', () => { + expect(wrapper.find('div')).to.have.style('background-image', `url(${src})`); }); it('renders a div element of the given size', () => { diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 0906bb0ae15..fb367ab7a09 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -421,4 +421,24 @@ RSpec.describe Account, type: :model do end end end + + describe 'static avatars' do + describe 'when GIF' do + it 'creates a png static style' do + subject.avatar = attachment_fixture('avatar.gif') + subject.save + + expect(subject.avatar_static_url).to_not eq subject.avatar_original_url + end + end + + describe 'when non-GIF' do + it 'does not create extra static style' do + subject.avatar = attachment_fixture('attachment.jpg') + subject.save + + expect(subject.avatar_static_url).to eq subject.avatar_original_url + end + end + end end From a28378646393de998663981e6660eb79e7a6375f Mon Sep 17 00:00:00 2001 From: Effy Elden Date: Tue, 11 Apr 2017 08:39:39 +1000 Subject: [PATCH 19/25] Add note about minimum docker-compose version (#1264) Add a note to ensure users are using the right version of docker-compose. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa944a9019e..41990ff7042 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Consult the example configuration file, `.env.production.sample` for the full li [![](https://images.microbadger.com/badges/version/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own image badge on microbadger.com") -The project now includes a `Dockerfile` and a `docker-compose.yml`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can: +The project now includes a `Dockerfile` and a `docker-compose.yml` file (which requires at least docker-compose version `1.10.0`). You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can: docker-compose build From 4ada50985a73ed5c859cdf5160500b04116ad0e4 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 10 Apr 2017 19:11:41 -0400 Subject: [PATCH 20/25] Pagination improvements (#1445) * Replace will_paginate with kaminari * Use #page instead of #paginate in controllers * Replace will_paginate.page_gap with pagination.truncate in i18n * Customize kaminari views to match prior styles * Set kaminari options to match prior behavior * Replace will_paginate with paginate in views --- Gemfile | 2 +- Gemfile.lock | 15 +++++++++++++-- app/assets/stylesheets/accounts.scss | 12 ++++++------ app/controllers/accounts_controller.rb | 4 ++-- app/controllers/admin/accounts_controller.rb | 2 +- .../admin/domain_blocks_controller.rb | 2 +- app/controllers/admin/pubsubhubbub_controller.rb | 2 +- app/controllers/admin/reports_controller.rb | 2 +- app/helpers/accounts_helper.rb | 12 ------------ app/views/accounts/followers.html.haml | 2 +- app/views/accounts/following.html.haml | 2 +- app/views/accounts/show.html.haml | 2 +- app/views/admin/accounts/index.html.haml | 2 +- app/views/admin/domain_blocks/index.html.haml | 2 +- app/views/admin/pubsubhubbub/index.html.haml | 2 +- app/views/admin/reports/index.html.haml | 2 +- app/views/kaminari/_next_page.html.haml | 9 +++++++++ app/views/kaminari/_paginator.html.haml | 16 ++++++++++++++++ app/views/kaminari/_prev_page.html.haml | 9 +++++++++ app/views/tags/show.html.haml | 2 +- config/i18n-tasks.yml | 2 +- config/initializers/kaminari_config.rb | 7 +++++++ config/initializers/pagination.rb | 0 config/locales/de.yml | 2 -- config/locales/en.yml | 3 +-- config/locales/eo.yml | 2 -- config/locales/es.yml | 2 -- config/locales/fi.yml | 2 -- config/locales/fr.yml | 2 -- config/locales/hu.yml | 2 -- config/locales/no.yml | 2 -- config/locales/pt.yml | 2 -- config/locales/ru.yml | 2 -- config/locales/uk.yml | 2 -- config/locales/zh-CN.yml | 2 -- spec/helpers/accounts_helper_spec.rb | 5 ----- 36 files changed, 77 insertions(+), 65 deletions(-) delete mode 100644 app/helpers/accounts_helper.rb create mode 100644 app/views/kaminari/_next_page.html.haml create mode 100644 app/views/kaminari/_paginator.html.haml create mode 100644 app/views/kaminari/_prev_page.html.haml create mode 100644 config/initializers/kaminari_config.rb create mode 100644 config/initializers/pagination.rb delete mode 100644 spec/helpers/accounts_helper_spec.rb diff --git a/Gemfile b/Gemfile index 078ac5806d2..9a1792623b5 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ gem 'htmlentities' gem 'http' gem 'http_accept_language' gem 'httplog' +gem 'kaminari' gem 'link_header' gem 'nokogiri' gem 'oj' @@ -52,7 +53,6 @@ gem 'simple_form' gem 'statsd-instrument' gem 'twitter-text' gem 'tzinfo-data' -gem 'will_paginate' gem 'react-rails' gem 'browserify-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 4fe8aa07600..f1bc9880ecb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -203,6 +203,18 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (2.0.3) + kaminari (1.0.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.0.1) + kaminari-activerecord (= 1.0.1) + kaminari-core (= 1.0.1) + kaminari-actionview (1.0.1) + actionview + kaminari-core (= 1.0.1) + kaminari-activerecord (1.0.1) + activerecord + kaminari-core (= 1.0.1) + kaminari-core (1.0.1) launchy (2.4.3) addressable (~> 2.3) letter_opener (1.4.1) @@ -433,7 +445,6 @@ GEM websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) - will_paginate (3.1.5) PLATFORMS ruby @@ -472,6 +483,7 @@ DEPENDENCIES httplog i18n-tasks (~> 0.9.6) jquery-rails + kaminari letter_opener letter_opener_web link_header @@ -513,7 +525,6 @@ DEPENDENCIES tzinfo-data uglifier (>= 1.3.0) webmock - will_paginate RUBY VERSION ruby 2.4.1p111 diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss index b3ae33500ed..2a05c2bb403 100644 --- a/app/assets/stylesheets/accounts.scss +++ b/app/assets/stylesheets/accounts.scss @@ -173,7 +173,7 @@ text-align: center; overflow: hidden; - a, .current, .next_page, .previous_page, .gap { + a, .current, .page, .gap { font-size: 14px; color: $color5; font-weight: 500; @@ -193,12 +193,12 @@ cursor: default; } - .previous_page, .next_page { + .prev, .next { text-transform: uppercase; color: $color2; } - .previous_page { + .prev { float: left; padding-left: 0; @@ -208,7 +208,7 @@ } } - .next_page { + .next { float: right; padding-right: 0; @@ -226,11 +226,11 @@ @media screen and (max-width: 360px) { padding: 30px 20px; - a, .current, .next_page, .previous_page, .gap { + a, .current, .next, .prev, .gap { display: none; } - .next_page, .previous_page { + .next, .prev { display: inline-block; } } diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 34103de0e8f..d4f157614d7 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -35,11 +35,11 @@ class AccountsController < ApplicationController end def followers - @followers = @account.followers.order('follows.created_at desc').paginate(page: params[:page], per_page: 12) + @followers = @account.followers.order('follows.created_at desc').page(params[:page]).per(12) end def following - @following = @account.following.order('follows.created_at desc').paginate(page: params[:page], per_page: 12) + @following = @account.following.order('follows.created_at desc').page(params[:page]).per(12) end private diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 60b631ece49..71cb8edd876 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -5,7 +5,7 @@ module Admin before_action :set_account, except: :index def index - @accounts = Account.alphabetic.paginate(page: params[:page], per_page: 40) + @accounts = Account.alphabetic.page(params[:page]) @accounts = @accounts.local if params[:local].present? @accounts = @accounts.remote if params[:remote].present? diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 58f1efa5b4b..a8b56c0859b 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -3,7 +3,7 @@ module Admin class DomainBlocksController < BaseController def index - @blocks = DomainBlock.paginate(page: params[:page], per_page: 40) + @blocks = DomainBlock.page(params[:page]) end def new diff --git a/app/controllers/admin/pubsubhubbub_controller.rb b/app/controllers/admin/pubsubhubbub_controller.rb index 95f79c52096..31c80a174fb 100644 --- a/app/controllers/admin/pubsubhubbub_controller.rb +++ b/app/controllers/admin/pubsubhubbub_controller.rb @@ -3,7 +3,7 @@ module Admin class PubsubhubbubController < BaseController def index - @subscriptions = Subscription.order('id desc').includes(:account).paginate(page: params[:page], per_page: 40) + @subscriptions = Subscription.order('id desc').includes(:account).page(params[:page]) end end end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 5a37d8e6e9d..3c3082318e1 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -5,7 +5,7 @@ module Admin before_action :set_report, except: [:index] def index - @reports = Report.includes(:account, :target_account).order('id desc').paginate(page: params[:page], per_page: 40) + @reports = Report.includes(:account, :target_account).order('id desc').page(params[:page]) @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb deleted file mode 100644 index af23a78d171..00000000000 --- a/app/helpers/accounts_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module AccountsHelper - def pagination_options - { - previous_label: safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '), - next_label: safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), - inner_window: 1, - outer_window: 0, - } - end -end diff --git a/app/views/accounts/followers.html.haml b/app/views/accounts/followers.html.haml index 49349102017..fa5071f383b 100644 --- a/app/views/accounts/followers.html.haml +++ b/app/views/accounts/followers.html.haml @@ -9,4 +9,4 @@ - else = render partial: 'grid_card', collection: @followers, as: :account, cached: true -= will_paginate @followers, pagination_options += paginate @followers diff --git a/app/views/accounts/following.html.haml b/app/views/accounts/following.html.haml index 370cd6c4834..987dcba1fad 100644 --- a/app/views/accounts/following.html.haml +++ b/app/views/accounts/following.html.haml @@ -9,4 +9,4 @@ - else = render partial: 'grid_card', collection: @following, as: :account, cached: true -= will_paginate @following, pagination_options += paginate @following diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index e90897729fd..3b0d69dcdfd 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -31,4 +31,4 @@ .pagination - if @statuses.size == 20 - = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next_page', rel: 'next' + = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next', rel: 'next' diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index f8ed4ef97c6..4d636601e84 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -46,4 +46,4 @@ = table_link_to 'globe', 'Public', TagManager.instance.url_for(account) = table_link_to 'pencil', 'Edit', admin_account_path(account.id) -= will_paginate @accounts, pagination_options += paginate @accounts diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml index eb7894b86cc..fe6ff683ffb 100644 --- a/app/views/admin/domain_blocks/index.html.haml +++ b/app/views/admin/domain_blocks/index.html.haml @@ -13,5 +13,5 @@ %samp= block.domain %td= block.severity -= will_paginate @blocks, pagination_options += paginate @blocks = link_to 'Add new', new_admin_domain_block_path, class: 'button' diff --git a/app/views/admin/pubsubhubbub/index.html.haml b/app/views/admin/pubsubhubbub/index.html.haml index cb11a502c9e..2b8e36e6a36 100644 --- a/app/views/admin/pubsubhubbub/index.html.haml +++ b/app/views/admin/pubsubhubbub/index.html.haml @@ -26,4 +26,4 @@ - else = l subscription.last_successful_delivery_at -= will_paginate @subscriptions, pagination_options += paginate @subscriptions diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 839259dc27a..9c5c7893564 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -29,4 +29,4 @@ %td= truncate(report.comment, length: 30, separator: ' ') %td= table_link_to 'circle', 'View', admin_report_path(report) -= will_paginate @reports, pagination_options += paginate @reports diff --git a/app/views/kaminari/_next_page.html.haml b/app/views/kaminari/_next_page.html.haml new file mode 100644 index 00000000000..30a3643d64e --- /dev/null +++ b/app/views/kaminari/_next_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Next" page +-# available local variables +-# url: url to the next page +-# current_page: a page object for the currently displayed page +-# total_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.next + = link_to_unless current_page.last?, safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), url, rel: 'next', remote: remote diff --git a/app/views/kaminari/_paginator.html.haml b/app/views/kaminari/_paginator.html.haml new file mode 100644 index 00000000000..b1da236d5d0 --- /dev/null +++ b/app/views/kaminari/_paginator.html.haml @@ -0,0 +1,16 @@ +-# The container tag +-# available local variables +-# current_page: a page object for the currently displayed page +-# total_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +-# paginator: the paginator that renders the pagination tags inside += paginator.render do + %nav.pagination + = prev_page_tag unless current_page.first? + - each_page do |page| + - if page.display_tag? + = page_tag page + - elsif !page.was_truncated? + = gap_tag + = next_page_tag unless current_page.last? diff --git a/app/views/kaminari/_prev_page.html.haml b/app/views/kaminari/_prev_page.html.haml new file mode 100644 index 00000000000..1089e356688 --- /dev/null +++ b/app/views/kaminari/_prev_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Previous" page +-# available local variables +-# url: url to the previous page +-# current_page: a page object for the currently displayed page +-# total_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.prev + = link_to_unless current_page.first?, safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '), url, rel: 'prev', remote: remote diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml index 32a50e15862..c894cdb2e2b 100644 --- a/app/views/tags/show.html.haml +++ b/app/views/tags/show.html.haml @@ -15,4 +15,4 @@ - if @statuses.size == 20 .pagination - = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next_page', rel: 'next' + = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next', rel: 'next' diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 4304bbd1807..7ae143f9304 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -33,7 +33,7 @@ search: ignore_unused: - 'activerecord.attributes.*' - - '{devise,will_paginate,doorkeeper}.*' + - '{devise,pagination,doorkeeper}.*' - '{datetime,time}.*' - 'simple_form.{yes,no}' - 'simple_form.{placeholders,hints,labels}.*' diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb new file mode 100644 index 00000000000..bd455f3827a --- /dev/null +++ b/config/initializers/kaminari_config.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +Kaminari.configure do |config| + config.default_per_page = 40 + config.window = 1 + config.left = 3 + config.right = 1 +end diff --git a/config/initializers/pagination.rb b/config/initializers/pagination.rb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/config/locales/de.yml b/config/locales/de.yml index ed54bb6991e..75ac4e1bba9 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -88,5 +88,3 @@ de: default: "%d.%m.%Y %H:%M" users: invalid_email: Inkorrekte E-mail-Addresse - will_paginate: - page_gap: "…" diff --git a/config/locales/en.yml b/config/locales/en.yml index 118798ba16c..6c4738991e5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -126,6 +126,7 @@ en: pagination: next: Next prev: Prev + truncate: "…" remote_follow: acct: Enter your username@domain you want to follow from missing_resource: Could not find the required redirect URL for your account @@ -169,5 +170,3 @@ en: users: invalid_email: The e-mail address is invalid invalid_otp_token: Invalid two-factor code - will_paginate: - page_gap: "…" diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 3644b37bb72..e82e42495d0 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -160,5 +160,3 @@ eo: users: invalid_email: La retpoŝt-adreso ne estas valida invalid_otp_token: La dufaktora aŭtentigila kodo ne estas valida - will_paginate: - page_gap: "…" diff --git a/config/locales/es.yml b/config/locales/es.yml index 19f2c71b8aa..42245d67519 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -51,5 +51,3 @@ es: settings: edit_profile: Editar perfil preferences: Preferencias - will_paginate: - page_gap: "…" diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 56aa9df497c..c1123722602 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -160,5 +160,3 @@ fi: users: invalid_email: Virheellinen sähköposti invalid_otp_token: Virheellinen kaksivaihe tunnistus koodi - will_paginate: - page_gap: "…" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9a9c1b6dedd..92cf4394424 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -167,5 +167,3 @@ fr: users: invalid_email: L'adresse courriel est invalide invalid_otp_token: Le code d'authentification à deux facteurs est invalide - will_paginate: - page_gap: "…" diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 915d02c198b..96b73d43c63 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -51,5 +51,3 @@ hu: settings: edit_profile: Profil szerkesztése preferences: Beállítások - will_paginate: - page_gap: "…" diff --git a/config/locales/no.yml b/config/locales/no.yml index b9a752d5af8..9aa966d2af8 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -160,5 +160,3 @@ users: invalid_email: E-post addressen er ugyldig invalid_otp_token: Ugyldig two-faktor kode - will_paginate: - page_gap: "…" diff --git a/config/locales/pt.yml b/config/locales/pt.yml index ad7d05e3b1e..f2c7458f7f2 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -51,5 +51,3 @@ pt: settings: edit_profile: Editar perfil preferences: Preferências - will_paginate: - page_gap: "…" diff --git a/config/locales/ru.yml b/config/locales/ru.yml index e5a9c0958c0..fab178629e9 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -161,5 +161,3 @@ ru: users: invalid_email: Введенный e-mail неверен invalid_otp_token: Введен неверный код - will_paginate: - page_gap: "…" diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 27e8135df6b..f7176e86de2 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -51,5 +51,3 @@ uk: settings: edit_profile: Редагувати профіль preferences: Налаштування - will_paginate: - page_gap: "…" diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 78c4d46e2c2..48028d00cdc 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -150,5 +150,3 @@ zh-CN: users: invalid_email: 无效的邮箱 invalid_otp_token: 无效的两步验证码 - will_paginate: - page_gap: "…" diff --git a/spec/helpers/accounts_helper_spec.rb b/spec/helpers/accounts_helper_spec.rb deleted file mode 100644 index 3aea1f909de..00000000000 --- a/spec/helpers/accounts_helper_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe AccountsHelper, type: :helper do - -end From 11898a6461c7a2261255b5a8f82ecb4181ae509d Mon Sep 17 00:00:00 2001 From: David Libeau Date: Tue, 11 Apr 2017 05:30:40 +0200 Subject: [PATCH 21/25] Add Mastodon.tools (#1457) --- docs/Using-Mastodon/Apps.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Using-Mastodon/Apps.md b/docs/Using-Mastodon/Apps.md index b5e1fa36ba2..ce3f2f1fca6 100644 --- a/docs/Using-Mastodon/Apps.md +++ b/docs/Using-Mastodon/Apps.md @@ -14,5 +14,6 @@ Some people have started working on apps for the Mastodon API. Here is a list of |Tooter|Chrome||[@effy@mastodon.social](https://mastodon.social/users/effy)| |tootstream|CLI||[@Raccoon@mastodon.social](https://mastodon.social/users/Raccoon)| |HackerNewsBot|CLI||[@rayalez@hackertribe.io](https://hackertribe.io/users/rayalez)| +|Mastodon.tools|Wordpress, web browser, social network||[@David@mastodon.xyz](https://mastodon.xyz/users/David)| If you have a project like this, let me know so I can add it to the list! From c9b92259515a3eb16a9709c3c1e505f5fe3a33bc Mon Sep 17 00:00:00 2001 From: Corey Dutson Date: Tue, 11 Apr 2017 08:20:18 -0400 Subject: [PATCH 22/25] Adjust css for user detail page (#1463) - details a background for contrast - add 5px padding to the top of the `details-counters` children to line them up with the bio to the right (Which has a 5px padding on the top) --- app/assets/stylesheets/accounts.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss index 2a05c2bb403..50181d86eba 100644 --- a/app/assets/stylesheets/accounts.scss +++ b/app/assets/stylesheets/accounts.scss @@ -72,6 +72,7 @@ position: relative; z-index: 2; flex-direction: row; + background: rgba(0,0,0,0.5); } .details-counters { @@ -83,7 +84,7 @@ .counter { width: 80px; color: $color3; - padding: 0 10px; + padding: 5px 10px 0px; margin-bottom: 10px; border-right: 1px solid $color3; cursor: default; From a85d4473aa2a6a619fcee851c642dca576e622f6 Mon Sep 17 00:00:00 2001 From: Yann GUERN Date: Tue, 11 Apr 2017 14:21:15 +0200 Subject: [PATCH 23/25] Avoid user enumeration with devise paranoid mode (#1527) --- config/initializers/devise.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index ede6640bb88..3c23e7b2e87 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -74,7 +74,8 @@ Devise.setup do |config| # It will change confirmation, password recovery and other workflows # to behave the same regardless if the e-mail provided was right or wrong. # Does not affect registerable. - # config.paranoid = true + # See : https://github.com/plataformatec/devise/wiki/How-To:-Using-paranoid-mode,-avoid-user-enumeration-on-registerable + config.paranoid = true # By default Devise will store the user in session. You can skip storage for # particular strategies by setting this option. From c35bda05511dd9a1397113852ae93b5ed7942635 Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Tue, 11 Apr 2017 06:06:07 -0700 Subject: [PATCH 24/25] fix(*): ruby version was updated in .ruby-version but not Vagrant. Make them match (#1502) --- Vagrantfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index cd7f74473a8..90f60464074 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -46,12 +46,12 @@ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build export PATH="$HOME/.rbenv/bin::$PATH" eval "$(rbenv init -)" -echo "Compiling Ruby 2.3.1: warning, this takes a while!!!" -rbenv install 2.3.1 -rbenv global 2.3.1 - cd /vagrant +echo "Compiling Ruby $(cat .ruby-version): warning, this takes a while!!!" +rbenv install $(cat .ruby-version) +rbenv global $(cat .ruby-version) + # Configure database sudo -u postgres createuser -U postgres vagrant -s sudo -u postgres createdb -U postgres mastodon_development From b723ee73fc7d74fd5908eb09a8f6b98e73597c2b Mon Sep 17 00:00:00 2001 From: Valentin Ouvrard Date: Wed, 12 Apr 2017 01:04:56 +1100 Subject: [PATCH 25/25] Add (commented) volume in docker-compose && Mitigating the HTTPoxy Vulnerability (#1253) * enable commented volume in docker-compose.yml * Disable unworking Nginx root directory && Mitigating the HTTPoxy Vulnerability * add my instance to the list * enable GZIP on nginx.conf * readd root /home/mastodon/live/public; --- docker-compose.yml | 11 +++++++++++ docs/Running-Mastodon/Production-guide.md | 14 ++++++++++++-- docs/Using-Mastodon/List-of-Mastodon-instances.md | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d6ba66ddeb4..910bf8cfe3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,20 @@ version: '2' services: + db: restart: always image: postgres:alpine +### Uncomment to enable DB persistance +# volumes: +# - ./postgres:/var/lib/postgresql/data + redis: restart: always image: redis:alpine +### Uncomment to enable REDIS persistance +# volumes: +# - ./redis:/data + web: restart: always build: . @@ -19,6 +28,7 @@ services: volumes: - ./public/assets:/mastodon/public/assets - ./public/system:/mastodon/public/system + streaming: restart: always build: . @@ -29,6 +39,7 @@ services: depends_on: - db - redis + sidekiq: restart: always build: . diff --git a/docs/Running-Mastodon/Production-guide.md b/docs/Running-Mastodon/Production-guide.md index ec67a452c17..49f3e59b216 100644 --- a/docs/Running-Mastodon/Production-guide.md +++ b/docs/Running-Mastodon/Production-guide.md @@ -34,10 +34,19 @@ server { keepalive_timeout 70; sendfile on; client_max_body_size 0; - gzip off; root /home/mastodon/live/public; + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; location / { @@ -49,7 +58,7 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; - + proxy_set_header Proxy ""; proxy_pass_header Server; proxy_pass http://localhost:3000; @@ -67,6 +76,7 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; + proxy_set_header Proxy ""; proxy_pass http://localhost:4000; proxy_buffering off; diff --git a/docs/Using-Mastodon/List-of-Mastodon-instances.md b/docs/Using-Mastodon/List-of-Mastodon-instances.md index db35edb1a3b..49b2c201298 100644 --- a/docs/Using-Mastodon/List-of-Mastodon-instances.md +++ b/docs/Using-Mastodon/List-of-Mastodon-instances.md @@ -76,7 +76,7 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz) | [mastodon.fun](https://mastodon.fun/)|Mastodon for everyone ! |Yes|Yes| | [oulipo.social](https://oulipo.social/)|An Oulipo Mastodon in which that fifth symbol in Latin script is taboo|Yes|No| | [indigo.zone](https://indigo.zone)|Open Registrations, General Purpose|Yes|No| +| [mastodon.cloud](https://mastodon.cloud)|An open Mastodon instance with people from all around the world|Yes|Yes| | [mst3k.interlinked.me](https://mst3k.interlinked.me)|Open registrations, general purpose|Yes|Yes| - We are no longer maintaining this list as instances are popping up too quickly for using GitHub to be a tenable system for tracking them. Please standby while we work on another solution