diff options
Diffstat (limited to 'app')
166 files changed, 1365 insertions, 732 deletions
diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb index 414a875d0..7f69a3363 100644 --- a/app/controllers/admin/account_moderation_notes_controller.rb +++ b/app/controllers/admin/account_moderation_notes_controller.rb @@ -1,31 +1,41 @@ # frozen_string_literal: true -class Admin::AccountModerationNotesController < Admin::BaseController - def create - @account_moderation_note = current_account.account_moderation_notes.new(resource_params) - if @account_moderation_note.save - @target_account = @account_moderation_note.target_account - redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.created_msg') - else - @account = @account_moderation_note.target_account - @moderation_notes = @account.targeted_moderation_notes.latest - render template: 'admin/accounts/show' +module Admin + class AccountModerationNotesController < BaseController + before_action :set_account_moderation_note, only: [:destroy] + + def create + authorize AccountModerationNote, :create? + + @account_moderation_note = current_account.account_moderation_notes.new(resource_params) + + if @account_moderation_note.save + redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg') + else + @account = @account_moderation_note.target_account + @moderation_notes = @account.targeted_moderation_notes.latest + + render template: 'admin/accounts/show' + end end - end - def destroy - @account_moderation_note = AccountModerationNote.find(params[:id]) - @target_account = @account_moderation_note.target_account - @account_moderation_note.destroy - redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg') - end + def destroy + authorize @account_moderation_note, :destroy? + @account_moderation_note.destroy + redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg') + end - private + private - def resource_params - params.require(:account_moderation_note).permit( - :content, - :target_account_id - ) + def resource_params + params.require(:account_moderation_note).permit( + :content, + :target_account_id + ) + end + + def set_account_moderation_note + @account_moderation_note = AccountModerationNote.find(params[:id]) + end end end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index ffa4dc850..0829bc769 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -2,29 +2,54 @@ module Admin class AccountsController < BaseController - before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload] + before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize] before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] + before_action :require_local_account!, only: [:enable, :disable, :memorialize] def index + authorize :account, :index? @accounts = filtered_accounts.page(params[:page]) end def show + authorize @account, :show? @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account) @moderation_notes = @account.targeted_moderation_notes.latest end def subscribe + authorize @account, :subscribe? Pubsubhubbub::SubscribeWorker.perform_async(@account.id) redirect_to admin_account_path(@account.id) end def unsubscribe + authorize @account, :unsubscribe? Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id) redirect_to admin_account_path(@account.id) end + def memorialize + authorize @account, :memorialize? + @account.memorialize! + redirect_to admin_account_path(@account.id) + end + + def enable + authorize @account.user, :enable? + @account.user.enable! + redirect_to admin_account_path(@account.id) + end + + def disable + authorize @account.user, :disable? + @account.user.disable! + redirect_to admin_account_path(@account.id) + end + def redownload + authorize @account, :redownload? + @account.reset_avatar! @account.reset_header! @account.save! @@ -42,6 +67,10 @@ module Admin redirect_to admin_account_path(@account.id) if @account.local? end + def require_local_account! + redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present? + end + def filtered_accounts AccountFilter.new(filter_params).results end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 11fe326bc..db4839a8f 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -2,7 +2,9 @@ module Admin class BaseController < ApplicationController - before_action :require_admin! + include Authorization + + before_action :require_staff! layout 'admin' end diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 2542e21ee..c10b0ebee 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -2,15 +2,18 @@ module Admin class ConfirmationsController < BaseController + before_action :set_user + def create - account_user.confirm + authorize @user, :confirm? + @user.confirm! redirect_to admin_accounts_path end private - def account_user - Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) + def set_user + @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) end end end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index cbd7abe95..693d28b1f 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -5,14 +5,18 @@ module Admin before_action :set_custom_emoji, except: [:index, :new, :create] def index - @custom_emojis = filtered_custom_emojis.page(params[:page]) + authorize :custom_emoji, :index? + @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]) end def new + authorize :custom_emoji, :create? @custom_emoji = CustomEmoji.new end def create + authorize :custom_emoji, :create? + @custom_emoji = CustomEmoji.new(resource_params) if @custom_emoji.save @@ -23,6 +27,8 @@ module Admin end def update + authorize @custom_emoji, :update? + if @custom_emoji.update(resource_params) redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg') else @@ -31,14 +37,17 @@ module Admin end def destroy + authorize @custom_emoji, :destroy? @custom_emoji.destroy redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg') end def copy - emoji = CustomEmoji.new(domain: nil, shortcode: @custom_emoji.shortcode, image: @custom_emoji.image) + authorize @custom_emoji, :copy? + + emoji = CustomEmoji.find_or_create_by(domain: nil, shortcode: @custom_emoji.shortcode) - if emoji.save + if emoji.update(image: @custom_emoji.image) flash[:notice] = I18n.t('admin.custom_emojis.copied_msg') else flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg') @@ -48,11 +57,13 @@ module Admin end def enable + authorize @custom_emoji, :enable? @custom_emoji.update!(disabled: false) redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg') end def disable + authorize @custom_emoji, :disable? @custom_emoji.update!(disabled: true) redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg') end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 1ab620e03..e383dc831 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -5,14 +5,18 @@ module Admin before_action :set_domain_block, only: [:show, :destroy] def index + authorize :domain_block, :index? @domain_blocks = DomainBlock.page(params[:page]) end def new + authorize :domain_block, :create? @domain_block = DomainBlock.new end def create + authorize :domain_block, :create? + @domain_block = DomainBlock.new(resource_params) if @domain_block.save @@ -23,9 +27,12 @@ module Admin end end - def show; end + def show + authorize @domain_block, :show? + end def destroy + authorize @domain_block, :destroy? UnblockDomainService.new.call(@domain_block, retroactive_unblock?) redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg') end diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index 09275d5dc..01058bf46 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -5,14 +5,18 @@ module Admin before_action :set_email_domain_block, only: [:show, :destroy] def index + authorize :email_domain_block, :index? @email_domain_blocks = EmailDomainBlock.page(params[:page]) end def new + authorize :email_domain_block, :create? @email_domain_block = EmailDomainBlock.new end def create + authorize :email_domain_block, :create? + @email_domain_block = EmailDomainBlock.new(resource_params) if @email_domain_block.save @@ -23,6 +27,7 @@ module Admin end def destroy + authorize @email_domain_block, :destroy? @email_domain_block.destroy redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg') end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 22f02e5d0..8ed0ea421 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -3,10 +3,12 @@ module Admin class InstancesController < BaseController def index + authorize :instance, :index? @instances = ordered_instances end def resubscribe + authorize :instance, :resubscribe? params.require(:by_domain) Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id)) redirect_to admin_instances_path diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb index 5a31adecf..4f66ce708 100644 --- a/app/controllers/admin/reported_statuses_controller.rb +++ b/app/controllers/admin/reported_statuses_controller.rb @@ -2,19 +2,20 @@ module Admin class ReportedStatusesController < BaseController - include Authorization - before_action :set_report before_action :set_status, only: [:update, :destroy] def create - @form = Form::StatusBatch.new(form_status_batch_params) - flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save + authorize :status, :update? + + @form = Form::StatusBatch.new(form_status_batch_params) + flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_report_path(@report) end def update + authorize @status, :update? @status.update(status_params) redirect_to admin_report_path(@report) end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 226467739..745757ee8 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -5,14 +5,17 @@ module Admin before_action :set_report, except: [:index] def index + authorize :report, :index? @reports = filtered_reports.page(params[:page]) end def show + authorize @report, :show? @form = Form::StatusBatch.new end def update + authorize @report, :update? process_report redirect_to admin_report_path(@report) end diff --git a/app/controllers/admin/resets_controller.rb b/app/controllers/admin/resets_controller.rb index 6db648403..00b590bf6 100644 --- a/app/controllers/admin/resets_controller.rb +++ b/app/controllers/admin/resets_controller.rb @@ -2,17 +2,18 @@ module Admin class ResetsController < BaseController - before_action :set_account + before_action :set_user def create - @account.user.send_reset_password_instructions + authorize @user, :reset_password? + @user.send_reset_password_instructions redirect_to admin_accounts_path end private - def set_account - @account = Account.find(params[:account_id]) + def set_user + @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) end end end diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb new file mode 100644 index 000000000..8f8685827 --- /dev/null +++ b/app/controllers/admin/roles_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Admin + class RolesController < BaseController + before_action :set_user + + def promote + authorize @user, :promote? + @user.promote! + redirect_to admin_account_path(@user.account_id) + end + + def demote + authorize @user, :demote? + @user.demote! + redirect_to admin_account_path(@user.account_id) + end + + private + + def set_user + @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) + end + end +end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index a2f86b8a9..e81290228 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -28,10 +28,13 @@ module Admin ).freeze def edit + authorize :settings, :show? @admin_settings = Form::AdminSettings.new end def update + authorize :settings, :update? + settings_params.each do |key, value| if UPLOAD_SETTINGS.include?(key) upload = SiteUpload.where(var: key).first_or_initialize(var: key) diff --git a/app/controllers/admin/silences_controller.rb b/app/controllers/admin/silences_controller.rb index 81a3008b9..01fb292de 100644 --- a/app/controllers/admin/silences_controller.rb +++ b/app/controllers/admin/silences_controller.rb @@ -5,11 +5,13 @@ module Admin before_action :set_account def create + authorize @account, :silence? @account.update(silenced: true) redirect_to admin_accounts_path end def destroy + authorize @account, :unsilence? @account.update(silenced: false) redirect_to admin_accounts_path end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index b05000b16..b54a9b824 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -2,8 +2,6 @@ module Admin class StatusesController < BaseController - include Authorization - helper_method :current_params before_action :set_account @@ -12,24 +10,30 @@ module Admin PER_PAGE = 20 def index + authorize :status, :index? + @statuses = @account.statuses + if params[:media] account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct @statuses.merge!(Status.where(id: account_media_status_ids)) end - @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE) - @form = Form::StatusBatch.new + @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE) + @form = Form::StatusBatch.new end def create - @form = Form::StatusBatch.new(form_status_batch_params) - flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save + authorize :status, :update? + + @form = Form::StatusBatch.new(form_status_batch_params) + flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_account_statuses_path(@account.id, current_params) end def update + authorize @status, :update? @status.update(status_params) redirect_to admin_account_statuses_path(@account.id, current_params) end @@ -60,6 +64,7 @@ module Admin def current_params page = (params[:page] || 1).to_i + { media: params[:media], page: page > 1 && page, diff --git a/app/controllers/admin/subscriptions_controller.rb b/app/controllers/admin/subscriptions_controller.rb index 624a475a3..40500ef43 100644 --- a/app/controllers/admin/subscriptions_controller.rb +++ b/app/controllers/admin/subscriptions_controller.rb @@ -3,6 +3,7 @@ module Admin class SubscriptionsController < BaseController def index + authorize :subscription, :index? @subscriptions = ordered_subscriptions.page(requested_page) end diff --git a/app/controllers/admin/suspensions_controller.rb b/app/controllers/admin/suspensions_controller.rb index 5d9048d94..778feea5e 100644 --- a/app/controllers/admin/suspensions_controller.rb +++ b/app/controllers/admin/suspensions_controller.rb @@ -5,12 +5,14 @@ module Admin before_action :set_account def create + authorize @account, :suspend? Admin::SuspensionWorker.perform_async(@account.id) redirect_to admin_accounts_path end def destroy - @account.update(suspended: false) + authorize @account, :unsuspend? + @account.unsuspend! redirect_to admin_accounts_path end diff --git a/app/controllers/admin/two_factor_authentications_controller.rb b/app/controllers/admin/two_factor_authentications_controller.rb index 69c08f605..5a45d25cd 100644 --- a/app/controllers/admin/two_factor_authentications_controller.rb +++ b/app/controllers/admin/two_factor_authentications_controller.rb @@ -5,6 +5,7 @@ module Admin before_action :set_user def destroy + authorize @user, :disable_2fa? @user.disable_two_factor! redirect_to admin_accounts_path end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 9592cd4bd..22828217d 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -19,7 +19,7 @@ class Api::V1::ReportsController < Api::BaseController comment: report_params[:comment] ) - User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later } + User.staff.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later } render json: @report, serializer: REST::ReportSerializer end diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index e183a71d7..d1b4e0402 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::V1::SearchController < Api::BaseController + include Authorization + RESULTS_LIMIT = 10 before_action -> { doorkeeper_authorize! :read } @@ -9,12 +11,24 @@ class Api::V1::SearchController < Api::BaseController respond_to :json def index - @search = Search.new(search_results) + @search = Search.new(search) render json: @search, serializer: REST::SearchSerializer end private + def search + search_results.tap do |search| + search[:statuses].keep_if do |status| + begin + authorize status, :show? + rescue Mastodon::NotPermittedError + false + end + end + end + end + def search_results SearchService.new.call( params[:q], diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d5eca6ffb..f41a7f9be 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -18,6 +18,7 @@ class ApplicationController < ActionController::Base rescue_from ActionController::RoutingError, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity + rescue_from Mastodon::NotPermittedError, with: :forbidden before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :check_suspension, if: :user_signed_in? @@ -40,6 +41,10 @@ class ApplicationController < ActionController::Base redirect_to root_path unless current_user&.admin? end + def require_staff! + redirect_to root_path unless current_user&.staff? + end + def check_suspension forbidden if current_user.account.suspended? end diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb index 7828fe48d..95a37e379 100644 --- a/app/controllers/concerns/authorization.rb +++ b/app/controllers/concerns/authorization.rb @@ -2,6 +2,7 @@ module Authorization extend ActiveSupport::Concern + include Pundit def pundit_user diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb index 09839f16e..ce2530c54 100644 --- a/app/controllers/settings/notifications_controller.rb +++ b/app/controllers/settings/notifications_controller.rb @@ -26,7 +26,7 @@ class Settings::NotificationsController < ApplicationController def user_settings_params params.require(:user).permit( notification_emails: %i(follow follow_request reblog favourite mention digest), - interactions: %i(must_be_follower must_be_following) + interactions: %i(must_be_follower must_be_following must_be_following_dm) ) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6d625e7db..7dfab1df1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -35,6 +35,11 @@ module ApplicationHelper Rails.env.production? ? site_title : "#{site_title} (Dev)" end + def can?(action, record) + return false if record.nil? + policy(record).public_send("#{action}?") + end + def fa_icon(icon, attributes = {}) class_names = attributes[:class]&.split(' ') || [] class_names << 'fa' @@ -43,6 +48,10 @@ module ApplicationHelper content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end + def custom_emoji_tag(custom_emoji) + image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:") + end + def opengraph(property, content) tag(:meta, content: content, property: property) end diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js index c94fb0851..7bc1a2189 100644 --- a/app/javascript/glitch/components/account/header.js +++ b/app/javascript/glitch/components/account/header.js @@ -51,6 +51,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import emojify from '../../../mastodon/features/emoji/emoji'; import IconButton from '../../../mastodon/components/icon_button'; import Avatar from '../../../mastodon/components/avatar'; +import { me } from '../../../mastodon/initial_state'; // Our imports // import { processBio } from '../../util/bio_metadata'; @@ -88,7 +89,6 @@ export default class AccountHeader extends ImmutablePureComponent { static propTypes = { account : ImmutablePropTypes.map, - me : PropTypes.string.isRequired, onFollow : PropTypes.func.isRequired, intl : PropTypes.object.isRequired, }; @@ -102,7 +102,7 @@ The `render()` function is used to render our component. */ render () { - const { account, me, intl } = this.props; + const { account, intl } = this.props; /* diff --git a/app/javascript/glitch/components/status/action_bar.js b/app/javascript/glitch/components/status/action_bar.js index f4450d31b..34588b008 100644 --- a/app/javascript/glitch/components/status/action_bar.js +++ b/app/javascript/glitch/components/status/action_bar.js @@ -9,6 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import RelativeTimestamp from '../../../mastodon/components/relative_timestamp'; import IconButton from '../../../mastodon/components/icon_button'; import DropdownMenuContainer from '../../../mastodon/containers/dropdown_menu_container'; +import { me } from '../../../mastodon/initial_state'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -50,7 +51,6 @@ export default class StatusActionBar extends ImmutablePureComponent { onEmbed: PropTypes.func, onMuteConversation: PropTypes.func, onPin: PropTypes.func, - me: PropTypes.string, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -59,7 +59,6 @@ export default class StatusActionBar extends ImmutablePureComponent { // evaluate to false. See react-immutable-pure-component for usage. updateOnProps = [ 'status', - 'me', 'withDismiss', ] @@ -119,7 +118,7 @@ export default class StatusActionBar extends ImmutablePureComponent { } render () { - const { status, me, intl, withDismiss } = this.props; + const { status, intl, withDismiss } = this.props; const mutingConversation = status.get('muted'); const anonymousAccess = !me; diff --git a/app/javascript/glitch/components/status/container.js b/app/javascript/glitch/components/status/container.js index 24261e763..0054abd14 100644 --- a/app/javascript/glitch/components/status/container.js +++ b/app/javascript/glitch/components/status/container.js @@ -140,12 +140,10 @@ Here are the props we pass to `<Status>`. return { status : status, account : account || ownProps.account, - me : state.getIn(['meta', 'me']), settings : state.get('local_settings'), prepend : prepend || ownProps.prepend, reblogModal : state.getIn(['meta', 'boost_modal']), deleteModal : state.getIn(['meta', 'delete_modal']), - autoPlayGif : state.getIn(['meta', 'auto_play_gif']), }; }; diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js index 6bd95b051..33a9730e5 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -39,6 +39,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; // Mastodon imports // import scheduleIdleTask from '../../../mastodon/features/ui/util/schedule_idle_task'; +import { autoPlayGif } from '../../../mastodon/initial_state'; // Our imports // import StatusPrepend from './prepend'; @@ -89,9 +90,6 @@ few parts: These are our local settings, fetched from our store. We need this to determine how best to collapse our statuses, among other things. - - __`me` (`PropTypes.number`) :__ - This is the id of the currently-signed-in user. - - __`onFavourite`, `onReblog`, `onModalReblog`, `onDelete`, `onMention`, `onMute`, `onMuteConversation`, onBlock`, `onReport`, `onOpenMedia`, `onOpenVideo` (`PropTypes.func`) :__ @@ -103,9 +101,6 @@ few parts: reblogging and deleting statuses. They are used by the `onReblog` and `onDelete` functions, but we don't deal with them here. - - __`autoPlayGif` (`PropTypes.bool`) :__ - This tells the frontend whether or not to autoplay gifs! - - __`muted` (`PropTypes.bool`) :__ This has nothing to do with a user or conversation mute! "Muted" is what Mastodon internally calls the subdued look of statuses in the @@ -160,7 +155,6 @@ export default class Status extends ImmutablePureComponent { account : ImmutablePropTypes.map, settings : ImmutablePropTypes.map, notification : ImmutablePropTypes.map, - me : PropTypes.string, onFavourite : PropTypes.func, onReblog : PropTypes.func, onModalReblog : PropTypes.func, @@ -177,7 +171,6 @@ export default class Status extends ImmutablePureComponent { onOpenVideo : PropTypes.func, reblogModal : PropTypes.bool, deleteModal : PropTypes.bool, - autoPlayGif : PropTypes.bool, muted : PropTypes.bool, collapse : PropTypes.bool, prepend : PropTypes.string, @@ -211,9 +204,7 @@ to remember to specify it here. 'account', 'settings', 'prepend', - 'me', 'boostModal', - 'autoPlayGif', 'muted', 'collapse', 'notification', @@ -560,7 +551,6 @@ this operation are further explained in the code below. intersectionObserverWrapper, onOpenVideo, onOpenMedia, - autoPlayGif, notification, ...other } = this.props; diff --git a/app/javascript/mastodon/actions/pin_statuses.js b/app/javascript/mastodon/actions/pin_statuses.js index 01bf8930b..3f40f6c2d 100644 --- a/app/javascript/mastodon/actions/pin_statuses.js +++ b/app/javascript/mastodon/actions/pin_statuses.js @@ -4,12 +4,13 @@ export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; +import { me } from '../initial_state'; + export function fetchPinnedStatuses() { return (dispatch, getState) => { dispatch(fetchPinnedStatusesRequest()); - const accountId = getState().getIn(['meta', 'me']); - api(getState).get(`/api/v1/accounts/${accountId}/statuses`, { params: { pinned: true } }).then(response => { + api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => { dispatch(fetchPinnedStatusesSuccess(response.data, null)); }).catch(error => { dispatch(fetchPinnedStatusesFail(error)); diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index a2e25c930..e60ddacd9 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -1,4 +1,4 @@ -import createStream from '../stream'; +import { connectStream } from '../stream'; import { updateTimeline, deleteFromTimelines, @@ -12,42 +12,19 @@ import { getLocale } from '../locales'; const { messages } = getLocale(); export function connectTimelineStream (timelineId, path, pollingRefresh = null) { - return (dispatch, getState) => { - const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); - const accessToken = getState().getIn(['meta', 'access_token']); - const locale = getState().getIn(['meta', 'locale']); - let polling = null; - - const setupPolling = () => { - polling = setInterval(() => { - pollingRefresh(dispatch); - }, 20000); - }; - - const clearPolling = () => { - if (polling) { - clearInterval(polling); - polling = null; - } - }; - - const subscription = createStream(streamingAPIBaseURL, accessToken, path, { - connected () { - if (pollingRefresh) { - clearPolling(); - } + return connectStream (path, pollingRefresh, (dispatch, getState) => { + const locale = getState().getIn(['meta', 'locale']); + return { + onConnect() { dispatch(connectTimeline(timelineId)); }, - disconnected () { - if (pollingRefresh) { - setupPolling(); - } + onDisconnect() { dispatch(disconnectTimeline(timelineId)); }, - received (data) { + onReceive (data) { switch(data.event) { case 'update': dispatch(updateTimeline(timelineId, JSON.parse(data.payload))); @@ -60,26 +37,8 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null) break; } }, - - reconnected () { - if (pollingRefresh) { - clearPolling(); - pollingRefresh(dispatch); - } - dispatch(connectTimeline(timelineId)); - }, - - }); - - const disconnect = () => { - if (subscription) { - subscription.close(); - } - clearPolling(); }; - - return disconnect; - }; + }); } function refreshHomeTimelineAndNotification (dispatch) { diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index 376e544fb..2c3a00064 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -7,6 +7,7 @@ import Permalink from './permalink'; import IconButton from './icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { me } from '../initial_state'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, @@ -23,7 +24,6 @@ export default class Account extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - me: PropTypes.string.isRequired, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, @@ -52,7 +52,7 @@ export default class Account extends ImmutablePureComponent { } render () { - const { account, me, intl, hidden } = this.props; + const { account, intl, hidden } = this.props; if (!account) { return <div />; @@ -82,7 +82,7 @@ export default class Account extends ImmutablePureComponent { } else if (muting) { let hidingNotificationsButton; if (muting.get('notifications')) { - hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />; + hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />; } else { hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />; } diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index 76b0da12f..d0c1b049f 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -65,6 +65,7 @@ export default class IconButton extends React.PureComponent { expanded, icon, inverted, + flip, overlay, pressed, tabIndex, @@ -78,8 +79,8 @@ export default class IconButton extends React.PureComponent { overlayed: overlay, }); - const flipDeg = this.props.flip ? -180 : -360; - const rotateDeg = this.props.active ? flipDeg : 0; + const flipDeg = flip ? -180 : -360; + const rotateDeg = active ? flipDeg : 0; const motionDefaultStyle = { rotate: rotateDeg, @@ -93,6 +94,25 @@ export default class IconButton extends React.PureComponent { rotate: animate ? spring(rotateDeg, springOpts) : 0, }; + if (!animate) { + // Perf optimization: avoid unnecessary <Motion> components unless + // we actually need to animate. + return ( + <button + aria-label={title} + aria-pressed={pressed} + aria-expanded={expanded} + title={title} + className={classes} + onClick={this.handleClick} + style={style} + tabIndex={tabIndex} + > + <i className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> + </button> + ); + } + return ( <Motion defaultStyle={motionDefaultStyle} style={motionStyle}> {({ rotate }) => diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 83cf8b871..5ed46dc93 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -9,6 +9,7 @@ import IconButton from './icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { isIOS } from '../is_mobile'; import classNames from 'classnames'; +import { autoPlayGif } from '../initial_state'; const messages = defineMessages({ toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, @@ -26,11 +27,9 @@ class Item extends React.PureComponent { index: PropTypes.number.isRequired, size: PropTypes.number.isRequired, onClick: PropTypes.func.isRequired, - autoPlayGif: PropTypes.bool, }; static defaultProps = { - autoPlayGif: false, standalone: false, index: 0, size: 1, @@ -50,7 +49,7 @@ class Item extends React.PureComponent { } hoverToPlay () { - const { attachment, autoPlayGif } = this.props; + const { attachment } = this.props; return !autoPlayGif && attachment.get('type') === 'gifv'; } @@ -142,7 +141,7 @@ class Item extends React.PureComponent { </a> ); } else if (attachment.get('type') === 'gifv') { - const autoPlay = !isIOS() && this.props.autoPlayGif; + const autoPlay = !isIOS() && autoPlayGif; thumbnail = ( <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> @@ -184,11 +183,9 @@ export default class MediaGallery extends React.PureComponent { height: PropTypes.number.isRequired, onOpenMedia: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, - autoPlayGif: PropTypes.bool, }; static defaultProps = { - autoPlayGif: false, standalone: false, }; @@ -264,9 +261,9 @@ export default class MediaGallery extends React.PureComponent { const size = media.take(4).size; if (this.isStandaloneEligible()) { - children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} autoPlayGif={this.props.autoPlayGif} />; + children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />; } else { - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />); + children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />); } } diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index ab9d48510..71228ca6c 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { ScrollContainer } from 'react-router-scroll'; +import { ScrollContainer } from 'react-router-scroll-4'; import PropTypes from 'prop-types'; import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container'; import LoadMore from './load_more'; diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index b9be20033..5a01c0cdd 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -39,9 +39,6 @@ export default class Status extends ImmutablePureComponent { onBlock: PropTypes.func, onEmbed: PropTypes.func, onHeightChange: PropTypes.func, - me: PropTypes.string, - boostModal: PropTypes.bool, - autoPlayGif: PropTypes.bool, muted: PropTypes.bool, hidden: PropTypes.bool, onMoveUp: PropTypes.func, @@ -57,9 +54,6 @@ export default class Status extends ImmutablePureComponent { updateOnProps = [ 'status', 'account', - 'me', - 'boostModal', - 'autoPlayGif', 'muted', 'hidden', ] @@ -200,7 +194,7 @@ export default class Status extends ImmutablePureComponent { } else { media = ( <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} > - {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />} + {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />} </Bundle> ); } diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index af152cc32..35daf70b9 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -8,6 +8,7 @@ import IconButton from './icon_button'; import DropdownMenuContainer from '../containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { me } from '../initial_state'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -50,7 +51,6 @@ export default class StatusActionBar extends ImmutablePureComponent { onEmbed: PropTypes.func, onMuteConversation: PropTypes.func, onPin: PropTypes.func, - me: PropTypes.string, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -59,7 +59,6 @@ export default class StatusActionBar extends ImmutablePureComponent { // evaluate to false. See react-immutable-pure-component for usage. updateOnProps = [ 'status', - 'me', 'withDismiss', ] @@ -119,7 +118,7 @@ export default class StatusActionBar extends ImmutablePureComponent { } render () { - const { status, me, intl, withDismiss } = this.props; + const { status, intl, withDismiss } = this.props; const mutingConversation = status.get('muted'); const anonymousAccess = !me; diff --git a/app/javascript/mastodon/containers/account_container.js b/app/javascript/mastodon/containers/account_container.js index 5728c878e..5a5136dd1 100644 --- a/app/javascript/mastodon/containers/account_container.js +++ b/app/javascript/mastodon/containers/account_container.js @@ -13,6 +13,7 @@ import { } from '../actions/accounts'; import { openModal } from '../actions/modal'; import { initMuteModal } from '../actions/mutes'; +import { unfollowModal } from '../initial_state'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, @@ -23,8 +24,6 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ account: getAccount(state, props.id), - me: state.getIn(['meta', 'me']), - unfollowModal: state.getIn(['meta', 'unfollow_modal']), }); return mapStateToProps; @@ -34,7 +33,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { - if (this.unfollowModal) { + if (unfollowModal) { dispatch(openModal('CONFIRM', { message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, confirm: intl.formatMessage(messages.unfollowConfirm), diff --git a/app/javascript/mastodon/containers/compose_container.js b/app/javascript/mastodon/containers/compose_container.js index db452d03a..5ee1d2f14 100644 --- a/app/javascript/mastodon/containers/compose_container.js +++ b/app/javascript/mastodon/containers/compose_container.js @@ -6,15 +6,14 @@ import { hydrateStore } from '../actions/store'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; import Compose from '../features/standalone/compose'; +import initialState from '../initial_state'; const { localeData, messages } = getLocale(); addLocaleData(localeData); const store = configureStore(); -const initialStateContainer = document.getElementById('initial-state'); -if (initialStateContainer !== null) { - const initialState = JSON.parse(initialStateContainer.textContent); +if (initialState) { store.dispatch(hydrateStore(initialState)); } diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index a7138e62d..d1710445b 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -4,23 +4,18 @@ import PropTypes from 'prop-types'; import configureStore from '../store/configureStore'; import { showOnboardingOnce } from '../actions/onboarding'; import { BrowserRouter, Route } from 'react-router-dom'; -import { ScrollContext } from 'react-router-scroll'; +import { ScrollContext } from 'react-router-scroll-4'; import UI from '../features/ui'; import { hydrateStore } from '../actions/store'; import { connectUserStream } from '../actions/streaming'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; +import initialState from '../initial_state'; const { localeData, messages } = getLocale(); addLocaleData(localeData); export const store = configureStore(); -const initialState = JSON.parse(document.getElementById('initial-state').textContent); -try { - initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings')); -} catch (e) { - initialState.local_settings = {}; -} const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index e8821223d..b9c461f31 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -17,20 +17,18 @@ import { pin, unpin, } from '../actions/interactions'; -import { - blockAccount, - muteAccount, -} from '../actions/accounts'; +import { blockAccount } from '../actions/accounts'; import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses'; +import { initMuteModal } from '../actions/mutes'; import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { boostModal, deleteModal } from '../initial_state'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, - muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' }, }); const makeMapStateToProps = () => { @@ -38,10 +36,6 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ status: getStatus(state, props.id), - me: state.getIn(['meta', 'me']), - boostModal: state.getIn(['meta', 'boost_modal']), - deleteModal: state.getIn(['meta', 'delete_modal']), - autoPlayGif: state.getIn(['meta', 'auto_play_gif']), }); return mapStateToProps; @@ -61,7 +55,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (status.get('reblogged')) { dispatch(unreblog(status)); } else { - if (e.shiftKey || !this.boostModal) { + if (e.shiftKey || !boostModal) { this.onModalReblog(status); } else { dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); @@ -90,7 +84,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onDelete (status) { - if (!this.deleteModal) { + if (!deleteModal) { dispatch(deleteStatus(status.get('id'))); } else { dispatch(openModal('CONFIRM', { @@ -126,11 +120,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onMute (account) { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.muteConfirm), - onConfirm: () => dispatch(muteAccount(account.get('id'))), - })); + dispatch(initMuteModal(account)); }, onMuteConversation (status) { diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js index 4be037955..e84c921ee 100644 --- a/app/javascript/mastodon/containers/timeline_container.js +++ b/app/javascript/mastodon/containers/timeline_container.js @@ -7,15 +7,14 @@ import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; import PublicTimeline from '../features/standalone/public_timeline'; import HashtagTimeline from '../features/standalone/hashtag_timeline'; +import initialState from '../initial_state'; const { localeData, messages } = getLocale(); addLocaleData(localeData); const store = configureStore(); -const initialStateContainer = document.getElementById('initial-state'); -if (initialStateContainer !== null) { - const initialState = JSON.parse(initialStateContainer.textContent); +if (initialState) { store.dispatch(hydrateStore(initialState)); } diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js index 718e7fbad..389296c42 100644 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ b/app/javascript/mastodon/features/account/components/action_bar.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import { Link } from 'react-router-dom'; import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; +import { me } from '../../../initial_state'; const messages = defineMessages({ mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, @@ -28,7 +29,6 @@ export default class ActionBar extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - me: PropTypes.string.isRequired, onFollow: PropTypes.func, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, @@ -47,7 +47,7 @@ export default class ActionBar extends React.PureComponent { } render () { - const { account, me, intl } = this.props; + const { account, intl } = this.props; let menu = []; let extraInfo = ''; diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 57678d162..b3a73a590 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -8,8 +8,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import IconButton from '../../../components/icon_button'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; -import { connect } from 'react-redux'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { autoPlayGif, me } from '../../../initial_state'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -17,19 +17,10 @@ const messages = defineMessages({ requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, }); -const makeMapStateToProps = () => { - const mapStateToProps = state => ({ - autoPlayGif: state.getIn(['meta', 'auto_play_gif']), - }); - - return mapStateToProps; -}; - class Avatar extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - autoPlayGif: PropTypes.bool.isRequired, }; state = { @@ -47,7 +38,7 @@ class Avatar extends ImmutablePureComponent { } render () { - const { account, autoPlayGif } = this.props; + const { account } = this.props; const { isHovered } = this.state; return ( @@ -74,20 +65,17 @@ class Avatar extends ImmutablePureComponent { } -@connect(makeMapStateToProps) @injectIntl export default class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, - me: PropTypes.string.isRequired, onFollow: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, - autoPlayGif: PropTypes.bool.isRequired, }; render () { - const { account, me, intl } = this.props; + const { account, intl } = this.props; if (!account) { return null; @@ -127,7 +115,7 @@ export default class Header extends ImmutablePureComponent { return ( <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}> <div> - <Avatar account={account} autoPlayGif={this.props.autoPlayGif} /> + <Avatar account={account} /> <span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} /> <span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span> diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index 2a88addc4..a40722417 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -12,14 +12,13 @@ import { getAccountGallery } from '../../selectors'; import MediaItem from './components/media_item'; import HeaderContainer from '../account_timeline/containers/header_container'; import { FormattedMessage } from 'react-intl'; -import { ScrollContainer } from 'react-router-scroll'; +import { ScrollContainer } from 'react-router-scroll-4'; import LoadMore from '../../components/load_more'; const mapStateToProps = (state, props) => ({ medias: getAccountGallery(state, props.params.accountId), isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']), - autoPlayGif: state.getIn(['meta', 'auto_play_gif']), }); @connect(mapStateToProps) @@ -31,7 +30,6 @@ export default class AccountGallery extends ImmutablePureComponent { medias: ImmutablePropTypes.list.isRequired, isLoading: PropTypes.bool, hasMore: PropTypes.bool, - autoPlayGif: PropTypes.bool, }; componentDidMount () { @@ -67,7 +65,7 @@ export default class AccountGallery extends ImmutablePureComponent { } render () { - const { medias, autoPlayGif, isLoading, hasMore } = this.props; + const { medias, isLoading, hasMore } = this.props; let loadMore = null; @@ -100,7 +98,6 @@ export default class AccountGallery extends ImmutablePureComponent { <MediaItem key={media.get('id')} media={media} - autoPlayGif={autoPlayGif} /> )} {loadMore} diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index b33df282f..9a087e922 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -10,7 +10,6 @@ export default class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, - me: PropTypes.string.isRequired, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, @@ -66,7 +65,7 @@ export default class Header extends ImmutablePureComponent { } render () { - const { account, me } = this.props; + const { account } = this.props; if (account === null) { return <MissingIndicator />; @@ -76,13 +75,11 @@ export default class Header extends ImmutablePureComponent { <div className='account-timeline__header'> <InnerHeader account={account} - me={me} onFollow={this.handleFollow} /> <ActionBar account={account} - me={me} onBlock={this.handleBlock} onMention={this.handleMention} onReblogToggle={this.handleReblogToggle} diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index 68c037e9b..b41eb19d4 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -15,6 +15,7 @@ import { initReport } from '../../../actions/reports'; import { openModal } from '../../../actions/modal'; import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { unfollowModal } from '../../../initial_state'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, @@ -27,8 +28,6 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, { accountId }) => ({ account: getAccount(state, accountId), - me: state.getIn(['meta', 'me']), - unfollowModal: state.getIn(['meta', 'unfollow_modal']), }); return mapStateToProps; @@ -38,7 +37,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { - if (this.unfollowModal) { + if (unfollowModal) { dispatch(openModal('CONFIRM', { message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, confirm: intl.formatMessage(messages.unfollowConfirm), diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index e3b864aee..3ad370e32 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -16,7 +16,6 @@ const mapStateToProps = (state, props) => ({ statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()), isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']), hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']), - me: state.getIn(['meta', 'me']), }); @connect(mapStateToProps) @@ -28,7 +27,6 @@ export default class AccountTimeline extends ImmutablePureComponent { statusIds: ImmutablePropTypes.list, isLoading: PropTypes.bool, hasMore: PropTypes.bool, - me: PropTypes.string.isRequired, }; componentWillMount () { @@ -50,7 +48,7 @@ export default class AccountTimeline extends ImmutablePureComponent { } render () { - const { statusIds, isLoading, hasMore, me } = this.props; + const { statusIds, isLoading, hasMore } = this.props; if (!statusIds && isLoading) { return ( @@ -70,7 +68,6 @@ export default class AccountTimeline extends ImmutablePureComponent { statusIds={statusIds} isLoading={isLoading} hasMore={hasMore} - me={me} onScrollToBottom={this.handleScrollToBottom} /> </Column> diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js index e73d984a9..9199529dd 100644 --- a/app/javascript/mastodon/features/blocks/index.js +++ b/app/javascript/mastodon/features/blocks/index.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import LoadingIndicator from '../../components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll'; +import { ScrollContainer } from 'react-router-scroll-4'; import Column from '../ui/components/column'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import AccountContainer from '../../containers/account_container'; diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 2da656fc0..13a41f23f 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -45,7 +45,6 @@ export default class ComposeForm extends ImmutablePureComponent { preselectDate: PropTypes.instanceOf(Date), is_submitting: PropTypes.bool, is_uploading: PropTypes.bool, - me: PropTypes.string, onChange: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired, onClearSuggestions: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index dffa04ff0..dc8fc02ba 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -157,7 +157,6 @@ class EmojiPickerMenu extends React.PureComponent { intl: PropTypes.object.isRequired, skinTone: PropTypes.number.isRequired, onSkinTone: PropTypes.func.isRequired, - autoPlay: PropTypes.bool, }; static defaultProps = { @@ -235,7 +234,7 @@ class EmojiPickerMenu extends React.PureComponent { } render () { - const { loading, style, intl, custom_emojis, autoPlay, skinTone, frequentlyUsedEmojis } = this.props; + const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props; if (loading) { return <div style={{ width: 299 }} />; @@ -250,7 +249,7 @@ class EmojiPickerMenu extends React.PureComponent { perLine={8} emojiSize={22} sheetSize={32} - custom={buildCustomEmojis(custom_emojis, autoPlay)} + custom={buildCustomEmojis(custom_emojis)} color='' emoji='' set='twitter' @@ -284,7 +283,6 @@ export default class EmojiPickerDropdown extends React.PureComponent { static propTypes = { custom_emojis: ImmutablePropTypes.list, frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), - autoPlay: PropTypes.bool, intl: PropTypes.object.isRequired, onPickEmoji: PropTypes.func.isRequired, onSkinTone: PropTypes.func.isRequired, @@ -346,7 +344,7 @@ export default class EmojiPickerDropdown extends React.PureComponent { } render () { - const { intl, onPickEmoji, autoPlay, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; + const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; const title = intl.formatMessage(messages.emoji); const { active, loading } = this.state; @@ -366,7 +364,6 @@ export default class EmojiPickerDropdown extends React.PureComponent { loading={loading} onClose={this.onHideDropdown} onPick={onPickEmoji} - autoPlay={autoPlay} onSkinTone={onSkinTone} skinTone={skinTone} frequentlyUsedEmojis={frequentlyUsedEmojis} diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index ffa0a3442..dfe8241c6 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -23,7 +23,6 @@ const mapStateToProps = state => ({ preselectDate: state.getIn(['compose', 'preselectDate']), is_submitting: state.getIn(['compose', 'is_submitting']), is_uploading: state.getIn(['compose', 'is_uploading']), - me: state.getIn(['compose', 'me']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), settings: state.get('local_settings'), filesAttached: state.getIn(['compose', 'media_attachments']).size > 0, diff --git a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js index 699687c69..e6a535a5d 100644 --- a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js @@ -61,7 +61,6 @@ const getCustomEmojis = createSelector([ const mapStateToProps = state => ({ custom_emojis: getCustomEmojis(state), - autoPlay: state.getIn(['meta', 'auto_play_gif']), skinTone: state.getIn(['settings', 'skinTone']), frequentlyUsedEmojis: getFrequentlyUsedEmojis(state), }); diff --git a/app/javascript/mastodon/features/compose/containers/navigation_container.js b/app/javascript/mastodon/features/compose/containers/navigation_container.js index 8cc53c087..eb9f3ea45 100644 --- a/app/javascript/mastodon/features/compose/containers/navigation_container.js +++ b/app/javascript/mastodon/features/compose/containers/navigation_container.js @@ -1,9 +1,10 @@ import { connect } from 'react-redux'; import NavigationBar from '../components/navigation_bar'; +import { me } from '../../../initial_state'; const mapStateToProps = state => { return { - account: state.getIn(['accounts', state.getIn(['meta', 'me'])]), + account: state.getIn(['accounts', me]), }; }; diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.js b/app/javascript/mastodon/features/compose/containers/warning_container.js index 35eab5976..d34471a3e 100644 --- a/app/javascript/mastodon/features/compose/containers/warning_container.js +++ b/app/javascript/mastodon/features/compose/containers/warning_container.js @@ -3,9 +3,10 @@ import { connect } from 'react-redux'; import Warning from '../components/warning'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; +import { me } from '../../../initial_state'; const mapStateToProps = state => ({ - needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked']), + needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']), }); const WarningWrapper = ({ needsLockWarning }) => { diff --git a/app/javascript/mastodon/features/compose/util/counter.js b/app/javascript/mastodon/features/compose/util/counter.js index e6d2487c5..700ba2163 100644 --- a/app/javascript/mastodon/features/compose/util/counter.js +++ b/app/javascript/mastodon/features/compose/util/counter.js @@ -5,5 +5,5 @@ const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx'; export function countableText(inputText) { return inputText .replace(urlRegex, urlPlaceholder) - .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '@$2'); + .replace(/(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '$1@$3'); }; diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js index 636402172..372459c78 100644 --- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js +++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js @@ -57,5 +57,21 @@ describe('emoji', () => { it('does an emoji whose filename is irregular', () => { expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />'); }); + + it('avoid emojifying on invisible text', () => { + expect(emojify('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>')) + .toEqual('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>'); + expect(emojify('<span class="invisible">:luigi:</span>', { ':luigi:': { static_url: 'luigi.exe' } })) + .toEqual('<span class="invisible">:luigi:</span>'); + }); + + it('avoid emojifying on invisible text with nested tags', () => { + expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇')) + .toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />'); + expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇')) + .toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />'); + expect(emojify('<span class="invisible">😄<br/>😴</span>😇')) + .toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />'); + }); }); }); diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index b70fc2b37..0f005dd50 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -1,3 +1,4 @@ +import { autoPlayGif } from '../../initial_state'; import unicodeMapping from './emoji_unicode_mapping_light'; import Trie from 'substring-trie'; @@ -5,13 +6,13 @@ const trie = new Trie(Object.keys(unicodeMapping)); const assetHost = process.env.CDN_HOST || ''; -let allowAnimations = false; - const emojify = (str, customEmojis = {}) => { - let rtn = ''; + const tagCharsWithoutEmojis = '<&'; + const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&'; + let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0; for (;;) { let match, i = 0, tag; - while (i < str.length && (tag = '<&:'.indexOf(str[i])) === -1 && !(match = trie.search(str.slice(i)))) { + while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) { i += str.codePointAt(i) < 65536 ? 1 : 2; } let rend, replacement = ''; @@ -27,7 +28,7 @@ const emojify = (str, customEmojis = {}) => { // now got a replacee as ':shortname:' // if you want additional emoji handler, add statements below which set replacement and return true. if (shortname in customEmojis) { - const filename = allowAnimations ? customEmojis[shortname].url : customEmojis[shortname].static_url; + const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`; return true; } @@ -35,7 +36,26 @@ const emojify = (str, customEmojis = {}) => { })()) rend = ++i; } else if (tag >= 0) { // <, & rend = str.indexOf('>;'[tag], i + 1) + 1; - if (!rend) break; + if (!rend) { + break; + } + if (tag === 0) { + if (invisible) { + if (str[i + 1] === '/') { // closing tag + if (!--invisible) { + tagChars = tagCharsWithEmojis; + } + } else if (str[rend - 2] !== '/') { // opening tag + invisible++; + } + } else { + if (str.startsWith('<span class="invisible">', i)) { + // avoid emojifying on invisible text + invisible = 1; + tagChars = tagCharsWithoutEmojis; + } + } + } i = rend; } else { // matched to unicode emoji const { filename, shortCode } = unicodeMapping[match]; @@ -51,14 +71,12 @@ const emojify = (str, customEmojis = {}) => { export default emojify; -export const buildCustomEmojis = (customEmojis, overrideAllowAnimations = false) => { +export const buildCustomEmojis = (customEmojis) => { const emojis = []; - allowAnimations = overrideAllowAnimations; - customEmojis.forEach(emoji => { const shortcode = emoji.get('shortcode'); - const url = allowAnimations ? emoji.get('url') : emoji.get('static_url'); + const url = autoPlayGif ? emoji.get('url') : emoji.get('static_url'); const name = shortcode.replace(':', ''); emojis.push({ diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.js b/app/javascript/mastodon/features/emoji/emoji_compressed.js index c0cba952a..e5b834a74 100644 --- a/app/javascript/mastodon/features/emoji/emoji_compressed.js +++ b/app/javascript/mastodon/features/emoji/emoji_compressed.js @@ -64,14 +64,14 @@ Object.keys(emojiMap).forEach(key => { Object.keys(emojiIndex.emojis).forEach(key => { const { native } = emojiIndex.emojis[key]; - const { short_names, search, unified } = emojiMartData.emojis[key]; + let { short_names, search, unified } = emojiMartData.emojis[key]; if (short_names[0] !== key) { throw new Error('The compresser expects the first short_code to be the ' + 'key. It may need to be rewritten if the emoji change such that this ' + 'is no longer the case.'); } - short_names.splice(0, 1); // first short name can be inferred from the key + short_names = short_names.slice(1); // first short name can be inferred from the key const searchData = [native, short_names, search]; if (unicodeToUnifiedName(native) !== unified) { diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js index 4dbfefd87..6f113beb4 100644 --- a/app/javascript/mastodon/features/favourites/index.js +++ b/app/javascript/mastodon/features/favourites/index.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import LoadingIndicator from '../../components/loading_indicator'; import { fetchFavourites } from '../../actions/interactions'; -import { ScrollContainer } from 'react-router-scroll'; +import { ScrollContainer } from 'react-router-scroll-4'; import AccountContainer from '../../containers/account_container'; import Column from '../ui/components/column'; import ColumnBackButton from '../../components/column_back_button'; diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js index 94109b151..1fa52d511 100644 --- a/app/javascript/mastodon/features/follow_requests/index.js +++ b/app/javascript/mastodon/features/follow_requests/index.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import LoadingIndicator from '../../components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll'; +import { ScrollContainer } from 'react-router-scroll-4'; import Column from '../ui/components/column'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import AccountAuthorizeContainer from './containers/account_authorize_container'; diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js index 89445559f..f64ed7948 100644 --- a/app/javascript/mastodon/features/followers/index.js +++ b/app/javascript/mastodon/features/followers/index.js @@ -8,7 +8,7 @@ import { fetchFollowers, expandFollowers, } from '../../actions/accounts'; -import { ScrollContainer } from 'react-router-scroll'; +import { ScrollContainer } from 'react-router-scroll-4'; import AccountContainer from '../../containers/account_container'; import Column from '../ui/components/column'; import HeaderContainer from '../account_timeline/containers/header_container'; diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js index c34830276..a0c0fac05 100644 --- a/app/javascript/mastodon/features/following/index.js +++ b/app/javascript/mastodon/features/following/index.js @@ -8,7 +8,7 @@ import { fetchFollowing, expandFollowing, } from '../../actions/accounts'; -import { ScrollContainer } from 'react-router-scroll'; +import { ScrollContainer } from 'react-router-scroll-4'; import AccountContainer from '../../containers/account_container'; import Column from '../ui/components/column'; import HeaderContainer from '../account_timeline/containers/header_container'; diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 9b94b9830..2f7d9281e 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -8,6 +8,7 @@ import { openModal } from '../../actions/modal'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { me } from '../../initial_state'; const messages = defineMessages({ heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -31,7 +32,7 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ - me: state.getIn(['accounts', state.getIn(['meta', 'me'])]), + myAccount: state.getIn(['accounts', me]), columns: state.getIn(['settings', 'columns']), }); @@ -41,7 +42,7 @@ export default class GettingStarted extends ImmutablePureComponent { static propTypes = { intl: PropTypes.object.isRequired, - me: ImmutablePropTypes.map.isRequired, + myAccount: ImmutablePropTypes.map.isRequired, columns: ImmutablePropTypes.list, multiColumn: PropTypes.bool, dispatch: PropTypes.func.isRequired, @@ -57,7 +58,7 @@ export default class GettingStarted extends ImmutablePureComponent { } render () { - const { intl, me, columns, multiColumn } = this.props; + const { intl, myAccount, columns, multiColumn } = this.props; let navItems = []; @@ -88,7 +89,7 @@ export default class GettingStarted extends ImmutablePureComponent { <ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />, ]); - if (me.get('locked')) { + if (myAccount.get('locked')) { navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />); } diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js index 0f3b8e710..ae6ec343f 100644 --- a/app/javascript/mastodon/features/mutes/index.js +++ b/app/javascript/mastodon/features/mutes/index.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import LoadingIndicator from '../../components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll'; +import { ScrollContainer } from 'react-router-scroll-4'; import Column from '../ui/components/column'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import AccountContainer from '../../containers/account_container'; diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js index f1904786a..579d6aaa0 100644 --- a/app/javascript/mastodon/features/reblogs/index.js +++ b/app/javascript/mastodon/features/reblogs/index.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import LoadingIndicator from '../../components/loading_indicator'; import { fetchReblogs } from '../../actions/interactions'; -import { ScrollContainer } from 'react-router-scroll'; +import { ScrollContainer } from 'react-router-scroll-4'; import AccountContainer from '../../containers/account_container'; import Column from '../ui/components/column'; import ColumnBackButton from '../../components/column_back_button'; diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 3e94f7446..8c6994a07 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -4,6 +4,7 @@ import IconButton from '../../../components/icon_button'; import ImmutablePropTypes from 'react-immutable-proptypes'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; +import { me } from '../../../initial_state'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -36,7 +37,6 @@ export default class ActionBar extends React.PureComponent { onReport: PropTypes.func, onPin: PropTypes.func, onEmbed: PropTypes.func, - me: PropTypes.string.isRequired, intl: PropTypes.object.isRequired, }; @@ -80,7 +80,7 @@ export default class ActionBar extends React.PureComponent { } render () { - const { status, me, intl } = this.props; + const { status, intl } = this.props; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index d8547db36..85a030ea8 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -25,7 +25,6 @@ export default class DetailedStatus extends ImmutablePureComponent { settings: ImmutablePropTypes.map.isRequired, onOpenMedia: PropTypes.func.isRequired, onOpenVideo: PropTypes.func.isRequired, - autoPlayGif: PropTypes.bool, }; handleAccountClick = (e) => { @@ -76,7 +75,6 @@ export default class DetailedStatus extends ImmutablePureComponent { fullwidth={settings.getIn(['media', 'fullwidth'])} height={250} onOpenMedia={this.props.onOpenMedia} - autoPlayGif={this.props.autoPlayGif} /> ); mediaIcon = 'picture-o'; diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index c40630a0a..e7ea046dd 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -1,6 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { fetchStatus } from '../../actions/statuses'; import MissingIndicator from '../../components/missing_indicator'; @@ -22,13 +23,15 @@ import { import { deleteStatus } from '../../actions/statuses'; import { initReport } from '../../actions/reports'; import { makeGetStatus } from '../../selectors'; -import { ScrollContainer } from 'react-router-scroll'; +import { ScrollContainer } from 'react-router-scroll-4'; import ColumnBackButton from '../../components/column_back_button'; import StatusContainer from '../../../glitch/components/status/container'; import { openModal } from '../../actions/modal'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; +import { boostModal, deleteModal } from '../../initial_state'; +import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../../features/ui/util/fullscreen'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -43,10 +46,6 @@ const makeMapStateToProps = () => { settings: state.get('local_settings'), ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]), descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]), - me: state.getIn(['meta', 'me']), - boostModal: state.getIn(['meta', 'boost_modal']), - deleteModal: state.getIn(['meta', 'delete_modal']), - autoPlayGif: state.getIn(['meta', 'auto_play_gif']), }); return mapStateToProps; @@ -67,17 +66,21 @@ export default class Status extends ImmutablePureComponent { settings: ImmutablePropTypes.map.isRequired, ancestorsIds: ImmutablePropTypes.list, descendantsIds: ImmutablePropTypes.list, - me: PropTypes.string, - boostModal: PropTypes.bool, - deleteModal: PropTypes.bool, - autoPlayGif: PropTypes.bool, intl: PropTypes.object.isRequired, }; + state = { + fullscreen: false, + }; + componentWillMount () { this.props.dispatch(fetchStatus(this.props.params.statusId)); } + componentDidMount () { + attachFullscreenListener(this.onFullScreenChange); + } + componentWillReceiveProps (nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { this._scrolledIntoView = false; @@ -113,7 +116,7 @@ export default class Status extends ImmutablePureComponent { if (status.get('reblogged')) { this.props.dispatch(unreblog(status)); } else { - if (e.shiftKey || !this.props.boostModal) { + if (e.shiftKey || !boostModal) { this.handleModalReblog(status); } else { this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog })); @@ -124,7 +127,7 @@ export default class Status extends ImmutablePureComponent { handleDeleteClick = (status) => { const { dispatch, intl } = this.props; - if (!this.props.deleteModal) { + if (!deleteModal) { dispatch(deleteStatus(status.get('id'))); } else { dispatch(openModal('CONFIRM', { @@ -259,9 +262,18 @@ export default class Status extends ImmutablePureComponent { } } + componentWillUnmount () { + detachFullscreenListener(this.onFullScreenChange); + } + + onFullScreenChange = () => { + this.setState({ fullscreen: isFullscreen() }); + } + render () { let ancestors, descendants; - const { status, settings, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props; + const { status, settings, ancestorsIds, descendantsIds } = this.props; + const { fullscreen } = this.state; if (status === null) { return ( @@ -295,7 +307,7 @@ export default class Status extends ImmutablePureComponent { <ColumnBackButton /> <ScrollContainer scrollKey='thread'> - <div className='scrollable detailed-status__wrapper' ref={this.setRef}> + <div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}> {ancestors} <HotKeys handlers={handlers}> @@ -303,15 +315,12 @@ export default class Status extends ImmutablePureComponent { <DetailedStatus status={status} settings={settings} - autoPlayGif={autoPlayGif} - me={me} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} /> <ActionBar status={status} - me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.js b/app/javascript/mastodon/features/ui/components/mute_modal.js index b5e83bb71..73e48cf09 100644 --- a/app/javascript/mastodon/features/ui/components/mute_modal.js +++ b/app/javascript/mastodon/features/ui/components/mute_modal.js @@ -2,6 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { injectIntl, FormattedMessage } from 'react-intl'; +import Toggle from 'react-toggle'; import Button from '../../../components/button'; import { closeModal } from '../../../actions/modal'; import { muteAccount } from '../../../actions/accounts'; @@ -80,12 +81,13 @@ export default class MuteModal extends React.PureComponent { values={{ name: <strong>@{account.get('acct')}</strong> }} /> </p> - <p> + <div> <label htmlFor='mute-modal__hide-notifications-checkbox'> <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' /> - <input id='mute-modal__hide-notifications-checkbox' type='checkbox' checked={notifications} onChange={this.toggleNotifications} /> + {' '} + <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} /> </label> - </p> + </div> </div> <div className='mute-modal__action-bar'> diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js index daf6b485c..1f9f0cd03 100644 --- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js +++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js @@ -14,6 +14,7 @@ import { List as ImmutableList, Map as ImmutableMap, } from 'immutable'; +import { me } from '../../../initial_state'; const noop = () => { }; @@ -43,11 +44,11 @@ PageOne.propTypes = { domain: PropTypes.string.isRequired, }; -const PageTwo = ({ me }) => ( +const PageTwo = ({ myAccount }) => ( <div className='onboarding-modal__page onboarding-modal__page-two'> <div className='figure non-interactive'> <div className='pseudo-drawer'> - <NavigationBar onClose={noop} account={me} /> + <NavigationBar onClose={noop} account={myAccount} /> </div> <ComposeForm text='Awoo! #introductions' @@ -73,10 +74,10 @@ const PageTwo = ({ me }) => ( ); PageTwo.propTypes = { - me: ImmutablePropTypes.map.isRequired, + myAccount: ImmutablePropTypes.map.isRequired, }; -const PageThree = ({ me }) => ( +const PageThree = ({ myAccount }) => ( <div className='onboarding-modal__page onboarding-modal__page-three'> <div className='figure non-interactive'> <Search @@ -88,7 +89,7 @@ const PageThree = ({ me }) => ( /> <div className='pseudo-drawer'> - <NavigationBar onClose={noop} account={me} /> + <NavigationBar onClose={noop} account={myAccount} /> </div> </div> @@ -98,7 +99,7 @@ const PageThree = ({ me }) => ( ); PageThree.propTypes = { - me: ImmutablePropTypes.map.isRequired, + myAccount: ImmutablePropTypes.map.isRequired, }; const PageFour = ({ domain, intl }) => ( @@ -166,7 +167,7 @@ PageSix.propTypes = { }; const mapStateToProps = state => ({ - me: state.getIn(['accounts', state.getIn(['meta', 'me'])]), + myAccount: state.getIn(['accounts', me]), admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]), domain: state.getIn(['meta', 'domain']), }); @@ -178,7 +179,7 @@ export default class OnboardingModal extends React.PureComponent { static propTypes = { onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, - me: ImmutablePropTypes.map.isRequired, + myAccount: ImmutablePropTypes.map.isRequired, domain: PropTypes.string.isRequired, admin: ImmutablePropTypes.map, }; @@ -188,11 +189,11 @@ export default class OnboardingModal extends React.PureComponent { }; componentWillMount() { - const { me, admin, domain, intl } = this.props; + const { myAccount, admin, domain, intl } = this.props; this.pages = [ - <PageOne acct={me.get('acct')} domain={domain} />, - <PageTwo me={me} />, - <PageThree me={me} />, + <PageOne acct={myAccount.get('acct')} domain={domain} />, + <PageTwo myAccount={myAccount} />, + <PageThree myAccount={myAccount} />, <PageFour domain={domain} intl={intl} />, <PageSix admin={admin} domain={domain} />, ]; diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index ff29bfdd4..a0aec4403 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -4,13 +4,13 @@ import { scrollTopTimeline } from '../../../actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { debounce } from 'lodash'; +import { me } from '../../../initial_state'; const makeGetStatusIds = () => createSelector([ (state, { type }) => state.getIn(['settings', type], ImmutableMap()), (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()), (state) => state.get('statuses'), - (state) => state.getIn(['meta', 'me']), -], (columnSettings, statusIds, statuses, me) => { +], (columnSettings, statusIds, statuses) => { const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim(); let regex = null; diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 9f77ab5aa..fc51df163 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -40,18 +40,20 @@ import { PinnedStatuses, } from './util/async-components'; import { HotKeys } from 'react-hotkeys'; +import { me } from '../../initial_state'; +import { defineMessages, injectIntl } from 'react-intl'; // Dummy import, to make sure that <Status /> ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. import '../../../glitch/components/status'; +const messages = defineMessages({ + beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' }, +}); + const mapStateToProps = state => ({ - systemFontUi: state.getIn(['meta', 'system_font_ui']), - layout: state.getIn(['local_settings', 'layout']), - isWide: state.getIn(['local_settings', 'stretch']), - navbarUnder: state.getIn(['local_settings', 'navbar_under']), - me: state.getIn(['meta', 'me']), isComposing: state.getIn(['compose', 'is_composing']), + hasComposingText: state.getIn(['compose', 'text']) !== '', }); const keyMap = { @@ -82,6 +84,7 @@ const keyMap = { }; @connect(mapStateToProps) +@injectIntl @withRouter export default class UI extends React.Component { @@ -97,8 +100,9 @@ export default class UI extends React.Component { systemFontUi: PropTypes.bool, navbarUnder: PropTypes.bool, isComposing: PropTypes.bool, - me: PropTypes.string, + hasComposingText: PropTypes.bool, location: PropTypes.object, + intl: PropTypes.object.isRequired, }; state = { @@ -106,6 +110,17 @@ export default class UI extends React.Component { draggingOver: false, }; + handleBeforeUnload = (e) => { + const { intl, isComposing, hasComposingText } = this.props; + + if (isComposing && hasComposingText) { + // Setting returnValue to any string causes confirmation dialog. + // Many browsers no longer display this text to users, + // but we set user-friendly message for other browsers, e.g. Edge. + e.returnValue = intl.formatMessage(messages.beforeUnload); + } + } + handleResize = debounce(() => { // The cached heights are no longer accurate, invalidate this.props.dispatch(clearHeight()); @@ -180,6 +195,7 @@ export default class UI extends React.Component { } componentWillMount () { + window.addEventListener('beforeunload', this.handleBeforeUnload, false); window.addEventListener('resize', this.handleResize, { passive: true }); document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragover', this.handleDragOver, false); @@ -222,6 +238,7 @@ export default class UI extends React.Component { } componentWillUnmount () { + window.removeEventListener('beforeunload', this.handleBeforeUnload); window.removeEventListener('resize', this.handleResize); document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragover', this.handleDragOver); @@ -321,7 +338,7 @@ export default class UI extends React.Component { } handleHotkeyGoToProfile = () => { - this.context.router.history.push(`/accounts/${this.props.me}`); + this.context.router.history.push(`/accounts/${me}`); } handleHotkeyGoToBlocked = () => { diff --git a/app/javascript/mastodon/features/ui/util/optional_motion.js b/app/javascript/mastodon/features/ui/util/optional_motion.js index af6368738..df3a8b54a 100644 --- a/app/javascript/mastodon/features/ui/util/optional_motion.js +++ b/app/javascript/mastodon/features/ui/util/optional_motion.js @@ -1,56 +1,5 @@ -// Like react-motion's Motion, but checks to see if the user prefers -// reduced motion and uses a cross-fade in those cases. - -import React from 'react'; +import { reduceMotion } from '../../../initial_state'; +import ReducedMotion from './reduced_motion'; import Motion from 'react-motion/lib/Motion'; -import PropTypes from 'prop-types'; - -const stylesToKeep = ['opacity', 'backgroundOpacity']; - -let reduceMotion; - -const extractValue = (value) => { - // This is either an object with a "val" property or it's a number - return (typeof value === 'object' && value && 'val' in value) ? value.val : value; -}; - -class OptionalMotion extends React.Component { - - static propTypes = { - defaultStyle: PropTypes.object, - style: PropTypes.object, - children: PropTypes.func, - } - - render() { - - const { style, defaultStyle, children } = this.props; - - if (typeof reduceMotion !== 'boolean') { - // This never changes without a page reload, so we can just grab it - // once from the body classes as opposed to using Redux's connect(), - // which would unnecessarily update every state change - reduceMotion = document.body.classList.contains('reduce-motion'); - } - if (reduceMotion) { - Object.keys(style).forEach(key => { - if (stylesToKeep.includes(key)) { - return; - } - // If it's setting an x or height or scale or some other value, we need - // to preserve the end-state value without actually animating it - style[key] = defaultStyle[key] = extractValue(style[key]); - }); - } - - return ( - <Motion style={style} defaultStyle={defaultStyle}> - {children} - </Motion> - ); - } - -} - -export default OptionalMotion; +export default reduceMotion ? ReducedMotion : Motion; diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js index 86b30d488..43007ddc3 100644 --- a/app/javascript/mastodon/features/ui/util/react_router_helpers.js +++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js @@ -7,11 +7,19 @@ import BundleColumnError from '../components/bundle_column_error'; import BundleContainer from '../containers/bundle_container'; // Small wrapper to pass multiColumn to the route components -export const WrappedSwitch = ({ multiColumn, children }) => ( - <Switch> - {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} - </Switch> -); +export class WrappedSwitch extends React.PureComponent { + + render () { + const { multiColumn, children } = this.props; + + return ( + <Switch> + {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} + </Switch> + ); + } + +} WrappedSwitch.propTypes = { multiColumn: PropTypes.bool, diff --git a/app/javascript/mastodon/features/ui/util/reduced_motion.js b/app/javascript/mastodon/features/ui/util/reduced_motion.js new file mode 100644 index 000000000..95519042b --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/reduced_motion.js @@ -0,0 +1,44 @@ +// Like react-motion's Motion, but reduces all animations to cross-fades +// for the benefit of users with motion sickness. +import React from 'react'; +import Motion from 'react-motion/lib/Motion'; +import PropTypes from 'prop-types'; + +const stylesToKeep = ['opacity', 'backgroundOpacity']; + +const extractValue = (value) => { + // This is either an object with a "val" property or it's a number + return (typeof value === 'object' && value && 'val' in value) ? value.val : value; +}; + +class ReducedMotion extends React.Component { + + static propTypes = { + defaultStyle: PropTypes.object, + style: PropTypes.object, + children: PropTypes.func, + } + + render() { + + const { style, defaultStyle, children } = this.props; + + Object.keys(style).forEach(key => { + if (stylesToKeep.includes(key)) { + return; + } + // If it's setting an x or height or scale or some other value, we need + // to preserve the end-state value without actually animating it + style[key] = defaultStyle[key] = extractValue(style[key]); + }); + + return ( + <Motion style={style} defaultStyle={defaultStyle}> + {children} + </Motion> + ); + } + +} + +export default ReducedMotion; diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js new file mode 100644 index 000000000..ef5d8b0ef --- /dev/null +++ b/app/javascript/mastodon/initial_state.js @@ -0,0 +1,21 @@ +const element = document.getElementById('initial-state'); +const initialState = element && function () { + const result = JSON.parse(element.textContent); + try { + result.local_settings = JSON.parse(localStorage.getItem('mastodon-settings')); + } catch (e) { + result.local_settings = {}; + } + return result; +}(); + +const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop]; + +export const reduceMotion = getMeta('reduce_motion'); +export const autoPlayGif = getMeta('auto_play_gif'); +export const unfollowModal = getMeta('unfollow_modal'); +export const boostModal = getMeta('boost_modal'); +export const deleteModal = getMeta('delete_modal'); +export const me = getMeta('me'); + +export default initialState; diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index d99dacd59..2919928af 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -63,7 +63,7 @@ "confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?", "confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", - "embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 퍼가세요.", + "embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 공유하세요.", "embed.preview": "다음과 같이 표시됩니다:", "emoji_button.activity": "활동", "emoji_button.custom": "Custom", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 1e0849d95..d826423b5 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -31,7 +31,7 @@ "column.favourites": "Favorits", "column.follow_requests": "Demandas d’abonament", "column.home": "Acuèlh", - "column.mutes": "Personas en silenci", + "column.mutes": "Personas rescondudas", "column.notifications": "Notificacions", "column.pins": "Tuts penjats", "column.public": "Flux public global", @@ -55,12 +55,12 @@ "confirmation_modal.cancel": "Anullar", "confirmations.block.confirm": "Blocar", "confirmations.block.message": "Sètz segur de voler blocar {name} ?", - "confirmations.delete.confirm": "Suprimir", - "confirmations.delete.message": "Sètz segur de voler suprimir l’estatut ?", + "confirmations.delete.confirm": "Escafar", + "confirmations.delete.message": "Sètz segur de voler escafar l’estatut ?", "confirmations.domain_block.confirm": "Amagar tot lo domeni", "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.", - "confirmations.mute.confirm": "Metre en silenci", - "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?", + "confirmations.mute.confirm": "Rescondre", + "confirmations.mute.message": "Sètz segur de voler rescondre {name} ?", "confirmations.unfollow.confirm": "Quitar de sègre", "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?", "embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.", @@ -135,7 +135,7 @@ "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.", "onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del mond que seguètz.", "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos", - "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum mai larg. Òm los apèla instàncias.", + "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per construire un malhum mai larg. Òm los apèla instàncias.", "onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}", "onboarding.page_one.welcome": "Benvengut a Mastodon !", "onboarding.page_six.admin": "Vòstre administrator d’instància es {admin}.", @@ -159,11 +159,11 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Mostrar pas dins los fluxes publics", "privacy.unlisted.short": "Pas-listat", - "relative_time.days": "fa {number}j", - "relative_time.hours": "fa {number}h", + "relative_time.days": "fa {number} d", + "relative_time.hours": "fa {number} h", "relative_time.just_now": "ara", - "relative_time.minutes": "fa {number} minutas", - "relative_time.seconds": "fa {number} segondas", + "relative_time.minutes": "fa {number} min", + "relative_time.seconds": "fa {number} s", "reply_indicator.cancel": "Anullar", "report.placeholder": "Comentaris addicionals", "report.submit": "Mandar", @@ -197,7 +197,7 @@ "status.share": "Partejar", "status.show_less": "Tornar plegar", "status.show_more": "Desplegar", - "status.unmute_conversation": "Conversacions amb silenci levat", + "status.unmute_conversation": "Tornar mostrar la conversacion", "status.unpin": "Tirar del perfil", "tabs_bar.compose": "Compausar", "tabs_bar.federated_timeline": "Flux public global", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index cf76f1b1f..b23a5e69f 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -89,7 +89,7 @@ "follow_request.reject": "Odrzuć", "getting_started.appsshort": "Aplikacje", "getting_started.faq": "FAQ", - "getting_started.heading": "Naucz się korzystać", + "getting_started.heading": "Rozpocznij", "getting_started.open_source_notice": "Mastodon jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitHubie tutaj: {github}.", "getting_started.userguide": "Podręcznik użytkownika", "home.column_settings.advanced": "Zaawansowane", @@ -174,7 +174,7 @@ "search_popout.tips.status": "wpis", "search_popout.tips.text": "Proste wyszukiwanie pasujących pseudonimów, nazw użytkowników i hashtagów", "search_popout.tips.user": "użytkownik", - "search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}", + "search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}", "standalone.public_title": "Spojrzenie w głąb…", "status.cannot_reblog": "Ten wpis nie może zostać podbity", "status.delete": "Usuń", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index ddb8b83f5..a04d1cc31 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -161,7 +161,7 @@ "privacy.unlisted.short": "Não listada", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", - "relative_time.just_now": "now", + "relative_time.just_now": "agora", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Cancelar", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 827c815cf..0f4bbb7c1 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -1,25 +1,25 @@ { "account.block": "屏蔽 @{name}", "account.block_domain": "隐藏一切来自 {domain} 的嘟文", - "account.disclaimer_full": "下列资料不一定完整。", + "account.disclaimer_full": "此处显示的信息可能不是全部内容。", "account.edit_profile": "修改个人资料", "account.follow": "关注", "account.followers": "关注者", - "account.follows": "正关注", - "account.follows_you": "关注你", + "account.follows": "正在关注", + "account.follows_you": "关注了你", "account.media": "媒体", "account.mention": "提及 @{name}", - "account.mute": "将 @{name} 静音", + "account.mute": "静音 @{name}", "account.posts": "嘟文", "account.report": "举报 @{name}", - "account.requested": "等待审批", - "account.share": "分享 @{name}的个人资料", - "account.unblock": "解除对 @{name} 的屏蔽", + "account.requested": "正在等待对方同意。点击以取消发送关注请求", + "account.share": "分享 @{name} 的个人资料", + "account.unblock": "不再屏蔽 @{name}", "account.unblock_domain": "不再隐藏 {domain}", "account.unfollow": "取消关注", - "account.unmute": "取消 @{name} 的静音", + "account.unmute": "不再静音 @{name}", "account.view_full_profile": "查看完整资料", - "boost_modal.combo": "如你想在下次路过时显示,请按{combo},", + "boost_modal.combo": "下次按住 {combo} 即可跳过此提示", "bundle_column_error.body": "载入组件出错。", "bundle_column_error.retry": "重试", "bundle_column_error.title": "网络错误", @@ -37,72 +37,72 @@ "column.public": "跨站公共时间轴", "column_back_button.label": "返回", "column_header.hide_settings": "隐藏设置", - "column_header.moveLeft_settings": "将栏左移", - "column_header.moveRight_settings": "将栏右移", + "column_header.moveLeft_settings": "将此栏左移", + "column_header.moveRight_settings": "将此栏右移", "column_header.pin": "固定", "column_header.show_settings": "显示设置", - "column_header.unpin": "取下", + "column_header.unpin": "取消固定", "column_subheading.navigation": "导航", "column_subheading.settings": "设置", - "compose_form.lock_disclaimer": "你的帐户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.", + "compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以通过关注你来查看仅关注者可见的嘟文。", "compose_form.lock_disclaimer.lock": "被保护", "compose_form.placeholder": "在想啥?", "compose_form.publish": "嘟嘟", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive": "将媒体文件标示为“敏感内容”", - "compose_form.spoiler": "将部分文本藏于警告消息之后", - "compose_form.spoiler_placeholder": "敏感内容的警告消息", + "compose_form.sensitive": "将媒体文件标记为敏感内容", + "compose_form.spoiler": "折叠嘟文内容", + "compose_form.spoiler_placeholder": "折叠部分的警告消息", "confirmation_modal.cancel": "取消", "confirmations.block.confirm": "屏蔽", - "confirmations.block.message": "想好了,真的要屏蔽 {name}?", + "confirmations.block.message": "想好了,真的要屏蔽 {name}?", "confirmations.delete.confirm": "删除", - "confirmations.delete.message": "想好了,真的要删除这条嘟文?", + "confirmations.delete.message": "想好了,真的要删除这条嘟文?", "confirmations.domain_block.confirm": "隐藏整个网站", - "confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain} ?多数情况下,封锁或静音几个特定目标就好。", + "confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain}?多数情况下,屏蔽或静音几个特定的用户就应该能满足你的需要了。", "confirmations.mute.confirm": "静音", - "confirmations.mute.message": "想好了,真的要静音 {name}?", + "confirmations.mute.message": "想好了,真的要静音 {name}?", "confirmations.unfollow.confirm": "取消关注", - "confirmations.unfollow.message": "确定要取消关注 {name}吗?", - "embed.instructions": "要内嵌此嘟文,请将以下代码贴进你的网站。", - "embed.preview": "到时大概长这样:", + "confirmations.unfollow.message": "确定要取消关注 {name} 吗?", + "embed.instructions": "要在你的网站上嵌入这条嘟文,请复制以下代码。", + "embed.preview": "它会像这样显示出来:", "emoji_button.activity": "活动", - "emoji_button.custom": "Custom", + "emoji_button.custom": "自定义", "emoji_button.flags": "旗帜", "emoji_button.food": "食物和饮料", "emoji_button.label": "加入表情符号", "emoji_button.nature": "自然", - "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "木有这个表情符号!(╯°□°)╯︵ ┻━┻", "emoji_button.objects": "物体", "emoji_button.people": "人物", - "emoji_button.recent": "Frequently used", + "emoji_button.recent": "常用", "emoji_button.search": "搜索…", - "emoji_button.search_results": "Search results", + "emoji_button.search_results": "搜索结果", "emoji_button.symbols": "符号", - "emoji_button.travel": "旅途和地点", - "empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!", - "empty_column.hashtag": "这个标签暂时未有内容。", + "emoji_button.travel": "旅行和地点", + "empty_column.community": "本站时间轴暂时没有内容,快嘟几个来抢头香啊!", + "empty_column.hashtag": "这个话题标签下暂时没有内容。", "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", "empty_column.home.public_timeline": "公共时间轴", - "empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。", - "empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。", - "follow_request.authorize": "批准", + "empty_column.notifications": "你还没有收到过通知信息,快向其他用户搭讪吧。", + "empty_column.public": "这里神马都没有!写一些公开的嘟文,或者关注其他实例的用户,这里就会有嘟文出现了哦!", + "follow_request.authorize": "同意", "follow_request.reject": "拒绝", - "getting_started.appsshort": "Apps", - "getting_started.faq": "FAQ", + "getting_started.appsshort": "应用", + "getting_started.faq": "常见问题", "getting_started.heading": "开始使用", - "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。", + "getting_started.open_source_notice": "Mastodon 是一个开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。", "getting_started.userguide": "用户指南", - "home.column_settings.advanced": "高端", - "home.column_settings.basic": "基本", - "home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤", - "home.column_settings.show_reblogs": "显示被转的嘟文", - "home.column_settings.show_replies": "显示回应嘟文", - "home.settings": "字段设置", + "home.column_settings.advanced": "高级设置", + "home.column_settings.basic": "基本设置", + "home.column_settings.filter_regex": "使用正则表达式(regex)过滤", + "home.column_settings.show_reblogs": "显示转嘟", + "home.column_settings.show_replies": "显示回复", + "home.settings": "栏目设置", "lightbox.close": "关闭", "lightbox.next": "下一步", "lightbox.previous": "上一步", "loading_indicator.label": "加载中……", - "media_gallery.toggle_visible": "打开或关上", + "media_gallery.toggle_visible": "切换显示/隐藏", "missing_indicator.label": "找不到内容", "navigation_bar.blocks": "被屏蔽的用户", "navigation_bar.community_timeline": "本站时间轴", @@ -119,9 +119,9 @@ "notification.follow": "{name} 开始关注你", "notification.mention": "{name} 提及你", "notification.reblog": "{name} 转嘟了你的嘟文", - "notifications.clear": "清空通知纪录", - "notifications.clear_confirmation": "你确定要清空通知纪录吗?", - "notifications.column_settings.alert": "显示桌面通知", + "notifications.clear": "清空通知列表", + "notifications.clear_confirmation": "你确定要清空通知列表吗?", + "notifications.column_settings.alert": "桌面通知", "notifications.column_settings.favourite": "你的嘟文被收藏:", "notifications.column_settings.follow": "关注你:", "notifications.column_settings.mention": "提及你:", @@ -132,90 +132,91 @@ "notifications.column_settings.sound": "播放音效", "onboarding.done": "出发!", "onboarding.next": "下一步", - "onboarding.page_five.public_timelines": "本站时间轴显示来自 {domain} 的所有人的公共嘟文。 跨站公共时间轴显示 {domain} 上的各位关注的来自所有Mastodon服务器实例上的人发表的公共嘟文。这些就是寻人好去处的公共时间轴啦。", - "onboarding.page_four.home": "你的主时间轴上是你关注的用户的嘟文.", - "onboarding.page_four.notifications": "如果你和他人产生了互动,便会出现在通知列上啦~", - "onboarding.page_one.federation": "Mastodon是由一系列独立的服务器共同打造的强大的社交网络,我们将这些独立但又相互连接的服务器叫做服务器实例。", - "onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整帐户名称。", - "onboarding.page_one.welcome": "欢迎来到 Mastodon!", + "onboarding.page_five.public_timelines": "本站时间轴显示的是由本站({domain})用户发布的所有公开嘟文。跨站公共时间轴显示的的是由本站用户关注对象所发布的所有公开嘟文。这些就是寻人好去处的公共时间轴啦。", + "onboarding.page_four.home": "你的主页上的时间轴上显示的是你关注对象的嘟文。", + "onboarding.page_four.notifications": "如果有人与你互动,便会出现在通知栏中哦~", + "onboarding.page_one.federation": "Mastodon 是由一系列独立的服务器共同打造的强大的社交网络,我们将这些各自独立但又相互连接的服务器叫做实例。", + "onboarding.page_one.handle": "你在 {domain},{handle} 就是你的完整帐户名称。", + "onboarding.page_one.welcome": "欢迎来到 Mastodon!", "onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.", - "onboarding.page_six.almost_done": "差不多了…", - "onboarding.page_six.appetoot": "嗷呜~", - "onboarding.page_six.apps_available": "也有适用于 iOS, Android 和其它平台的 {apps} 咯~", - "onboarding.page_six.github": "Mastodon 是自由的开放源代码软件。欢迎来 {github} 报告问题,提交功能请求,或者贡献代码 :-)", + "onboarding.page_six.almost_done": "差不多了……", + "onboarding.page_six.appetoot": "嗷呜~", + "onboarding.page_six.apps_available": "我们还有适用于 iOS、Android 和其它平台的{apps}哦~", + "onboarding.page_six.github": "Mastodon 是自由的开源软件。欢迎前往 {github} 反馈问题、提出对新功能的建议或贡献代码 :-)", "onboarding.page_six.guidelines": "社区指南", - "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}!", - "onboarding.page_six.various_app": "移动应用程序", - "onboarding.page_three.profile": "修改你的个人资料,比如头像、简介、和昵称等等。在那还可以找到其它首选项。", - "onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整帐户名称(用户名@域名)啦。", - "onboarding.page_two.compose": "从这里开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.", - "onboarding.skip": "好啦好啦我知道啦", - "privacy.change": "调整隐私设置", - "privacy.direct.long": "只有提及的用户能看到", - "privacy.direct.short": "私人消息", - "privacy.private.long": "只有关注你用户能看到", - "privacy.private.short": "关注者", - "privacy.public.long": "在公共时间轴显示", - "privacy.public.short": "公共", - "privacy.unlisted.long": "公开,但不在公共时间轴显示", - "privacy.unlisted.short": "公开", - "relative_time.days": "{number}d", - "relative_time.hours": "{number}h", - "relative_time.just_now": "now", - "relative_time.minutes": "{number}m", - "relative_time.seconds": "{number}s", + "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的{guidelines}!", + "onboarding.page_six.various_app": "移动设备应用", + "onboarding.page_three.profile": "你可以修改你的个人资料,比如头像、简介和昵称等偏好设置。", + "onboarding.page_three.search": "你可以通过搜索功能寻找用户和话题标签,比如{illustration}或者{introductions}。如果你想搜索其他实例上的用户,就需要输入完整帐户名称(用户名@域名)哦。", + "onboarding.page_two.compose": "在撰写栏中开始嘟嘟吧!下方的按钮分别用来上传图片,修改嘟文可见范围,以及添加警告信息。", + "onboarding.skip": "跳过", + "privacy.change": "设置嘟文可见范围", + "privacy.direct.long": "只有被提及的用户能看到", + "privacy.direct.short": "私信", + "privacy.private.long": "只有关注你的用户能看到", + "privacy.private.short": "仅关注者", + "privacy.public.long": "所有人可见,并会出现在公共时间轴上", + "privacy.public.short": "公开", + "privacy.unlisted.long": "所有人可见,但不会出现在公共时间轴上", + "privacy.unlisted.short": "不公开", + "relative_time.days": "{number} 天", + "relative_time.hours": "{number} 时", + "relative_time.just_now": "刚刚", + "relative_time.minutes": "{number} 分", + "relative_time.seconds": "{number} 秒", "reply_indicator.cancel": "取消", - "report.placeholder": "额外消息", + "report.placeholder": "附言", "report.submit": "提交", - "report.target": "Reporting", + "report.target": "举报 {target}", "search.placeholder": "搜索", - "search_popout.search_format": "Advanced search format", - "search_popout.tips.hashtag": "hashtag", - "search_popout.tips.status": "status", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", - "search_popout.tips.user": "user", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "search_popout.search_format": "高级搜索格式", + "search_popout.tips.hashtag": "话题标签", + "search_popout.tips.status": "嘟文", + "search_popout.tips.text": "使用普通字符进行搜索将会返回昵称、用户名和话题标签", + "search_popout.tips.user": "用户", + "search_results.total": "共 {count, number} 个结果", "standalone.public_title": "大家都在干啥?", - "status.cannot_reblog": "没法转嘟这条嘟文啦……", + "status.cannot_reblog": "无法转嘟这条嘟文", "status.delete": "删除", "status.embed": "嵌入", "status.favourite": "收藏", "status.load_more": "加载更多", "status.media_hidden": "隐藏媒体内容", "status.mention": "提及 @{name}", - "status.more": "More", - "status.mute_conversation": "静音对话", + "status.more": "更多", + "status.mute_conversation": "静音此对话", "status.open": "展开嘟文", - "status.pin": "置顶到资料", + "status.pin": "在个人资料页面置顶", "status.reblog": "转嘟", - "status.reblogged_by": "{name} 转嘟", - "status.reply": "回应", - "status.replyAll": "回应整串", + "status.reblogged_by": "{name} 转嘟了", + "status.reply": "回复", + "status.replyAll": "回复所有人", "status.report": "举报 @{name}", "status.sensitive_toggle": "点击显示", "status.sensitive_warning": "敏感内容", - "status.share": "Share", - "status.show_less": "减少显示", - "status.show_more": "显示更多", - "status.unmute_conversation": "解禁对话", - "status.unpin": "解除置顶", + "status.share": "分享", + "status.show_less": "隐藏内容", + "status.show_more": "显示内容", + "status.unmute_conversation": "不再静音此对话", + "status.unpin": "在个人资料页面取消置顶", "tabs_bar.compose": "撰写", "tabs_bar.federated_timeline": "跨站", "tabs_bar.home": "主页", "tabs_bar.local_timeline": "本站", "tabs_bar.notifications": "通知", - "upload_area.title": "将文件拖放至此上传", + "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。", + "upload_area.title": "将文件拖放到此处开始上传", "upload_button.label": "上传媒体文件", - "upload_form.description": "Describe for the visually impaired", - "upload_form.undo": "还原", - "upload_progress.label": "上传中……", - "video.close": "关闭影片", + "upload_form.description": "为视觉障碍人士添加文字说明", + "upload_form.undo": "取消上传", + "upload_progress.label": "上传中…", + "video.close": "关闭视频", "video.exit_fullscreen": "退出全屏", - "video.expand": "展开影片", + "video.expand": "展开视频", "video.fullscreen": "全屏", - "video.hide": "隐藏影片", + "video.hide": "隐藏视频", "video.mute": "静音", "video.pause": "暂停", "video.play": "播放", - "video.unmute": "解除静音" + "video.unmute": "取消静音" } diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 251a40144..5d0acbd60 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -33,6 +33,7 @@ import { TIMELINE_DELETE } from '../actions/timelines'; import { STORE_HYDRATE } from '../actions/store'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import uuid from '../uuid'; +import { me } from '../initial_state'; const initialState = ImmutableMap({ mounted: false, @@ -54,7 +55,6 @@ const initialState = ImmutableMap({ media_attachments: ImmutableList(), suggestion_token: null, suggestions: ImmutableList(), - me: null, default_advanced_options: ImmutableMap({ do_not_federate: false, }), @@ -77,7 +77,6 @@ const initialState = ImmutableMap({ function statusToTextMentions(state, status) { let set = ImmutableOrderedSet([]); - let me = state.get('me'); if (status.getIn(['account', 'id']) !== me) { set = set.add(`@${status.getIn(['account', 'acct'])} `); diff --git a/app/javascript/mastodon/reducers/custom_emojis.js b/app/javascript/mastodon/reducers/custom_emojis.js index f2a8ca5d2..307bcc7dc 100644 --- a/app/javascript/mastodon/reducers/custom_emojis.js +++ b/app/javascript/mastodon/reducers/custom_emojis.js @@ -8,7 +8,7 @@ const initialState = ImmutableList(); export default function custom_emojis(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: - emojiSearch('', { custom: buildCustomEmojis(action.state.get('custom_emojis', []), action.state.getIn(['meta', 'auto_play_gif'], false)) }); + emojiSearch('', { custom: buildCustomEmojis(action.state.get('custom_emojis', [])) }); return action.state.get('custom_emojis'); default: return state; diff --git a/app/javascript/mastodon/reducers/meta.js b/app/javascript/mastodon/reducers/meta.js index 119ef9d8f..36a5a1c35 100644 --- a/app/javascript/mastodon/reducers/meta.js +++ b/app/javascript/mastodon/reducers/meta.js @@ -4,7 +4,6 @@ import { Map as ImmutableMap } from 'immutable'; const initialState = ImmutableMap({ streaming_api_base_url: null, access_token: null, - me: null, }); export default function meta(state = initialState, action) { diff --git a/app/javascript/mastodon/reducers/mutes.js b/app/javascript/mastodon/reducers/mutes.js index 496e6846a..a96232dbd 100644 --- a/app/javascript/mastodon/reducers/mutes.js +++ b/app/javascript/mastodon/reducers/mutes.js @@ -22,7 +22,7 @@ export default function mutes(state = initialState, action) { state.setIn(['new', 'notifications'], true); }); case MUTES_TOGGLE_HIDE_NOTIFICATIONS: - return state.setIn(['new', 'notifications'], !state.getIn(['new', 'notifications'])); + return state.updateIn(['new', 'notifications'], (old) => !old); default: return state; } diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js index 4b36082b2..36c68ffc5 100644 --- a/app/javascript/mastodon/stream.js +++ b/app/javascript/mastodon/stream.js @@ -1,5 +1,66 @@ import WebSocketClient from 'websocket.js'; +export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) { + return (dispatch, getState) => { + const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); + const accessToken = getState().getIn(['meta', 'access_token']); + const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState); + let polling = null; + + const setupPolling = () => { + polling = setInterval(() => { + pollingRefresh(dispatch); + }, 20000); + }; + + const clearPolling = () => { + if (polling) { + clearInterval(polling); + polling = null; + } + }; + + const subscription = getStream(streamingAPIBaseURL, accessToken, path, { + connected () { + if (pollingRefresh) { + clearPolling(); + } + onConnect(); + }, + + disconnected () { + if (pollingRefresh) { + setupPolling(); + } + onDisconnect(); + }, + + received (data) { + onReceive(data); + }, + + reconnected () { + if (pollingRefresh) { + clearPolling(); + pollingRefresh(dispatch); + } + onConnect(); + }, + + }); + + const disconnect = () => { + if (subscription) { + subscription.close(); + } + clearPolling(); + }; + + return disconnect; + }; +} + + export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) { const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`); diff --git a/app/javascript/packs/custom.js b/app/javascript/packs/custom.js deleted file mode 100644 index 4db2964f6..000000000 --- a/app/javascript/packs/custom.js +++ /dev/null @@ -1 +0,0 @@ -require('../styles/custom.scss'); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 2506bbe62..6a6d1bdca 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -253,6 +253,15 @@ width: 0; height: 0; position: absolute; + + img, + svg { + margin: 0 !important; + border: 0 !important; + padding: 0 !important; + width: 0 !important; + height: 0 !important; + } } .ellipsis { @@ -555,6 +564,7 @@ font-weight: 400; overflow: visible; white-space: pre-wrap; + padding-top: 5px; &.status__content--with-spoiler { white-space: normal; @@ -565,8 +575,9 @@ } .emojione { - width: 18px; - height: 18px; + width: 20px; + height: 20px; + margin: -5px 0 0; } p { @@ -671,7 +682,7 @@ outline: 0; background: lighten($ui-base-color, 4%); - &.status-direct { + .status.status-direct { background: lighten($ui-base-color, 12%); } @@ -690,6 +701,12 @@ border-bottom: 1px solid lighten($ui-base-color, 8%); cursor: default; + @supports (-ms-overflow-style: -ms-autohiding-scrollbar) { + // Add margin to avoid Edge auto-hiding scrollbar appearing over content. + // On Edge 16 this is 16px and Edge <=15 it's 12px, so aim for 16px. + padding-right: 26px; // 10px + 16px + } + @keyframes fade { 0% { opacity: 0; } 100% { opacity: 1; } @@ -920,8 +937,9 @@ line-height: 24px; .emojione { - width: 22px; - height: 22px; + width: 24px; + height: 24px; + margin: -5px 0 0; } } @@ -2908,7 +2926,7 @@ button.icon-button.active i.fa-retweet { color: $primary-text-color; position: absolute; top: 10px; - right: 10px; + left: 10px; opacity: 0.7; display: inline-block; vertical-align: top; @@ -2923,7 +2941,7 @@ button.icon-button.active i.fa-retweet { .account--action-button { position: absolute; top: 10px; - left: 20px; + right: 20px; } .setting-toggle { @@ -3973,6 +3991,14 @@ button.icon-button.active i.fa-retweet { } } +.mute-modal { + line-height: 24px; +} + +.mute-modal .react-toggle { + vertical-align: middle; +} + .report-modal__statuses, .report-modal__comment { padding: 10px; diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss index 15ff84912..0bf9daafd 100644 --- a/app/javascript/styles/mastodon/landing_strip.scss +++ b/app/javascript/styles/mastodon/landing_strip.scss @@ -1,4 +1,5 @@ -.landing-strip { +.landing-strip, +.memoriam-strip { background: rgba(darken($ui-base-color, 7%), 0.8); color: $ui-primary-color; font-weight: 400; @@ -29,3 +30,7 @@ margin-bottom: 0; } } + +.memoriam-strip { + background: rgba($base-shadow-color, 0.7); +} diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 57f105da7..733a1c4b7 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -89,20 +89,28 @@ class Formatter end end + def count_tag_nesting(tag) + if tag[1] == '/' then -1 + elsif tag[-2] == '/' then 0 + else 1 + end + end + def encode_custom_emojis(html, emojis) return html if emojis.empty? emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url(:static))] }.to_h i = -1 - inside_tag = false + tag_open_index = nil inside_shortname = false shortname_start_index = -1 + invisible_depth = 0 while i + 1 < html.size i += 1 - if inside_shortname && html[i] == ':' + if invisible_depth.zero? && inside_shortname && html[i] == ':' shortcode = html[shortname_start_index + 1..i - 1] emoji = emoji_map[shortcode] @@ -116,12 +124,18 @@ class Formatter end inside_shortname = false - elsif inside_tag && html[i] == '>' - inside_tag = false + elsif tag_open_index && html[i] == '>' + tag = html[tag_open_index..i] + tag_open_index = nil + if invisible_depth.positive? + invisible_depth += count_tag_nesting(tag) + elsif tag == '<span class="invisible">' + invisible_depth = 1 + end elsif html[i] == '<' - inside_tag = true + tag_open_index = i inside_shortname = false - elsif !inside_tag && html[i] == ':' + elsif !tag_open_index && html[i] == ':' inside_shortname = true shortname_start_index = i end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 80c9d8ccf..d79f26366 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -7,6 +7,8 @@ class NotificationMailer < ApplicationMailer @me = recipient @status = notification.target_status + return if @me.user.disabled? + locale_for_account(@me) do thread_by_conversation(@status.conversation) mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct) @@ -17,6 +19,8 @@ class NotificationMailer < ApplicationMailer @me = recipient @account = notification.from_account + return if @me.user.disabled? + locale_for_account(@me) do mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct) end @@ -27,6 +31,8 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status + return if @me.user.disabled? + locale_for_account(@me) do thread_by_conversation(@status.conversation) mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct) @@ -38,6 +44,8 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status + return if @me.user.disabled? + locale_for_account(@me) do thread_by_conversation(@status.conversation) mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct) @@ -48,6 +56,8 @@ class NotificationMailer < ApplicationMailer @me = recipient @account = notification.from_account + return if @me.user.disabled? + locale_for_account(@me) do mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) end @@ -59,15 +69,11 @@ class NotificationMailer < ApplicationMailer @notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since) @follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count - return if @notifications.empty? + return if @me.user.disabled? || @notifications.empty? locale_for_account(@me) do mail to: @me.user.email, - subject: I18n.t( - :subject, - scope: [:notification_mailer, :digest], - count: @notifications.size - ) + subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications.size) end end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index c475a9911..bdb29ebad 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -10,6 +10,8 @@ class UserMailer < Devise::Mailer @token = token @instance = Rails.configuration.x.local_domain + return if @resource.disabled? + I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.unconfirmed_email.blank? ? @resource.email : @resource.unconfirmed_email, subject: I18n.t('devise.mailer.confirmation_instructions.subject', instance: @instance) end @@ -20,6 +22,8 @@ class UserMailer < Devise::Mailer @token = token @instance = Rails.configuration.x.local_domain + return if @resource.disabled? + I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject') end @@ -29,6 +33,8 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain + return if @resource.disabled? + I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject') end diff --git a/app/models/account.rb b/app/models/account.rb index 85684c259..863bfaa25 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -3,7 +3,7 @@ # # Table name: accounts # -# id :integer not null, primary key +# id :bigint not null, primary key # username :string default(""), not null # domain :string # secret :string default(""), not null @@ -41,10 +41,11 @@ # shared_inbox_url :string default(""), not null # followers_url :string default(""), not null # protocol :integer default("ostatus"), not null +# memorial :boolean default(FALSE), not null # class Account < ApplicationRecord - MENTION_RE = /(?:^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i + MENTION_RE = /(?<=^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i include AccountAvatar include AccountFinderConcern @@ -152,6 +153,20 @@ class Account < ApplicationRecord ResolveRemoteAccountService.new.call(acct) end + def unsuspend! + transaction do + user&.enable! if local? + update!(suspended: false) + end + end + + def memorialize! + transaction do + user&.disable! if local? + update!(memorial: true) + end + end + def keypair @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key) end diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb index fb695e473..9c98ec2a6 100644 --- a/app/models/account_domain_block.rb +++ b/app/models/account_domain_block.rb @@ -6,8 +6,8 @@ # domain :string # created_at :datetime not null # updated_at :datetime not null -# account_id :integer -# id :integer not null, primary key +# account_id :bigint +# id :bigint not null, primary key # class AccountDomainBlock < ApplicationRecord diff --git a/app/models/account_moderation_note.rb b/app/models/account_moderation_note.rb index 3ac9b1ac1..06f464850 100644 --- a/app/models/account_moderation_note.rb +++ b/app/models/account_moderation_note.rb @@ -3,10 +3,10 @@ # # Table name: account_moderation_notes # -# id :integer not null, primary key +# id :bigint not null, primary key # content :text not null -# account_id :integer not null -# target_account_id :integer not null +# account_id :bigint not null +# target_account_id :bigint not null # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/models/block.rb b/app/models/block.rb index a913782ed..5778f7e90 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -5,9 +5,9 @@ # # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# id :integer not null, primary key -# target_account_id :integer not null +# account_id :bigint not null +# id :bigint not null, primary key +# target_account_id :bigint not null # class Block < ApplicationRecord diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index a68f7c3d8..c41f92581 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -23,7 +23,7 @@ module AccountInteractions def muting_map(target_account_ids, account_id) Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping| mapping[mute.target_account_id] = { - notifications: mute.hide_notifications? + notifications: mute.hide_notifications?, } end end @@ -91,8 +91,7 @@ module AccountInteractions mute = mute_relationships.create_with(hide_notifications: notifications).find_or_create_by!(target_account: other_account) # When toggling a mute between hiding and allowing notifications, the mute will already exist, so the find_or_create_by! call will return the existing Mute without updating the hide_notifications attribute. Therefore, we check that hide_notifications? is what we want and set it if it isn't. if mute.hide_notifications? != notifications - mute.hide_notifications = notifications - mute.save! + mute.update!(hide_notifications: notifications) end end diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 08c1ce945..e08532522 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -3,7 +3,7 @@ # # Table name: conversations # -# id :integer not null, primary key +# id :bigint not null, primary key # uri :string # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/conversation_mute.rb b/app/models/conversation_mute.rb index 8d2399adf..316865bd2 100644 --- a/app/models/conversation_mute.rb +++ b/app/models/conversation_mute.rb @@ -3,9 +3,9 @@ # # Table name: conversation_mutes # -# conversation_id :integer not null -# account_id :integer not null -# id :integer not null, primary key +# conversation_id :bigint not null +# account_id :bigint not null +# id :bigint not null, primary key # class ConversationMute < ApplicationRecord diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 28b6a2b0b..5723ebd5d 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -3,7 +3,7 @@ # # Table name: custom_emojis # -# id :integer not null, primary key +# id :bigint not null, primary key # shortcode :string default(""), not null # domain :string # image_file_name :string @@ -25,6 +25,8 @@ class CustomEmoji < ApplicationRecord :(#{SHORTCODE_RE_FRAGMENT}): (?=[^[:alnum:]:]|$)/x + has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode + has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce -strip' } } validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes } diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 1268290bc..557d0a19c 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -8,7 +8,7 @@ # updated_at :datetime not null # severity :integer default("silence") # reject_media :boolean default(FALSE), not null -# id :integer not null, primary key +# id :bigint not null, primary key # class DomainBlock < ApplicationRecord diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb index 839038bea..2c348197c 100644 --- a/app/models/email_domain_block.rb +++ b/app/models/email_domain_block.rb @@ -3,15 +3,34 @@ # # Table name: email_domain_blocks # -# id :integer not null, primary key -# domain :string not null +# id :bigint not null, primary key +# domain :string default(""), not null # created_at :datetime not null # updated_at :datetime not null # class EmailDomainBlock < ApplicationRecord + before_validation :normalize_domain + + validates :domain, presence: true, uniqueness: true + def self.block?(email) - domain = email.gsub(/.+@([^.]+)/, '\1') + _, domain = email.split('@', 2) + + return true if domain.nil? + + begin + domain = TagManager.instance.normalize_domain(domain) + rescue Addressable::URI::InvalidURIError + return true + end + where(domain: domain).exists? end + + private + + def normalize_domain + self.domain = TagManager.instance.normalize_domain(domain) + end end diff --git a/app/models/favourite.rb b/app/models/favourite.rb index d28d5c05b..f611aa6a9 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -5,9 +5,9 @@ # # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# id :integer not null, primary key -# status_id :integer not null +# account_id :bigint not null +# id :bigint not null, primary key +# status_id :bigint not null # class Favourite < ApplicationRecord diff --git a/app/models/follow.rb b/app/models/follow.rb index a8ddcb7f0..ea00a377a 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -5,9 +5,9 @@ # # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# id :integer not null, primary key -# target_account_id :integer not null +# account_id :bigint not null +# id :bigint not null, primary key +# target_account_id :bigint not null # show_reblogs :boolean default(TRUE), not null # diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 1a1c52382..962b61411 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -5,9 +5,9 @@ # # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# id :integer not null, primary key -# target_account_id :integer not null +# account_id :bigint not null +# id :bigint not null, primary key +# target_account_id :bigint not null # show_reblogs :boolean default(TRUE), not null # @@ -28,7 +28,5 @@ class FollowRequest < ApplicationRecord destroy! end - def reject! - destroy! - end + alias reject! destroy! end diff --git a/app/models/import.rb b/app/models/import.rb index 8ae7e3a46..6f1278556 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -11,8 +11,8 @@ # data_content_type :string # data_file_size :integer # data_updated_at :datetime -# account_id :integer not null -# id :integer not null, primary key +# account_id :bigint not null +# id :bigint not null, primary key # class Import < ApplicationRecord diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index f6c8879c5..7a553f703 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -3,14 +3,14 @@ # # Table name: media_attachments # -# id :integer not null, primary key -# status_id :integer +# id :bigint not null, primary key +# status_id :bigint # file_file_name :string # file_content_type :string # file_file_size :integer # file_updated_at :datetime # remote_url :string default(""), not null -# account_id :integer +# account_id :bigint # created_at :datetime not null # updated_at :datetime not null # shortcode :string diff --git a/app/models/mention.rb b/app/models/mention.rb index 3700c781c..fc089d365 100644 --- a/app/models/mention.rb +++ b/app/models/mention.rb @@ -3,11 +3,11 @@ # # Table name: mentions # -# status_id :integer +# status_id :bigint # created_at :datetime not null # updated_at :datetime not null -# account_id :integer -# id :integer not null, primary key +# account_id :bigint +# id :bigint not null, primary key # class Mention < ApplicationRecord diff --git a/app/models/mute.rb b/app/models/mute.rb index bcd3d247c..74b445c0b 100644 --- a/app/models/mute.rb +++ b/app/models/mute.rb @@ -3,11 +3,11 @@ # # Table name: mutes # +# id :bigint not null, primary key # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# id :integer not null, primary key -# target_account_id :integer not null +# account_id :bigint not null +# target_account_id :bigint not null # hide_notifications :boolean default(TRUE), not null # diff --git a/app/models/notification.rb b/app/models/notification.rb index 0a5d987cf..c88af9021 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -3,13 +3,13 @@ # # Table name: notifications # -# id :integer not null, primary key -# account_id :integer -# activity_id :integer +# id :bigint not null, primary key +# account_id :bigint +# activity_id :bigint # activity_type :string # created_at :datetime not null # updated_at :datetime not null -# from_account_id :integer +# from_account_id :bigint # class Notification < ApplicationRecord diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index e2bf65d94..63c04b410 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -3,7 +3,7 @@ # # Table name: preview_cards # -# id :integer not null, primary key +# id :bigint not null, primary key # url :string default(""), not null # title :string default(""), not null # description :string default(""), not null diff --git a/app/models/report.rb b/app/models/report.rb index bffb42b48..99c90b7dd 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -8,10 +8,10 @@ # action_taken :boolean default(FALSE), not null # created_at :datetime not null # updated_at :datetime not null -# account_id :integer not null -# action_taken_by_account_id :integer -# id :integer not null, primary key -# target_account_id :integer not null +# account_id :bigint not null +# action_taken_by_account_id :bigint +# id :bigint not null, primary key +# target_account_id :bigint not null # class Report < ApplicationRecord diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb index c1645223b..59565f877 100644 --- a/app/models/session_activation.rb +++ b/app/models/session_activation.rb @@ -3,25 +3,25 @@ # # Table name: session_activations # -# id :integer not null, primary key -# user_id :integer not null +# id :bigint not null, primary key +# user_id :bigint not null # session_id :string not null # created_at :datetime not null # updated_at :datetime not null # user_agent :string default(""), not null # ip :inet -# access_token_id :integer -# web_push_subscription_id :integer +# access_token_id :bigint +# web_push_subscription_id :bigint # -# id :integer not null, primary key -# user_id :integer not null +# id :bigint not null, primary key +# user_id :bigint not null # session_id :string not null # created_at :datetime not null # updated_at :datetime not null # user_agent :string default(""), not null # ip :inet -# access_token_id :integer +# access_token_id :bigint # class SessionActivation < ApplicationRecord diff --git a/app/models/setting.rb b/app/models/setting.rb index a14f156a1..be68d3123 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -8,8 +8,8 @@ # thing_type :string # created_at :datetime # updated_at :datetime -# id :integer not null, primary key -# thing_id :integer +# id :bigint not null, primary key +# thing_id :bigint # class Setting < RailsSettings::Base diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb index 8ffdc8313..ba2ca777b 100644 --- a/app/models/site_upload.rb +++ b/app/models/site_upload.rb @@ -3,7 +3,7 @@ # # Table name: site_uploads # -# id :integer not null, primary key +# id :bigint not null, primary key # var :string default(""), not null # file_file_name :string # file_content_type :string diff --git a/app/models/status.rb b/app/models/status.rb index d78a921b5..1dda6594e 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -3,25 +3,25 @@ # # Table name: statuses # -# id :integer not null, primary key +# id :bigint not null, primary key # uri :string -# account_id :integer not null +# account_id :bigint not null # text :text default(""), not null # created_at :datetime not null # updated_at :datetime not null -# in_reply_to_id :integer -# reblog_of_id :integer +# in_reply_to_id :bigint +# reblog_of_id :bigint # url :string # sensitive :boolean default(FALSE), not null # visibility :integer default("public"), not null -# in_reply_to_account_id :integer -# application_id :integer +# in_reply_to_account_id :bigint +# application_id :bigint # spoiler_text :text default(""), not null # reply :boolean default(FALSE), not null # favourites_count :integer default(0), not null # reblogs_count :integer default(0), not null # language :string -# conversation_id :integer +# conversation_id :bigint # local :boolean # diff --git a/app/models/status_pin.rb b/app/models/status_pin.rb index a72c19750..5795d07bf 100644 --- a/app/models/status_pin.rb +++ b/app/models/status_pin.rb @@ -3,9 +3,9 @@ # # Table name: status_pins # -# id :integer not null, primary key -# account_id :integer not null -# status_id :integer not null +# id :bigint not null, primary key +# account_id :bigint not null +# status_id :bigint not null # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb index 720cd518c..2b812d826 100644 --- a/app/models/stream_entry.rb +++ b/app/models/stream_entry.rb @@ -3,13 +3,13 @@ # # Table name: stream_entries # -# activity_id :integer +# activity_id :bigint # activity_type :string # created_at :datetime not null # updated_at :datetime not null # hidden :boolean default(FALSE), not null -# account_id :integer -# id :integer not null, primary key +# account_id :bigint +# id :bigint not null, primary key # class StreamEntry < ApplicationRecord diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 39860196b..bc50c5317 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -11,8 +11,8 @@ # updated_at :datetime not null # last_successful_delivery_at :datetime # domain :string -# account_id :integer not null -# id :integer not null, primary key +# account_id :bigint not null +# id :bigint not null, primary key # class Subscription < ApplicationRecord diff --git a/app/models/tag.rb b/app/models/tag.rb index 0fa08e157..6ebaf1145 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -3,7 +3,7 @@ # # Table name: tags # -# id :integer not null, primary key +# id :bigint not null, primary key # name :string default(""), not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/user.rb b/app/models/user.rb index 325e27f44..326b871a1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,9 +3,8 @@ # # Table name: users # -# id :integer not null, primary key +# id :bigint not null, primary key # email :string default(""), not null -# account_id :integer not null # created_at :datetime not null # updated_at :datetime not null # encrypted_password :string default(""), not null @@ -31,10 +30,14 @@ # last_emailed_at :datetime # otp_backup_codes :string is an Array # filtered_languages :string default([]), not null, is an Array +# account_id :bigint not null +# disabled :boolean default(FALSE), not null +# moderator :boolean default(FALSE), not null # class User < ApplicationRecord include Settings::Extend + ACTIVE_DURATION = 14.days devise :registerable, :recoverable, @@ -51,8 +54,10 @@ class User < ApplicationRecord validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates_with BlacklistedEmailValidator, if: :email_changed? - scope :recent, -> { order(id: :desc) } - scope :admins, -> { where(admin: true) } + scope :recent, -> { order(id: :desc) } + scope :admins, -> { where(admin: true) } + scope :moderators, -> { where(moderator: true) } + scope :staff, -> { admins.or(moderators) } scope :confirmed, -> { where.not(confirmed_at: nil) } scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) } scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended: false }) } @@ -68,54 +73,71 @@ class User < ApplicationRecord has_many :session_activations, dependent: :destroy + delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, + :reduce_motion, :system_font_ui, :noindex, :theme, + to: :settings, prefix: :setting, allow_nil: false + def confirmed? confirmed_at.present? end - def disable_two_factor! - self.otp_required_for_login = false - otp_backup_codes&.clear - save! - end - - def setting_default_privacy - settings.default_privacy || (account.locked? ? 'private' : 'public') + def staff? + admin? || moderator? end - def setting_default_sensitive - settings.default_sensitive + def role + if admin? + 'admin' + elsif moderator? + 'moderator' + else + 'user' + end end - def setting_unfollow_modal - settings.unfollow_modal + def disable! + update!(disabled: true, + last_sign_in_at: current_sign_in_at, + current_sign_in_at: nil) end - def setting_boost_modal - settings.boost_modal + def enable! + update!(disabled: false) end - def setting_delete_modal - settings.delete_modal + def confirm! + skip_confirmation! + save! end - def setting_auto_play_gif - settings.auto_play_gif + def promote! + if moderator? + update!(moderator: false, admin: true) + elsif !admin? + update!(moderator: true) + end end - def setting_reduce_motion - settings.reduce_motion + def demote! + if admin? + update!(admin: false, moderator: true) + elsif moderator? + update!(moderator: false) + end end - def setting_system_font_ui - settings.system_font_ui + def disable_two_factor! + self.otp_required_for_login = false + otp_backup_codes&.clear + save! end - def setting_noindex - settings.noindex + def active_for_authentication? + super && !disabled? end - def setting_theme - settings.theme + def setting_default_privacy + settings.default_privacy || (account.locked? ? 'private' : 'public') end def token_for_app(a) diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index cb15dfa37..da67e7665 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -3,7 +3,7 @@ # # Table name: web_push_subscriptions # -# id :integer not null, primary key +# id :bigint not null, primary key # endpoint :string not null # key_p256dh :string not null # key_auth :string not null diff --git a/app/models/web/setting.rb b/app/models/web/setting.rb index 1b0bfb2b7..6d08c4d35 100644 --- a/app/models/web/setting.rb +++ b/app/models/web/setting.rb @@ -6,8 +6,8 @@ # data :json # created_at :datetime not null # updated_at :datetime not null -# id :integer not null, primary key -# user_id :integer +# id :bigint not null, primary key +# user_id :bigint # class Web::Setting < ApplicationRecord diff --git a/app/policies/account_moderation_note_policy.rb b/app/policies/account_moderation_note_policy.rb new file mode 100644 index 000000000..885411a5b --- /dev/null +++ b/app/policies/account_moderation_note_policy.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AccountModerationNotePolicy < ApplicationPolicy + def create? + staff? + end + + def destroy? + admin? || owner? + end + + private + + def owner? + record.account_id == current_account&.id + end +end diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb new file mode 100644 index 000000000..85e2c8419 --- /dev/null +++ b/app/policies/account_policy.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class AccountPolicy < ApplicationPolicy + def index? + staff? + end + + def show? + staff? + end + + def suspend? + staff? && !record.user&.staff? + end + + def unsuspend? + staff? + end + + def silence? + staff? && !record.user&.staff? + end + + def unsilence? + staff? + end + + def redownload? + admin? + end + + def subscribe? + admin? + end + + def unsubscribe? + admin? + end + + def memorialize? + admin? && !record.user&.admin? + end +end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb new file mode 100644 index 000000000..3e617001f --- /dev/null +++ b/app/policies/application_policy.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class ApplicationPolicy + attr_reader :current_account, :record + + def initialize(current_account, record) + @current_account = current_account + @record = record + end + + delegate :admin?, :moderator?, :staff?, to: :current_user, allow_nil: true + + private + + def current_user + current_account&.user + end +end diff --git a/app/policies/custom_emoji_policy.rb b/app/policies/custom_emoji_policy.rb new file mode 100644 index 000000000..a8c3cbc73 --- /dev/null +++ b/app/policies/custom_emoji_policy.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class CustomEmojiPolicy < ApplicationPolicy + def index? + staff? + end + + def create? + admin? + end + + def update? + admin? + end + + def copy? + admin? + end + + def enable? + staff? + end + + def disable? + staff? + end + + def destroy? + admin? + end +end diff --git a/app/policies/domain_block_policy.rb b/app/policies/domain_block_policy.rb new file mode 100644 index 000000000..47c0a81af --- /dev/null +++ b/app/policies/domain_block_policy.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class DomainBlockPolicy < ApplicationPolicy + def index? + admin? + end + + def show? + admin? + end + + def create? + admin? + end + + def destroy? + admin? + end +end diff --git a/app/policies/email_domain_block_policy.rb b/app/policies/email_domain_block_policy.rb new file mode 100644 index 000000000..5a75ee183 --- /dev/null +++ b/app/policies/email_domain_block_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class EmailDomainBlockPolicy < ApplicationPolicy + def index? + admin? + end + + def create? + admin? + end + + def destroy? + admin? + end +end diff --git a/app/policies/instance_policy.rb b/app/policies/instance_policy.rb new file mode 100644 index 000000000..d1956e2de --- /dev/null +++ b/app/policies/instance_policy.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class InstancePolicy < ApplicationPolicy + def index? + admin? + end + + def resubscribe? + admin? + end +end diff --git a/app/policies/report_policy.rb b/app/policies/report_policy.rb new file mode 100644 index 000000000..95b5c30c8 --- /dev/null +++ b/app/policies/report_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ReportPolicy < ApplicationPolicy + def update? + staff? + end + + def index? + staff? + end + + def show? + staff? + end +end diff --git a/app/policies/settings_policy.rb b/app/policies/settings_policy.rb new file mode 100644 index 000000000..2dcb79f51 --- /dev/null +++ b/app/policies/settings_policy.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class SettingsPolicy < ApplicationPolicy + def update? + admin? + end + + def show? + admin? + end +end diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index f4a5e7c6c..8e0c1eef1 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -1,22 +1,19 @@ # frozen_string_literal: true -class StatusPolicy - attr_reader :account, :status - - def initialize(account, status) - @account = account - @status = status +class StatusPolicy < ApplicationPolicy + def index? + staff? end def show? return false if local_only? && account.nil? if direct? - owned? || status.mentions.where(account: account).exists? + owned? || record.mentions.where(account: current_account).exists? elsif private? - owned? || account&.following?(status.account) || status.mentions.where(account: account).exists? + owned? || current_account&.following?(author) || record.mentions.where(account: current_account).exists? else - account.nil? || !status.account.blocking?(account) + current_account.nil? || !author.blocking?(current_account) end end @@ -25,30 +22,34 @@ class StatusPolicy end def destroy? - admin? || owned? + staff? || owned? end alias unreblog? destroy? - private - - def admin? - account&.user&.admin? + def update? + staff? end + private + def direct? - status.direct_visibility? + record.direct_visibility? end def owned? - status.account.id == account&.id + author.id == current_account&.id end def private? - status.private_visibility? + record.private_visibility? + end + + def author + record.account end def local_only? - status.local_only? + record.local_only? end end diff --git a/app/policies/subscription_policy.rb b/app/policies/subscription_policy.rb new file mode 100644 index 000000000..ac9a8a6c4 --- /dev/null +++ b/app/policies/subscription_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class SubscriptionPolicy < ApplicationPolicy + def index? + admin? + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 000000000..aae207d06 --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class UserPolicy < ApplicationPolicy + def reset_password? + staff? && !record.staff? + end + + def disable_2fa? + admin? && !record.staff? + end + + def confirm? + staff? && !record.confirmed? + end + + def enable? + admin? + end + + def disable? + admin? && !record.admin? + end + + def promote? + admin? && promoteable? + end + + def demote? + admin? && !record.admin? && demoteable? + end + + private + + def promoteable? + !record.staff? || !record.admin? + end + + def demoteable? + record.staff? + end +end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 1f5ee789a..787c5add3 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -7,7 +7,7 @@ class InitialStateSerializer < ActiveModel::Serializer has_many :custom_emojis, serializer: REST::CustomEmojiSerializer def custom_emojis - CustomEmoji.local + CustomEmoji.local.where(disabled: false) end def meta diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index e2a89a87c..8d7b7a17c 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -16,7 +16,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService return if actor_id.nil? || !trustworthy_attribution?(@json['id'], actor_id) actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account) - actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id, id: true) if actor.nil? + actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id, id: true) if actor.nil? || needs_update(actor) return if actor.suspended? @@ -44,4 +44,8 @@ class ActivityPub::FetchRemoteStatusService < BaseService def expected_type? %w(Note Article).include? @json['type'] end + + def needs_update(actor) + actor.possibly_stale? + end end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 3fa3f152c..d5960c3ad 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -36,17 +36,58 @@ class NotifyService < BaseService false end + def following_sender? + return @following_sender if defined?(@following_sender) + @following_sender = @recipient.following?(@notification.from_account) || @recipient.requested?(@notification.from_account) + end + + def optional_non_follower? + @recipient.user.settings.interactions['must_be_follower'] && !@notification.from_account.following?(@recipient) + end + + def optional_non_following? + @recipient.user.settings.interactions['must_be_following'] && !following_sender? + end + + def direct_message? + @notification.type == :mention && @notification.target_status.direct_visibility? + end + + def response_to_recipient? + @notification.target_status.in_reply_to_account_id == @recipient.id + end + + def optional_non_following_and_direct? + direct_message? && + @recipient.user.settings.interactions['must_be_following_dm'] && + !following_sender? && + !response_to_recipient? + end + + def hellbanned? + @notification.from_account.silenced? && !following_sender? + end + + def from_self? + @recipient.id == @notification.from_account.id + end + + def domain_blocking? + @recipient.domain_blocking?(@notification.from_account.domain) && !following_sender? + end + def blocked? - blocked = @recipient.suspended? # Skip if the recipient account is suspended anyway - blocked ||= @recipient.id == @notification.from_account.id # Skip for interactions with self - blocked ||= @recipient.domain_blocking?(@notification.from_account.domain) && !@recipient.following?(@notification.from_account) # Skip for domain blocked accounts - blocked ||= @recipient.blocking?(@notification.from_account) # Skip for blocked accounts + blocked = @recipient.suspended? # Skip if the recipient account is suspended anyway + blocked ||= from_self? # Skip for interactions with self + blocked ||= domain_blocking? # Skip for domain blocked accounts + blocked ||= @recipient.blocking?(@notification.from_account) # Skip for blocked accounts blocked ||= @recipient.muting_notifications?(@notification.from_account) - blocked ||= (@notification.from_account.silenced? && !@recipient.following?(@notification.from_account)) # Hellban - blocked ||= (@recipient.user.settings.interactions['must_be_follower'] && !@notification.from_account.following?(@recipient)) # Options - blocked ||= (@recipient.user.settings.interactions['must_be_following'] && !@recipient.following?(@notification.from_account)) # Options + blocked ||= hellbanned? # Hellban + blocked ||= optional_non_follower? # Options + blocked ||= optional_non_following? # Options + blocked ||= optional_non_following_and_direct? # Options blocked ||= conversation_muted? - blocked ||= send("blocked_#{@notification.type}?") # Type-dependent filters + blocked ||= send("blocked_#{@notification.type}?") # Type-dependent filters blocked end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index d1b8f42c7..974c586f2 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -74,11 +74,11 @@ class PostStatusService < BaseService end def process_mentions_service - @process_mentions_service ||= ProcessMentionsService.new + ProcessMentionsService.new end def process_hashtags_service - @process_hashtags_service ||= ProcessHashtagsService.new + ProcessHashtagsService.new end def redis diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index 1c3eea369..a229d4ff8 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -10,23 +10,26 @@ class ProcessMentionsService < BaseService def call(status) return unless status.local? - status.text.scan(Account::MENTION_RE).each do |match| - username, domain = match.first.split('@') - mentioned_account = Account.find_remote(username, domain) - - if mentioned_account.nil? && !domain.nil? - begin - mentioned_account = follow_remote_account_service.call(match.first.to_s) - rescue Goldfinger::Error, HTTP::Error - mentioned_account = nil - end + status.text = status.text.gsub(Account::MENTION_RE) do |match| + begin + mentioned_account = resolve_remote_account_service.call($1) + rescue Goldfinger::Error, HTTP::Error + mentioned_account = nil end - next if mentioned_account.nil? + if mentioned_account.nil? + username, domain = match.first.split('@') + mentioned_account = Account.find_remote(username, domain) + end + + next match if mentioned_account.nil? || (!mentioned_account.local? && mentioned_account.ostatus? && status.stream_entry.hidden?) mentioned_account.mentions.where(status: status).first_or_create(status: status) + "@#{mentioned_account.acct}" end + status.save! + status.mentions.includes(:account).each do |mention| create_notification(status, mention) end @@ -54,7 +57,7 @@ class ProcessMentionsService < BaseService ).as_json).sign!(status.account)) end - def follow_remote_account_service - @follow_remote_account_service ||= ResolveRemoteAccountService.new + def resolve_remote_account_service + ResolveRemoteAccountService.new end end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 983c5495b..5b37ba9ba 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -1,22 +1,27 @@ # frozen_string_literal: true class SuspendAccountService < BaseService - def call(account, remove_user = false) + def call(account, options = {}) @account = account + @options = options - purge_user if remove_user - purge_profile - purge_content - unsubscribe_push_subscribers + purge_user! + purge_profile! + purge_content! + unsubscribe_push_subscribers! end private - def purge_user - @account.user.destroy + def purge_user! + if @options[:remove_user] + @account.user&.destroy + else + @account.user&.disable! + end end - def purge_content + def purge_content! @account.statuses.reorder(nil).find_in_batches do |statuses| BatchedRemoveStatusService.new.call(statuses) end @@ -33,7 +38,7 @@ class SuspendAccountService < BaseService end end - def purge_profile + def purge_profile! @account.suspended = true @account.display_name = '' @account.note = '' @@ -42,7 +47,7 @@ class SuspendAccountService < BaseService @account.save! end - def unsubscribe_push_subscribers + def unsubscribe_push_subscribers! destroy_all(@account.subscriptions) end diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index 5b504912d..47ee02cf0 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -1,22 +1,23 @@ - processed_bio = FrontmatterHandler.instance.process_bio Formatter.instance.simplified_format account .card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" } .card__illustration - - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) - .controls - - if current_account.following?(account) - = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do - = fa_icon 'user-times' - = t('accounts.unfollow') - - else - = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do - = fa_icon 'user-plus' - = t('accounts.follow') - - elsif !user_signed_in? - .controls - .remote-follow - = link_to account_remote_follow_path(account), class: 'icon-button' do - = fa_icon 'user-plus' - = t('accounts.remote_follow') + - unless account.memorial? + - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) + .controls + - if current_account.following?(account) + = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do + = fa_icon 'user-times' + = t('accounts.unfollow') + - else + = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do + = fa_icon 'user-plus' + = t('accounts.follow') + - elsif !user_signed_in? + .controls + .remote-follow + = link_to account_remote_follow_path(account), class: 'icon-button' do + = fa_icon 'user-plus' + = t('accounts.remote_follow') .avatar= image_tag account.avatar.url(:original), class: 'u-photo' diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 6c90b2c04..fd8ad5530 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -12,7 +12,9 @@ = opengraph 'og:type', 'profile' = render 'og', account: @account, url: short_account_url(@account, only_path: false) -- if show_landing_strip? +- if @account.memorial? + .memoriam-strip= t('in_memoriam_html') +- elsif show_landing_strip? = render partial: 'shared/landing_strip', locals: { account: @account } .h-feed diff --git a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml index 4651630e9..6761a4319 100644 --- a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml +++ b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml @@ -7,4 +7,4 @@ %time.formatted{ datetime: account_moderation_note.created_at.iso8601, title: l(account_moderation_note.created_at) } = l account_moderation_note.created_at %td - = link_to t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete + = link_to t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note) diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 1b56a3a31..27a0682d8 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -42,7 +42,7 @@ - if params[key].present? = hidden_field_tag key, params[key] - - %i(username display_name email ip).each do |key| + - %i(username by_domain display_name email ip).each do |key| .input.string.optional = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.accounts.#{key}") diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 1f5c8fcf5..f49594828 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -17,7 +17,20 @@ - if @account.local? %tr %th= t('admin.accounts.email') - %td= @account.user_email + %td + = @account.user_email + + - if @account.user_confirmed? + = fa_icon('check') + %tr + %th= t('admin.accounts.login_status') + %td + - if @account.user&.disabled? + = t('admin.accounts.disabled') + = table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post if can?(:enable, @account.user) + - else + = t('admin.accounts.enabled') + = table_link_to 'lock', t('admin.accounts.disable'), disable_admin_account_path(@account.id), method: :post if can?(:disable, @account.user) %tr %th= t('admin.accounts.most_recent_ip') %td= @account.user_current_sign_in_ip @@ -62,26 +75,28 @@ %div{ style: 'overflow: hidden' } %div{ style: 'float: right' } - if @account.local? - = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' + = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user) - if @account.user&.otp_required_for_login? - = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' + = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user) + - unless @account.memorial? + = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:memorialize, @account) - else - = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' + = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) %div{ style: 'float: left' } - if @account.silenced? - = link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button' + = link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button' if can?(:unsilence, @account) - else - = link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button' + = link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button' if can?(:silence, @account) - if @account.local? - unless @account.user_confirmed? - = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' + = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user) - if @account.suspended? - = link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button' + = link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button' if can?(:unsuspend, @account) - else - = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' + = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:suspend, @account) - unless @account.local? %hr @@ -107,9 +122,9 @@ %div{ style: 'overflow: hidden' } %div{ style: 'float: right' } - = link_to @account.subscribed? ? t('admin.accounts.resubscribe') : t('admin.accounts.subscribe'), subscribe_admin_account_path(@account.id), method: :post, class: 'button' + = link_to @account.subscribed? ? t('admin.accounts.resubscribe') : t('admin.accounts.subscribe'), subscribe_admin_account_path(@account.id), method: :post, class: 'button' if can?(:subscribe, @account) - if @account.subscribed? - = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' + = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account) %hr %h3 ActivityPub @@ -130,6 +145,20 @@ %th= t('admin.accounts.followers_url') %td= link_to @account.followers_url, @account.followers_url +- else + %hr + + .table-wrapper + %table.table + %tbody + %tr + %th= t('admin.accounts.role') + %td + = t("admin.accounts.roles.#{@account.user&.role}") + %td< + = table_link_to 'angle-double-up', t('admin.accounts.promote'), promote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:promote, @account.user) + = table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user) + %hr %h3= t('admin.accounts.moderation_notes') diff --git a/app/views/admin/custom_emojis/_custom_emoji.html.haml b/app/views/admin/custom_emojis/_custom_emoji.html.haml index 399d13bbd..bab34bc8d 100644 --- a/app/views/admin/custom_emojis/_custom_emoji.html.haml +++ b/app/views/admin/custom_emojis/_custom_emoji.html.haml @@ -1,6 +1,6 @@ %tr %td - = image_tag custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:" + = custom_emoji_tag(custom_emoji) %td %samp= ":#{custom_emoji.shortcode}:" %td @@ -15,7 +15,10 @@ - else = table_link_to 'eye-slash', t('admin.custom_emojis.unlisted'), admin_custom_emoji_path(custom_emoji, custom_emoji: { visible_in_picker: true }), method: :patch - else - = table_link_to 'copy', t('admin.custom_emojis.copy'), copy_admin_custom_emoji_path(custom_emoji, page: params[:page]), method: :post + - if custom_emoji.local_counterpart.present? + = link_to safe_join([custom_emoji_tag(custom_emoji.local_counterpart), t('admin.custom_emojis.overwrite')]), copy_admin_custom_emoji_path(custom_emoji, page: params[:page]), method: :post, class: 'table-action-link' + - else + = table_link_to 'copy', t('admin.custom_emojis.copy'), copy_admin_custom_emoji_path(custom_emoji, page: params[:page]), method: :post %td - if custom_emoji.disabled? = table_link_to 'power-off', t('admin.custom_emojis.enable'), enable_admin_custom_emoji_path(custom_emoji), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index cba2bbbd4..878ffb032 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -3,9 +3,6 @@ %link{ href: asset_pack_path('features/compose.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/ %link{ href: asset_pack_path('features/home_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/ %link{ href: asset_pack_path('features/notifications.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/ - %link{ href: asset_pack_path('features/community_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/ - %link{ href: asset_pack_path('features/public_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/ - %link{ href: asset_pack_path('features/direct_timeline.js'), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/ %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key} %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index b1fd9ef40..24b74c787 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -29,7 +29,6 @@ = yield :header_tags - body_classes ||= @body_classes || '' - - body_classes += ' reduce-motion' if current_account&.user&.setting_reduce_motion - body_classes += ' system-font' if current_account&.user&.setting_system_font_ui %body{ class: add_rtl_body_class(body_classes) } diff --git a/app/views/settings/notifications/show.html.haml b/app/views/settings/notifications/show.html.haml index 80cd615c7..b718b62df 100644 --- a/app/views/settings/notifications/show.html.haml +++ b/app/views/settings/notifications/show.html.haml @@ -11,7 +11,7 @@ = ff.input :reblog, as: :boolean, wrapper: :with_label = ff.input :favourite, as: :boolean, wrapper: :with_label = ff.input :mention, as: :boolean, wrapper: :with_label - + .fields-group = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| = ff.input :digest, as: :boolean, wrapper: :with_label @@ -20,6 +20,7 @@ = f.simple_fields_for :interactions, hash_to_object(current_user.settings.interactions) do |ff| = ff.input :must_be_follower, as: :boolean, wrapper: :with_label = ff.input :must_be_following, as: :boolean, wrapper: :with_label + = ff.input :must_be_following_dm, as: :boolean, wrapper: :with_label .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/user_mailer/confirmation_instructions.pt-BR.html.erb b/app/views/user_mailer/confirmation_instructions.pt-BR.html.erb index 80edcfda7..0be16d994 100644 --- a/app/views/user_mailer/confirmation_instructions.pt-BR.html.erb +++ b/app/views/user_mailer/confirmation_instructions.pt-BR.html.erb @@ -1,6 +1,6 @@ <p>Boas vindas, <%= @resource.email %>!</p> -<p>Você acabou de criar uma conta no <%= @instance %>.</p> +<p>Você acabou de criar uma conta na instância <%= @instance %>.</p> <p>Para confirmar o seu cadastro, por favor clique no link a seguir: <br> <%= link_to 'Confirmar cadastro', confirmation_url(@resource, confirmation_token: @token) %> @@ -9,4 +9,4 @@ <p>Atenciosamente,<p> -<p>A equipe do <%= @instance %></p> +<p>A equipe da instância <%= @instance %></p> diff --git a/app/views/user_mailer/confirmation_instructions.pt-BR.text.erb b/app/views/user_mailer/confirmation_instructions.pt-BR.text.erb index 95efb3436..578f7acb5 100644 --- a/app/views/user_mailer/confirmation_instructions.pt-BR.text.erb +++ b/app/views/user_mailer/confirmation_instructions.pt-BR.text.erb @@ -1,6 +1,6 @@ Boas vindas, <%= @resource.email %>! -Você acabou de criar uma conta no <%= @instance %>. +Você acabou de criar uma conta na instância <%= @instance %>. Para confirmar o seu cadastro, por favor clique no link a seguir: <%= confirmation_url(@resource, confirmation_token: @token) %> @@ -9,4 +9,4 @@ Por favor, leia também os nossos termos e condições de uso <%= terms_url %> Atenciosamente, -A equipe do <%= @instance %> +A equipe da instância <%= @instance %> diff --git a/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb b/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb index de2f8b6e0..8a676498a 100644 --- a/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb +++ b/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb @@ -1,10 +1,13 @@ -<p><%= @resource.email %> ,嗨呀!</p> +<p><%= @resource.email %>,你好呀!</p> -<p>你刚刚在 <%= @instance %> 创建了帐号。</p> +<p>你刚刚在 <%= @instance %> 创建了一个帐户呢。</p> -<p>点击下面的链接来完成注册啦 : <br> +<p>点击下面的链接来完成注册啦:<br> <%= link_to '确认帐户', confirmation_url(@resource, confirmation_token: @token) %> -<p>别忘了看看 <%= link_to '使用条款', terms_url %>。</p> +<p>上面的链接按不动?把下面的链接复制到地址栏再试试:<br> +<span><%= confirmation_url(@resource, confirmation_token: @token) %></span> -<p> <%= @instance %> 敬上</p> \ No newline at end of file +<p>记得读一读我们的<%= link_to '使用条款', terms_url %>哦。</p> + +<p>来自 <%= @instance %> 管理团队</p> diff --git a/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb b/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb index d7d4b4b23..25d901f16 100644 --- a/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb +++ b/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb @@ -1,10 +1,10 @@ -<%= @resource.email %> ,嗨呀! +<%= @resource.email %>,你好呀! -你刚刚在 <%= @instance %> 创建了帐号。 +你刚刚在 <%= @instance %> 创建了一个帐户呢。 -点击下面的链接来完成注册啦 : <br> -<%= link_to '确认帐户', confirmation_url(@resource, confirmation_token: @token) %> +点击下面的链接来完成注册啦: +<%= confirmation_url(@resource, confirmation_token: @token) %> -别忘了看看 <%= link_to 'terms and conditions', terms_url %>。 +记得读一读我们的使用条款哦:<%= terms_url %> -<%= @instance %> 敬上 \ No newline at end of file +来自 <%= @instance %> 管理团队 \ No newline at end of file diff --git a/app/views/user_mailer/password_change.pt-BR.html.erb b/app/views/user_mailer/password_change.pt-BR.html.erb index 5f707ba09..a1aaa265e 100644 --- a/app/views/user_mailer/password_change.pt-BR.html.erb +++ b/app/views/user_mailer/password_change.pt-BR.html.erb @@ -1,3 +1,3 @@ <p>Olá, <%= @resource.email %>!</p> -<p>Estamos te contatando para te notificar que a senha senha no <%= @instance %> foi modificada.</p> +<p>Estamos te contatando para te notificar que a sua senha na instância <%= @instance %> foi modificada.</p> diff --git a/app/views/user_mailer/password_change.pt-BR.text.erb b/app/views/user_mailer/password_change.pt-BR.text.erb index d8b76648c..eb7368ba9 100644 --- a/app/views/user_mailer/password_change.pt-BR.text.erb +++ b/app/views/user_mailer/password_change.pt-BR.text.erb @@ -1,3 +1,3 @@ Olá, <%= @resource.email %>! -Estamos te contatando para te notificar que a senha senha no <%= @instance %> foi modificada. +Estamos te contatando para te notificar que a sua senha na instância <%= @instance %> foi modificada. diff --git a/app/views/user_mailer/password_change.zh-cn.html.erb b/app/views/user_mailer/password_change.zh-cn.html.erb index 115030af4..64e8b6b2f 100644 --- a/app/views/user_mailer/password_change.zh-cn.html.erb +++ b/app/views/user_mailer/password_change.zh-cn.html.erb @@ -1,3 +1,3 @@ -<p><%= @resource.email %>,嗨呀!</p> +<p><%= @resource.email %>,你好呀!</p> -<p>这只是一封用来通知你的密码已经被修改的邮件。_(:3」∠)_</p> +<p>提醒一下,你在 <%= @instance %> 上的密码被更改了哦。</p> diff --git a/app/views/user_mailer/password_change.zh-cn.text.erb b/app/views/user_mailer/password_change.zh-cn.text.erb index 5a989d324..dbc065173 100644 --- a/app/views/user_mailer/password_change.zh-cn.text.erb +++ b/app/views/user_mailer/password_change.zh-cn.text.erb @@ -1,3 +1,3 @@ -<%= @resource.email %>,嗨呀! +<%= @resource.email %>,你好呀! -这只是一封用来通知你的密码已经被修改的邮件。_(:3」∠)_ +提醒一下,你在 <%= @instance %> 上的密码被更改了哦。 diff --git a/app/views/user_mailer/reset_password_instructions.oc.html.erb b/app/views/user_mailer/reset_password_instructions.oc.html.erb index 6c775b3a1..92e4b8f8b 100644 --- a/app/views/user_mailer/reset_password_instructions.oc.html.erb +++ b/app/views/user_mailer/reset_password_instructions.oc.html.erb @@ -1,6 +1,6 @@ <p>Bonjorn <%= @resource.email %> !</p> -<p>Qualqu’un a demandat la reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p> +<p>Qualqu’un a demandat la reïnicializacion de vòstre senhal per Mastodon. Podètz realizar la reïnicializacion en clicant sul ligam çai-jos.</p> <p><%= link_to 'Modificar mon senhal', edit_password_url(@resource, reset_password_token: @token) %></p> diff --git a/app/views/user_mailer/reset_password_instructions.oc.text.erb b/app/views/user_mailer/reset_password_instructions.oc.text.erb index 26432d2df..5a5219589 100644 --- a/app/views/user_mailer/reset_password_instructions.oc.text.erb +++ b/app/views/user_mailer/reset_password_instructions.oc.text.erb @@ -1,6 +1,6 @@ Bonjorn <%= @resource.email %> ! -Qualqu’un a demandat la reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p> +Qualqu’un a demandat la reïnicializacion de vòstre senhal per Mastodon. Podètz realizar la reïnicializacion en clicant sul ligam çai-jos.</p> <%= link_to 'Modificar mon senhal', edit_password_url(@resource, reset_password_token: @token) %> diff --git a/app/views/user_mailer/reset_password_instructions.pt-BR.html.erb b/app/views/user_mailer/reset_password_instructions.pt-BR.html.erb index 940438b7c..9b21aae92 100644 --- a/app/views/user_mailer/reset_password_instructions.pt-BR.html.erb +++ b/app/views/user_mailer/reset_password_instructions.pt-BR.html.erb @@ -1,8 +1,8 @@ <p>Olá, <%= @resource.email %>!</p> -<p>Alguém solicitou um link para mudar a sua senha no <%= @instance %>. Você pode fazer isso através do link abaixo:</p> +<p>Alguém solicitou um link para mudar a sua senha na instância <%= @instance %>. Você pode fazer isso através do link abaixo:</p> <p><%= link_to 'Mudar a minha senha', edit_password_url(@resource, reset_password_token: @token) %></p> <p>Se você não solicitou isso, por favor ignore este e-mail.</p> -<p>A senha senha não será modificada até que você acesse o link acima e crie uma nova.</p> +<p>A senha não será modificada até que você acesse o link acima e crie uma nova.</p> diff --git a/app/views/user_mailer/reset_password_instructions.pt-BR.text.erb b/app/views/user_mailer/reset_password_instructions.pt-BR.text.erb index f574fe08f..2abff0c0d 100644 --- a/app/views/user_mailer/reset_password_instructions.pt-BR.text.erb +++ b/app/views/user_mailer/reset_password_instructions.pt-BR.text.erb @@ -1,8 +1,8 @@ Olá, <%= @resource.email %>! -Alguém solicitou um link para mudar a sua senha no <%= @instance %>. Você pode fazer isso através do link abaixo: +Alguém solicitou um link para mudar a sua senha na instância <%= @instance %>. Você pode fazer isso através do link abaixo: <%= edit_password_url(@resource, reset_password_token: @token) %> Se você não solicitou isso, por favor ignore este e-mail. -A senha senha não será modificada até que você acesse o link acima e crie uma nova. +A senha não será modificada até que você acesse o link acima e crie uma nova. diff --git a/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb b/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb index 51e3073f1..124305675 100644 --- a/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb +++ b/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb @@ -1,7 +1,8 @@ -<p><%= @resource.email %> ,嗨呀!!</p> +<p><%= @resource.email %>,你好呀!</p> -<p>有人(但愿是你)请求更改你Mastodon帐户的密码。如果是你的话,请点击下面的链接:</p> +<p>有人想修改你在 <%= @instance %> 上的密码呢。如果你确实想修改密码的话,点击下面的链接吧:</p> -<p><%= link_to '更改密码', edit_password_url(@resource, reset_password_token: @token) %></p> +<p><%= link_to '修改密码', edit_password_url(@resource, reset_password_token: @token) %></p> -<p>如果不是的话,忘了它吧。只有你本人通过上面的链接设置新的密码以后你的新密码才会生效。</p> +<p>如果你不想修改密码的话,还请忽略这封邮件哦。</p> +<p>在你点击上面的链接并修改密码前,你的密码是不会改变的。</p> diff --git a/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb b/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb index 7df590f78..f7cd88847 100644 --- a/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb +++ b/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb @@ -1,7 +1,8 @@ -<%= @resource.email %> ,嗨呀!! +<%= @resource.email %>,你好呀! -有人(但愿是你)请求更改你Mastodon帐户的密码。如果是你的话,请点击下面的链接: +有人想修改你在 <%= @instance %> 上的密码呢。如果你确实想修改密码的话,点击下面的链接吧: -<%= link_to '更改密码', edit_password_url(@resource, reset_password_token: @token) %> +<%= edit_password_url(@resource, reset_password_token: @token) %> -如果不是的话,忘了它吧。只有你本人通过上面的链接设置新的密码以后你的新密码才会生效。 +如果你不想修改密码的话,还请忽略这封邮件哦。 +在你点击上面的链接并修改密码前,你的密码是不会改变的。 diff --git a/app/workers/admin/suspension_worker.rb b/app/workers/admin/suspension_worker.rb index 6338b1130..e41465ccc 100644 --- a/app/workers/admin/suspension_worker.rb +++ b/app/workers/admin/suspension_worker.rb @@ -6,6 +6,6 @@ class Admin::SuspensionWorker sidekiq_options queue: 'pull' def perform(account_id, remove_user = false) - SuspendAccountService.new.call(Account.find(account_id), remove_user) + SuspendAccountService.new.call(Account.find(account_id), remove_user: remove_user) end end diff --git a/app/workers/thread_resolve_worker.rb b/app/workers/thread_resolve_worker.rb index 38287e8e6..c18a778d5 100644 --- a/app/workers/thread_resolve_worker.rb +++ b/app/workers/thread_resolve_worker.rb @@ -3,7 +3,11 @@ class ThreadResolveWorker include Sidekiq::Worker - sidekiq_options queue: 'pull', retry: false + sidekiq_options queue: 'pull', retry: 3 + + sidekiq_retry_in do |count| + 15 + 10 * (count**4) + rand(10 * (count**4)) + end def perform(child_status_id, parent_url) child_status = Status.find(child_status_id) |