diff --git a/.rubocop.yml b/.rubocop.yml index 1033db92d91..b5598b65afb 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -185,3 +185,11 @@ Style/TrailingCommaInHashLiteral: Style/SymbolArray: Enabled: false + +# Reason: Prefer less intendation in conditional assignments +# https://docs.rubocop.org/rubocop/cops_style.html#styleredundantbegin +Style/RedundantBegin: + Enabled: false + +RSpec/NamedSubject: + EnforcedStyle: named_only diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 64a6b6b33ac..4ad266c8c71 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -259,6 +259,7 @@ Metrics/ModuleLength: - 'app/helpers/jsonld_helper.rb' - 'app/helpers/statuses_helper.rb' - 'app/models/concerns/account_interactions.rb' + - 'app/models/concerns/has_user_settings.rb' # Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. Metrics/ParameterLists: diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 94b707771fd..7c7d70fd32c 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -13,7 +13,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController def update @account = current_account UpdateAccountService.new.call(@account, account_params, raise_error: true) - UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params + current_user.update(user_params) if user_params ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end @@ -34,15 +34,17 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController ) end - def user_settings_params + def user_params return nil if params[:source].blank? source_params = params.require(:source) { - 'setting_default_privacy' => source_params.fetch(:privacy, @account.user.setting_default_privacy), - 'setting_default_sensitive' => source_params.fetch(:sensitive, @account.user.setting_default_sensitive), - 'setting_default_language' => source_params.fetch(:language, @account.user.setting_default_language), + settings_attributes: { + default_privacy: source_params.fetch(:privacy, @account.user.setting_default_privacy), + default_sensitive: source_params.fetch(:sensitive, @account.user.setting_default_sensitive), + default_language: source_params.fetch(:language, @account.user.setting_default_language), + }, } end end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index f5d5c12449b..281deb64d17 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -4,8 +4,6 @@ class Settings::PreferencesController < Settings::BaseController def show; end def update - user_settings.update(user_settings_params.to_h) - if current_user.update(user_params) I18n.locale = current_user.locale redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg') @@ -20,43 +18,7 @@ class Settings::PreferencesController < Settings::BaseController settings_preferences_path end - def user_settings - UserSettingsDecorator.new(current_user) - end - def user_params - params.require(:user).permit( - :locale, - chosen_languages: [] - ) - end - - def user_settings_params - params.require(:user).permit( - :setting_default_privacy, - :setting_default_sensitive, - :setting_default_language, - :setting_unfollow_modal, - :setting_boost_modal, - :setting_delete_modal, - :setting_auto_play_gif, - :setting_display_media, - :setting_expand_spoilers, - :setting_reduce_motion, - :setting_disable_swiping, - :setting_system_font_ui, - :setting_noindex, - :setting_theme, - :setting_aggregate_reblogs, - :setting_show_application, - :setting_advanced_layout, - :setting_use_blurhash, - :setting_use_pending_items, - :setting_trends, - :setting_crop_images, - :setting_always_send_emails, - notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag appeal), - interactions: %i(must_be_follower must_be_following must_be_following_dm) - ) + params.require(:user).permit(:locale, chosen_languages: [], settings_attributes: UserSettings.keys) end end diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb deleted file mode 100644 index 5fb7655a9b4..00000000000 --- a/app/lib/user_settings_decorator.rb +++ /dev/null @@ -1,155 +0,0 @@ -# frozen_string_literal: true - -class UserSettingsDecorator - attr_reader :user, :settings - - def initialize(user) - @user = user - end - - def update(settings) - @settings = settings - process_update - end - - private - - def process_update - user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails') - user.settings['interactions'] = merged_interactions if change?('interactions') - user.settings['default_privacy'] = default_privacy_preference if change?('setting_default_privacy') - user.settings['default_sensitive'] = default_sensitive_preference if change?('setting_default_sensitive') - user.settings['default_language'] = default_language_preference if change?('setting_default_language') - user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal') - user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal') - user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal') - user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif') - user.settings['display_media'] = display_media_preference if change?('setting_display_media') - user.settings['expand_spoilers'] = expand_spoilers_preference if change?('setting_expand_spoilers') - user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') - user.settings['disable_swiping'] = disable_swiping_preference if change?('setting_disable_swiping') - user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') - user.settings['noindex'] = noindex_preference if change?('setting_noindex') - user.settings['theme'] = theme_preference if change?('setting_theme') - user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs') - user.settings['show_application'] = show_application_preference if change?('setting_show_application') - user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout') - user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash') - user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items') - user.settings['trends'] = trends_preference if change?('setting_trends') - user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images') - user.settings['always_send_emails'] = always_send_emails_preference if change?('setting_always_send_emails') - end - - def merged_notification_emails - user.settings['notification_emails'].merge coerced_settings('notification_emails').to_h - end - - def merged_interactions - user.settings['interactions'].merge coerced_settings('interactions').to_h - end - - def default_privacy_preference - settings['setting_default_privacy'] - end - - def default_sensitive_preference - boolean_cast_setting 'setting_default_sensitive' - end - - def unfollow_modal_preference - boolean_cast_setting 'setting_unfollow_modal' - end - - def boost_modal_preference - boolean_cast_setting 'setting_boost_modal' - end - - def delete_modal_preference - boolean_cast_setting 'setting_delete_modal' - end - - def system_font_ui_preference - boolean_cast_setting 'setting_system_font_ui' - end - - def auto_play_gif_preference - boolean_cast_setting 'setting_auto_play_gif' - end - - def display_media_preference - settings['setting_display_media'] - end - - def expand_spoilers_preference - boolean_cast_setting 'setting_expand_spoilers' - end - - def reduce_motion_preference - boolean_cast_setting 'setting_reduce_motion' - end - - def disable_swiping_preference - boolean_cast_setting 'setting_disable_swiping' - end - - def noindex_preference - boolean_cast_setting 'setting_noindex' - end - - def show_application_preference - boolean_cast_setting 'setting_show_application' - end - - def theme_preference - settings['setting_theme'] - end - - def default_language_preference - settings['setting_default_language'] - end - - def aggregate_reblogs_preference - boolean_cast_setting 'setting_aggregate_reblogs' - end - - def advanced_layout_preference - boolean_cast_setting 'setting_advanced_layout' - end - - def use_blurhash_preference - boolean_cast_setting 'setting_use_blurhash' - end - - def use_pending_items_preference - boolean_cast_setting 'setting_use_pending_items' - end - - def trends_preference - boolean_cast_setting 'setting_trends' - end - - def crop_images_preference - boolean_cast_setting 'setting_crop_images' - end - - def always_send_emails_preference - boolean_cast_setting 'setting_always_send_emails' - end - - def boolean_cast_setting(key) - ActiveModel::Type::Boolean.new.cast(settings[key]) - end - - def coerced_settings(key) - coerce_values settings.fetch(key, {}) - end - - def coerce_values(params_hash) - params_hash.transform_values { |x| ActiveModel::Type::Boolean.new.cast(x) } - end - - def change?(key) - !settings[key].nil? - end -end diff --git a/app/lib/user_settings_serializer.rb b/app/lib/user_settings_serializer.rb new file mode 100644 index 00000000000..10d1be04d5a --- /dev/null +++ b/app/lib/user_settings_serializer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class UserSettingsSerializer + def self.load(value) + json = begin + if value.blank? + {} + else + Oj.load(value, symbol_keys: true) + end + end + + UserSettings.new(json) + end + + def self.dump(value) + Oj.dump(value.as_json) + end +end diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb new file mode 100644 index 00000000000..b3fa1f683bf --- /dev/null +++ b/app/models/concerns/has_user_settings.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +module HasUserSettings + extend ActiveSupport::Concern + + included do + serialize :settings, UserSettingsSerializer + end + + def settings_attributes=(attributes) + settings.update(attributes) + end + + def prefers_noindex? + settings['noindex'] + end + + def preferred_posting_language + valid_locale_cascade(settings['default_language'], locale, I18n.locale) + end + + def setting_auto_play_gif + settings['web.auto_play'] + end + + def setting_default_sensitive + settings['default_sensitive'] + end + + def setting_unfollow_modal + settings['web.unfollow_modal'] + end + + def setting_boost_modal + settings['web.reblog_modal'] + end + + def setting_delete_modal + settings['web.delete_modal'] + end + + def setting_reduce_motion + settings['web.reduce_motion'] + end + + def setting_system_font_ui + settings['web.use_system_font'] + end + + def setting_noindex + settings['noindex'] + end + + def setting_theme + settings['theme'] + end + + def setting_display_media + settings['web.display_media'] + end + + def setting_expand_spoilers + settings['web.expand_content_warnings'] + end + + def setting_default_language + settings['default_language'] + end + + def setting_aggregate_reblogs + settings['aggregate_reblogs'] + end + + def setting_show_application + settings['show_application'] + end + + def setting_advanced_layout + settings['web.advanced_layout'] + end + + def setting_use_blurhash + settings['web.use_blurhash'] + end + + def setting_use_pending_items + settings['web.use_pending_items'] + end + + def setting_trends + settings['web.trends'] + end + + def setting_crop_images + settings['web.crop_images'] + end + + def setting_disable_swiping + settings['web.disable_swiping'] + end + + def setting_always_send_emails + settings['always_send_emails'] + end + + def setting_default_privacy + settings['default_privacy'] || (account.locked? ? 'private' : 'public') + end + + def allows_report_emails? + settings['notification_emails.report'] + end + + def allows_pending_account_emails? + settings['notification_emails.pending_account'] + end + + def allows_appeal_emails? + settings['notification_emails.appeal'] + end + + def allows_trends_review_emails? + settings['notification_emails.trends'] + end + + def aggregates_reblogs? + settings['aggregate_reblogs'] + end + + def shows_application? + settings['show_application'] + end + + def show_all_media? + settings['web.display_media'] == 'show_all' + end + + def hide_all_media? + settings['web.display_media'] == 'hide_all' + end +end diff --git a/app/models/user.rb b/app/models/user.rb index d56a9b9ca1a..9b225d75f17 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -39,10 +39,11 @@ # webauthn_id :string # sign_up_ip :inet # role_id :bigint(8) +# settings :text # class User < ApplicationRecord - self.ignored_columns = %w( + self.ignored_columns += %w( remember_created_at remember_token current_sign_in_ip @@ -51,9 +52,9 @@ class User < ApplicationRecord filtered_languages ) - include Settings::Extend include Redisable include LanguagesHelper + include HasUserSettings # The home and list feeds will be stored in Redis for this amount # of time, and status fan-out to followers will include only people @@ -132,13 +133,6 @@ class User < ApplicationRecord has_many :session_activations, dependent: :destroy - delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, - :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, - :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, - :advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images, - :disable_swiping, :always_send_emails, - to: :settings, prefix: :setting, allow_nil: false - delegate :can?, to: :role attr_reader :invite_code @@ -302,42 +296,6 @@ class User < ApplicationRecord save! end - def prefers_noindex? - setting_noindex - end - - def preferred_posting_language - valid_locale_cascade(settings.default_language, locale, I18n.locale) - end - - def setting_default_privacy - settings.default_privacy || (account.locked? ? 'private' : 'public') - end - - def allows_report_emails? - settings.notification_emails['report'] - end - - def allows_pending_account_emails? - settings.notification_emails['pending_account'] - end - - def allows_appeal_emails? - settings.notification_emails['appeal'] - end - - def allows_trends_review_emails? - settings.notification_emails['trending_tag'] - end - - def aggregates_reblogs? - @aggregates_reblogs ||= settings.aggregate_reblogs - end - - def shows_application? - @shows_application ||= settings.show_application - end - def token_for_app(app) return nil if app.nil? || app.owner != self @@ -417,14 +375,6 @@ class User < ApplicationRecord send_reset_password_instructions end - def show_all_media? - setting_display_media == 'show_all' - end - - def hide_all_media? - setting_display_media == 'hide_all' - end - protected def send_devise_notification(notification, *args, **kwargs) @@ -494,7 +444,8 @@ class User < ApplicationRecord def sanitize_languages return if chosen_languages.nil? - chosen_languages.reject!(&:blank?) + chosen_languages.compact_blank! + self.chosen_languages = nil if chosen_languages.empty? end diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb new file mode 100644 index 00000000000..2c025d6c564 --- /dev/null +++ b/app/models/user_settings.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +class UserSettings + class Error < StandardError; end + class KeyError < Error; end + + include UserSettings::DSL + include UserSettings::Glue + + setting :always_send_emails, default: false + setting :aggregate_reblogs, default: true + setting :theme, default: -> { ::Setting.theme } + setting :noindex, default: -> { ::Setting.noindex } + setting :show_application, default: true + setting :default_language, default: nil + setting :default_sensitive, default: false + setting :default_privacy, default: nil + + namespace :web do + setting :crop_images, default: true + setting :advanced_layout, default: false + setting :trends, default: true + setting :use_blurhash, default: true + setting :use_pending_items, default: false + setting :use_system_font, default: false + setting :disable_swiping, default: false + setting :delete_modal, default: true + setting :reblog_modal, default: false + setting :unfollow_modal, default: true + setting :reduce_motion, default: false + setting :expand_content_warnings, default: false + setting :display_media, default: 'default', in: %w(default show_all hide_all) + setting :auto_play, default: false + end + + namespace :notification_emails do + setting :follow, default: true + setting :reblog, default: false + setting :favourite, default: false + setting :mention, default: true + setting :follow_request, default: true + setting :report, default: true + setting :pending_account, default: true + setting :trends, default: true + setting :appeal, default: true + end + + namespace :interactions do + setting :must_be_follower, default: false + setting :must_be_following, default: false + setting :must_be_following_dm, default: false + end + + def initialize(original_hash) + @original_hash = original_hash || {} + end + + def [](key) + key = key.to_sym + + raise KeyError, "Undefined setting: #{key}" unless self.class.definition_for?(key) + + if @original_hash.key?(key) + @original_hash[key] + else + self.class.definition_for(key).default_value + end + end + + def []=(key, value) + key = key.to_sym + + raise KeyError, "Undefined setting: #{key}" unless self.class.definition_for?(key) + + typecast_value = self.class.definition_for(key).type_cast(value) + + if typecast_value.nil? + @original_hash.delete(key) + else + @original_hash[key] = typecast_value + end + end + + def update(params) + params.each do |k, v| + self[k] = v unless v.nil? + end + end + + keys.each do |key| + define_method(key) do + self[key] + end + end + + def as_json + @original_hash + end +end diff --git a/app/models/user_settings/dsl.rb b/app/models/user_settings/dsl.rb new file mode 100644 index 00000000000..26238bbbe81 --- /dev/null +++ b/app/models/user_settings/dsl.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module UserSettings::DSL + module ClassMethods + def setting(key, options = {}) + @definitions ||= {} + + UserSettings::Setting.new(key, options).tap do |s| + @definitions[s.key] = s + end + end + + def namespace(key, &block) + @definitions ||= {} + + UserSettings::Namespace.new(key).configure(&block).tap do |n| + @definitions.merge!(n.definitions) + end + end + + def keys + @definitions.keys + end + + def definition_for(key) + @definitions[key.to_sym] + end + + def definition_for?(key) + @definitions.key?(key.to_sym) + end + end + + def self.included(base) + base.extend ClassMethods + end +end diff --git a/app/models/user_settings/glue.rb b/app/models/user_settings/glue.rb new file mode 100644 index 00000000000..02066a4110e --- /dev/null +++ b/app/models/user_settings/glue.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module UserSettings::Glue + def to_model + self + end + + def to_key + '' + end + + def persisted? + false + end + + def type_for_attribute(key) + self.class.definition_for(key)&.type + end + + def has_attribute?(key) # rubocop:disable Naming/PredicateName + self.class.definition_for?(key) + end +end diff --git a/app/models/user_settings/namespace.rb b/app/models/user_settings/namespace.rb new file mode 100644 index 00000000000..b8f7e092e80 --- /dev/null +++ b/app/models/user_settings/namespace.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class UserSettings::Namespace + attr_reader :name, :definitions + + def initialize(name) + @name = name.to_sym + @definitions = {} + end + + def configure(&block) + instance_eval(&block) + self + end + + def setting(key, options = {}) + UserSettings::Setting.new(key, options.merge(namespace: name)).tap do |s| + @definitions[s.key] = s + end + end +end diff --git a/app/models/user_settings/setting.rb b/app/models/user_settings/setting.rb new file mode 100644 index 00000000000..c359c593b1a --- /dev/null +++ b/app/models/user_settings/setting.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class UserSettings::Setting + attr_reader :name, :namespace, :in + + def initialize(name, options = {}) + @name = name.to_sym + @default_value = options[:default] + @namespace = options[:namespace] + @in = options[:in] + end + + def default_value + if @default_value.respond_to?(:call) + @default_value.call + else + @default_value + end + end + + def type + if @default_value.is_a?(TrueClass) || @default_value.is_a?(FalseClass) + ActiveModel::Type::Boolean.new + else + ActiveModel::Type::String.new + end + end + + def type_cast(value) + if type.respond_to?(:cast) + type.cast(value) + else + value + end + end + + def to_a + [key, default_value] + end + + def key + if namespace + "#{namespace}.#{name}".to_sym + else + name + end + end +end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 4c7acbcac17..994ca588af5 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -3,6 +3,11 @@ class NotifyService < BaseService include Redisable + NON_EMAIL_TYPES = %i( + admin.report + admin.sign_up + ).freeze + def call(recipient, type, activity) @recipient = recipient @activity = activity @@ -36,11 +41,11 @@ class NotifyService < BaseService end def optional_non_follower? - @recipient.user.settings.interactions['must_be_follower'] && !@notification.from_account.following?(@recipient) + @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? + @recipient.user.settings['interactions.must_be_following'] && !following_sender? end def message? @@ -82,7 +87,7 @@ class NotifyService < BaseService def optional_non_following_and_direct? direct_message? && - @recipient.user.settings.interactions['must_be_following_dm'] && + @recipient.user.settings['interactions.must_be_following_dm'] && !following_sender? && !response_to_recipient? end @@ -171,6 +176,6 @@ class NotifyService < BaseService end def send_email_for_notification_type? - @recipient.user.settings.notification_emails[@notification.type.to_s] + NON_EMAIL_TYPES.exclude?(@notification.type) && @recipient.user.settings["notification_emails.#{@notification.type}"] end end diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index bc23df6473f..5358310e5bd 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -9,57 +9,58 @@ .fields-group.fields-row__column.fields-row__column-6 = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| native_locale_name(locale) }, selected: I18n.locale, hint: false .fields-group.fields-row__column.fields-row__column-6 - = f.input :setting_theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false, hint: false + = f.simple_fields_for :settings, current_user.settings do |ff| + = ff.input :theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false, hint: false - unless I18n.locale == :en .flash-message.translation-prompt #{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: '_blank', rel: 'noopener')} - %h4= t 'appearance.advanced_web_interface' + = f.simple_fields_for :settings, current_user.settings do |ff| + %h4= t 'appearance.advanced_web_interface' - %p.hint= t 'appearance.advanced_web_interface_hint' + %p.hint= t 'appearance.advanced_web_interface_hint' - .fields-group - = f.input :setting_advanced_layout, as: :boolean, wrapper: :with_label, hint: false + .fields-group + = ff.input :'web.advanced_layout', wrapper: :with_label, hint: false, label: I18n.t('simple_form.labels.defaults.setting_advanced_layout') + %h4= t 'appearance.animations_and_accessibility' - %h4= t 'appearance.animations_and_accessibility' + .fields-group + = ff.input :'web.use_pending_items', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_use_pending_items'), hint: I18n.t('simple_form.hints.defaults.setting_use_pending_items') - .fields-group - = f.input :setting_use_pending_items, as: :boolean, wrapper: :with_label + .fields-group + = ff.input :'web.auto_play', wrapper: :with_label, recommended: true, label: I18n.t('simple_form.labels.defaults.setting_auto_play_gif') + = ff.input :'web.reduce_motion', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_reduce_motion') + = ff.input :'web.disable_swiping', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_disable_swiping') + = ff.input :'web.use_system_font', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_system_font_ui') - .fields-group - = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label, recommended: true - = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label - = f.input :setting_disable_swiping, as: :boolean, wrapper: :with_label - = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label + %h4= t 'appearance.toot_layout' - %h4= t 'appearance.toot_layout' + .fields-group + = ff.input :'web.crop_images', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_crop_images') - .fields-group - = f.input :setting_crop_images, as: :boolean, wrapper: :with_label + %h4= t 'appearance.discovery' - %h4= t 'appearance.discovery' + .fields-group + = ff.input :'web.trends', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_trends') - .fields-group - = f.input :setting_trends, as: :boolean, wrapper: :with_label + %h4= t 'appearance.confirmation_dialogs' - %h4= t 'appearance.confirmation_dialogs' + .fields-group + = ff.input :'web.unfollow_modal', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_unfollow_modal') + = ff.input :'web.reblog_modal', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_boost_modal') + = ff.input :'web.delete_modal', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_delete_modal') - .fields-group - = f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label - = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label - = f.input :setting_delete_modal, as: :boolean, wrapper: :with_label + %h4= t 'appearance.sensitive_content' - %h4= t 'appearance.sensitive_content' + .fields-group + = ff.input :'web.display_media', collection: ['default', 'show_all', 'hide_all'],label_method: lambda { |item| t("simple_form.hints.defaults.setting_display_media_#{item}") }, hint: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', wrapper: :with_floating_label, label: I18n.t('simple_form.labels.defaults.setting_display_media') - .fields-group - = f.input :setting_display_media, collection: ['default', 'show_all', 'hide_all'], label_method: lambda { |item| t("simple_form.hints.defaults.setting_display_media_#{item}") }, hint: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', wrapper: :with_floating_label + .fields-group + = ff.input :'web.use_blurhash', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_use_blurhash'), hint: I18n.t('simple_form.hints.defaults.setting_use_blurhash') - .fields-group - = f.input :setting_use_blurhash, as: :boolean, wrapper: :with_label - - .fields-group - = f.input :setting_expand_spoilers, as: :boolean, wrapper: :with_label + .fields-group + = ff.input :'web.expand_content_warnings', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_expand_spoilers') .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index f00dbadd495..cb1ad0886de 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -11,25 +11,25 @@ %p.hint= t 'notifications.email_events_hint' - .fields-group - = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| - = ff.input :follow, as: :boolean, wrapper: :with_label - = ff.input :follow_request, as: :boolean, wrapper: :with_label - = ff.input :reblog, as: :boolean, wrapper: :with_label - = ff.input :favourite, as: :boolean, wrapper: :with_label - = ff.input :mention, as: :boolean, wrapper: :with_label - = ff.input :report, as: :boolean, wrapper: :with_label if current_user.can?(:manage_reports) - = ff.input :appeal, as: :boolean, wrapper: :with_label if current_user.can?(:manage_appeals) - = ff.input :pending_account, as: :boolean, wrapper: :with_label if current_user.can?(:manage_users) - = ff.input :trending_tag, as: :boolean, wrapper: :with_label if current_user.can?(:manage_taxonomies) + = f.simple_fields_for :settings, current_user.settings do |ff| + .fields-group + = ff.input :'notification_emails.follow', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.follow') + = ff.input :'notification_emails.follow_request', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.follow_request') + = ff.input :'notification_emails.reblog', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.reblog') + = ff.input :'notification_emails.favourite', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.favourite') + = ff.input :'notification_emails.mention', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.mention') + = ff.input :'notification_emails.report', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.report') if current_user.can?(:manage_reports) + = ff.input :'notification_emails.appeal', as: :boolean, wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.appeal') if current_user.can?(:manage_appeals) + = ff.input :'notification_emails.pending_account', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.pending_account') if current_user.can?(:manage_users) + = ff.input :'notification_emails.trends', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.trending_tag') if current_user.can?(:manage_taxonomies) - .fields-group - = f.input :setting_always_send_emails, as: :boolean, wrapper: :with_label + .fields-group + = ff.input :always_send_emails, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_always_send_emails'), hint: I18n.t('simple_form.hints.defaults.setting_always_send_emails') %h4= t 'notifications.other_settings' .fields-group - = 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 + = f.simple_fields_for :settings, current_user.settings do |ff| + = ff.input :'interactions.must_be_follower', wrapper: :with_label, label: I18n.t('simple_form.labels.interactions.must_be_follower') + = ff.input :'interactions.must_be_following', wrapper: :with_label, label: I18n.t('simple_form.labels.interactions.must_be_following') + = ff.input :'interactions.must_be_following_dm', wrapper: :with_label, label: I18n.t('simple_form.labels.interactions.must_be_following_dm') diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 44f4af2eba8..6590ec7c212 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -7,26 +7,27 @@ = simple_form_for current_user, url: settings_preferences_other_path, html: { method: :put, id: 'edit_preferences' } do |f| = render 'shared/error_messages', object: current_user - .fields-group - = f.input :setting_noindex, as: :boolean, wrapper: :with_label + = f.simple_fields_for :settings, current_user.settings do |ff| + .fields-group + = ff.input :noindex, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_noindex'), hint: I18n.t('simple_form.hints.defaults.setting_noindex') - .fields-group - = f.input :setting_aggregate_reblogs, as: :boolean, wrapper: :with_label, recommended: true + .fields-group + = ff.input :aggregate_reblogs, wrapper: :with_label, recommended: true, label: I18n.t('simple_form.labels.defaults.setting_aggregate_reblogs'), hint: I18n.t('simple_form.hints.defaults.setting_aggregate_reblogs') - %h4= t 'preferences.posting_defaults' + %h4= t 'preferences.posting_defaults' - .fields-row - .fields-group.fields-row__column.fields-row__column-6 - = f.input :setting_default_privacy, collection: Status.selectable_visibilities, wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false + .fields-row + .fields-group.fields-row__column.fields-row__column-6 + = ff.input :default_privacy, collection: Status.selectable_visibilities, wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_privacy') - .fields-group.fields-row__column.fields-row__column-6 - = f.input :setting_default_language, collection: [nil] + filterable_languages, wrapper: :with_label, label_method: lambda { |locale| locale.nil? ? I18n.t('statuses.default_language') : native_locale_name(locale) }, required: false, include_blank: false, hint: false + .fields-group.fields-row__column.fields-row__column-6 + = ff.input :default_language, collection: [nil] + filterable_languages, wrapper: :with_label, label_method: lambda { |locale| locale.nil? ? I18n.t('statuses.default_language') : native_locale_name(locale) }, required: false, include_blank: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_language') - .fields-group - = f.input :setting_default_sensitive, as: :boolean, wrapper: :with_label + .fields-group + = ff.input :default_sensitive, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_default_sensitive'), hint: I18n.t('simple_form.hints.defaults.setting_default_sensitive') - .fields-group - = f.input :setting_show_application, as: :boolean, wrapper: :with_label, recommended: true + .fields-group + = ff.input :show_application, wrapper: :with_label, recommended: true, label: I18n.t('simple_form.labels.defaults.setting_show_application'), hint: I18n.t('simple_form.hints.defaults.setting_show_application') %h4= t 'preferences.public_timelines' diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index a361cb0ec33..95f0b5788bf 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -26,6 +26,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'URL' inflect.acronym 'ASCII' inflect.acronym 'DeepL' + inflect.acronym 'DSL' inflect.singular 'data', 'data' end diff --git a/config/settings.yml b/config/settings.yml index f0b09dd5c88..4ac521a4b0b 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -12,48 +12,14 @@ defaults: &defaults registrations_mode: 'open' profile_directory: true closed_registrations_message: '' - open_deletion: true - min_invite_role: 'admin' timeline_preview: true show_staff_badge: true - default_sensitive: false - unfollow_modal: false - boost_modal: false - delete_modal: true - auto_play_gif: false - display_media: 'default' - expand_spoilers: false preview_sensitive_media: false - reduce_motion: false - disable_swiping: false - show_application: true - system_font_ui: false noindex: false theme: 'default' - aggregate_reblogs: true - advanced_layout: false - use_blurhash: true - use_pending_items: false trends: true trends_as_landing_page: true trendable_by_default: false - crop_images: true - notification_emails: - follow: true - reblog: false - favourite: false - mention: true - follow_request: true - digest: true - report: true - pending_account: true - trending_tag: true - appeal: true - always_send_emails: false - interactions: - must_be_follower: false - must_be_following: false - must_be_following_dm: false reserved_usernames: - admin - support diff --git a/db/migrate/20230215074327_add_settings_to_users.rb b/db/migrate/20230215074327_add_settings_to_users.rb new file mode 100644 index 00000000000..ff5308f4234 --- /dev/null +++ b/db/migrate/20230215074327_add_settings_to_users.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddSettingsToUsers < ActiveRecord::Migration[6.1] + def change + add_column :users, :settings, :text + end +end diff --git a/db/migrate/20230215074423_move_user_settings.rb b/db/migrate/20230215074423_move_user_settings.rb new file mode 100644 index 00000000000..351a8b61df2 --- /dev/null +++ b/db/migrate/20230215074423_move_user_settings.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +class MoveUserSettings < ActiveRecord::Migration[6.1] + class User < ApplicationRecord; end + + MAPPING = { + default_privacy: 'default_privacy', + default_sensitive: 'web.default_sensitive', + default_language: 'default_language', + noindex: 'noindex', + theme: 'theme', + trends: 'web.trends', + unfollow_modal: 'web.unfollow_modal', + boost_modal: 'web.reblog_modal', + delete_modal: 'web.delete_modal', + auto_play_gif: 'web.auto_play', + display_media: 'web.display_media', + expand_spoilers: 'web.expand_content_warnings', + reduce_motion: 'web.reduce_motion', + disable_swiping: 'web.disable_swiping', + show_application: 'show_application', + system_font_ui: 'web.use_system_font', + aggregate_reblogs: 'aggregate_reblogs', + advanced_layout: 'web.advanced_layout', + use_blurhash: 'web.use_blurhash', + use_pending_items: 'web.use_pending_items', + crop_images: 'web.crop_images', + notification_emails: { + follow: 'notification_emails.follow', + reblog: 'notification_emails.reblog', + favourite: 'notification_emails.favourite', + mention: 'notification_emails.mention', + follow_request: 'notification_emails.follow_request', + report: 'notification_emails.report', + pending_account: 'notification_emails.pending_account', + trending_tag: 'notification_emails.trends', + appeal: 'notification_emails.appeal', + }.freeze, + always_send_emails: 'always_send_emails', + interactions: { + must_be_follower: 'interactions.must_be_follower', + must_be_following: 'interactions.must_be_following', + must_be_following_dm: 'interactions.must_be_following_dm', + }.freeze, + }.freeze + + class LegacySetting < ApplicationRecord + self.table_name = 'settings' + + def var + self[:var]&.to_sym + end + + def value + YAML.safe_load(self[:value], permitted_classes: [ActiveSupport::HashWithIndifferentAccess]) if self[:value].present? + end + end + + def up + User.find_each do |user| + previous_settings = LegacySetting.where(thing_type: 'User', thing_id: user.id).index_by(&:var) + + user_settings = {} + + MAPPING.each do |legacy_key, new_key| + value = previous_settings[legacy_key]&.value + + next if value.blank? + + if value.is_a?(Hash) + value.each do |nested_key, nested_value| + user_settings[MAPPING[legacy_key][nested_key.to_sym]] = nested_value + end + else + user_settings[new_key] = value + end + end + + user.update_column('settings', Oj.dump(user_settings)) # rubocop:disable Rails/SkipsModelValidations + end + end + + def down; end +end diff --git a/db/schema.rb b/db/schema.rb index 704cef1228b..620bed2bc95 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_12_06_114142) do +ActiveRecord::Schema.define(version: 2023_02_15_074423) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1060,6 +1060,7 @@ ActiveRecord::Schema.define(version: 2022_12_06_114142) do t.inet "sign_up_ip" t.boolean "skip_sign_in_token" t.bigint "role_id" + t.text "settings" t.index ["account_id"], name: "index_users_on_account_id" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)" diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake index 51a6ee0d75c..35073b78bbc 100644 --- a/lib/tasks/tests.rake +++ b/lib/tasks/tests.rake @@ -54,7 +54,7 @@ namespace :tests do exit(1) end - unless User.find(1).settings.notification_emails['favourite'] == true && User.find(1).settings.notification_emails['mention'] == false + unless User.find(1).settings['notification_emails.favourite'] == true && User.find(1).settings['notification_emails.mention'] == false puts 'User settings not kept as expected' exit(1) end diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb index 57fe0aee686..b5d5c37a9c7 100644 --- a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb @@ -46,6 +46,7 @@ describe Api::V1::Accounts::CredentialsController do end it 'updates account info' do + user.reload user.account.reload expect(user.account.display_name).to eq("Alice Isn't Dead") diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 35c7326cb13..bc6c6c0c5e5 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -88,21 +88,19 @@ describe ApplicationController, type: :controller do it 'returns instances\'s default theme when user didn\'t set theme' do current_user = Fabricate(:user) + current_user.settings.update(theme: 'contrast', noindex: false) + current_user.save sign_in current_user - allow(Setting).to receive(:[]).with('theme').and_return 'contrast' - allow(Setting).to receive(:[]).with('noindex').and_return false - expect(controller.view_context.current_theme).to eq 'contrast' end it 'returns user\'s theme when it is set' do current_user = Fabricate(:user) - current_user.settings['theme'] = 'mastodon-light' + current_user.settings.update(theme: 'mastodon-light') + current_user.save sign_in current_user - allow(Setting).to receive(:[]).with('theme').and_return 'contrast' - expect(controller.view_context.current_theme).to eq 'mastodon-light' end end diff --git a/spec/controllers/settings/preferences/notifications_controller_spec.rb b/spec/controllers/settings/preferences/notifications_controller_spec.rb index 66fb8c5eb98..29b7b6aec58 100644 --- a/spec/controllers/settings/preferences/notifications_controller_spec.rb +++ b/spec/controllers/settings/preferences/notifications_controller_spec.rb @@ -20,20 +20,22 @@ describe Settings::Preferences::NotificationsController do describe 'PUT #update' do it 'updates notifications settings' do - user.settings['notification_emails'] = user.settings['notification_emails'].merge('follow' => false) - user.settings['interactions'] = user.settings['interactions'].merge('must_be_follower' => true) + user.settings.update('notification_emails.follow': false, 'interactions.must_be_follower': true) + user.save put :update, params: { user: { - notification_emails: { follow: '1' }, - interactions: { must_be_follower: '0' }, + settings_attributes: { + 'notification_emails.follow': '1', + 'interactions.must_be_follower': '0', + }, }, } expect(response).to redirect_to(settings_preferences_notifications_path) user.reload - expect(user.settings['notification_emails']['follow']).to be true - expect(user.settings['interactions']['must_be_follower']).to be false + expect(user.settings['notification_emails.follow']).to be true + expect(user.settings['interactions.must_be_follower']).to be false end end end diff --git a/spec/controllers/settings/preferences/other_controller_spec.rb b/spec/controllers/settings/preferences/other_controller_spec.rb index 63eeefaf0ff..249d1b5b59a 100644 --- a/spec/controllers/settings/preferences/other_controller_spec.rb +++ b/spec/controllers/settings/preferences/other_controller_spec.rb @@ -29,20 +29,22 @@ describe Settings::Preferences::OtherController do end it 'updates user settings' do - user.settings['boost_modal'] = false - user.settings['delete_modal'] = true + user.settings.update('web.reblog_modal': false, 'web.delete_modal': true) + user.save put :update, params: { user: { - setting_boost_modal: '1', - setting_delete_modal: '0', + settings_attributes: { + 'web.reblog_modal': '1', + 'web.delete_modal': '0', + }, }, } expect(response).to redirect_to(settings_preferences_other_path) user.reload - expect(user.settings['boost_modal']).to be true - expect(user.settings['delete_modal']).to be false + expect(user.settings['web.reblog_modal']).to be true + expect(user.settings['web.delete_modal']).to be false end end end diff --git a/spec/lib/settings/extend_spec.rb b/spec/lib/settings/extend_spec.rb deleted file mode 100644 index ea623137b4a..00000000000 --- a/spec/lib/settings/extend_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Settings::Extend do - class User - include Settings::Extend - end - - describe '#settings' do - it 'sets @settings as an instance of Settings::ScopedSettings' do - user = Fabricate(:user) - expect(user.settings).to be_a Settings::ScopedSettings - end - end -end diff --git a/spec/lib/settings/scoped_settings_spec.rb b/spec/lib/settings/scoped_settings_spec.rb deleted file mode 100644 index 7566685b4a5..00000000000 --- a/spec/lib/settings/scoped_settings_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Settings::ScopedSettings do - let(:object) { Fabricate(:user) } - let(:scoped_setting) { described_class.new(object) } - let(:val) { 'whatever' } - let(:methods) { %i(auto_play_gif default_sensitive unfollow_modal boost_modal delete_modal reduce_motion system_font_ui noindex theme) } - - describe '.initialize' do - it 'sets @object' do - scoped_setting = described_class.new(object) - expect(scoped_setting.instance_variable_get(:@object)).to be object - end - end - - describe '#method_missing' do - it 'sets scoped_setting.method_name = val' do - methods.each do |key| - scoped_setting.send("#{key}=", val) - expect(scoped_setting.send(key)).to eq val - end - end - end - - describe '#[]= and #[]' do - it 'sets [key] = val' do - methods.each do |key| - scoped_setting[key] = val - expect(scoped_setting[key]).to eq val - end - end - end -end diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb deleted file mode 100644 index 3b9b7ee2b2e..00000000000 --- a/spec/lib/user_settings_decorator_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe UserSettingsDecorator do - describe 'update' do - let(:user) { Fabricate(:user) } - let(:settings) { described_class.new(user) } - - it 'updates the user settings value for email notifications' do - values = { 'notification_emails' => { 'follow' => '1' } } - - settings.update(values) - expect(user.settings['notification_emails']['follow']).to be true - end - - it 'updates the user settings value for interactions' do - values = { 'interactions' => { 'must_be_follower' => '0' } } - - settings.update(values) - expect(user.settings['interactions']['must_be_follower']).to be false - end - - it 'updates the user settings value for privacy' do - values = { 'setting_default_privacy' => 'public' } - - settings.update(values) - expect(user.settings['default_privacy']).to eq 'public' - end - - it 'updates the user settings value for sensitive' do - values = { 'setting_default_sensitive' => '1' } - - settings.update(values) - expect(user.settings['default_sensitive']).to be true - end - - it 'updates the user settings value for unfollow modal' do - values = { 'setting_unfollow_modal' => '0' } - - settings.update(values) - expect(user.settings['unfollow_modal']).to be false - end - - it 'updates the user settings value for boost modal' do - values = { 'setting_boost_modal' => '1' } - - settings.update(values) - expect(user.settings['boost_modal']).to be true - end - - it 'updates the user settings value for delete toot modal' do - values = { 'setting_delete_modal' => '0' } - - settings.update(values) - expect(user.settings['delete_modal']).to be false - end - - it 'updates the user settings value for gif auto play' do - values = { 'setting_auto_play_gif' => '0' } - - settings.update(values) - expect(user.settings['auto_play_gif']).to be false - end - - it 'updates the user settings value for system font in UI' do - values = { 'setting_system_font_ui' => '0' } - - settings.update(values) - expect(user.settings['system_font_ui']).to be false - end - - it 'decoerces setting values before applying' do - values = { - 'setting_delete_modal' => 'false', - 'setting_boost_modal' => 'true', - } - - settings.update(values) - expect(user.settings['delete_modal']).to be false - expect(user.settings['boost_modal']).to be true - end - end -end diff --git a/spec/models/user_settings/namespace_spec.rb b/spec/models/user_settings/namespace_spec.rb new file mode 100644 index 00000000000..ae2fa7b482c --- /dev/null +++ b/spec/models/user_settings/namespace_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UserSettings::Namespace do + subject { described_class.new(name) } + + let(:name) { :foo } + + describe '#setting' do + before do + subject.setting :bar, default: 'baz' + end + + it 'adds setting to definitions' do + expect(subject.definitions[:'foo.bar']).to have_attributes(name: :bar, namespace: :foo, default_value: 'baz') + end + end + + describe '#definitions' do + it 'returns a hash' do + expect(subject.definitions).to be_a Hash + end + end +end diff --git a/spec/models/user_settings/setting_spec.rb b/spec/models/user_settings/setting_spec.rb new file mode 100644 index 00000000000..6e4ec678933 --- /dev/null +++ b/spec/models/user_settings/setting_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UserSettings::Setting do + subject { described_class.new(name, options) } + + let(:name) { :foo } + let(:options) { { default: default, namespace: namespace } } + let(:default) { false } + let(:namespace) { nil } + + describe '#default_value' do + context 'when default value is a primitive value' do + it 'returns default value' do + expect(subject.default_value).to eq default + end + end + + context 'when default value is a proc' do + let(:default) { -> { 'bar' } } + + it 'returns value from proc' do + expect(subject.default_value).to eq 'bar' + end + end + end + + describe '#type' do + it 'returns a type' do + expect(subject.type).to be_a ActiveModel::Type::Value + end + end + + describe '#type_cast' do + context 'when default value is a boolean' do + let(:default) { false } + + it 'returns boolean' do + expect(subject.type_cast('1')).to be true + end + end + + context 'when default value is a string' do + let(:default) { '' } + + it 'returns string' do + expect(subject.type_cast(1)).to eq '1' + end + end + end + + describe '#to_a' do + it 'returns an array' do + expect(subject.to_a).to eq [name, default] + end + end + + describe '#key' do + context 'when there is no namespace' do + it 'returnsn a symbol' do + expect(subject.key).to eq :foo + end + end + + context 'when there is a namespace' do + let(:namespace) { :bar } + + it 'returns a symbol' do + expect(subject.key).to eq :'bar.foo' + end + end + end +end diff --git a/spec/models/user_settings_spec.rb b/spec/models/user_settings_spec.rb new file mode 100644 index 00000000000..f0e4272fd99 --- /dev/null +++ b/spec/models/user_settings_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UserSettings do + subject { described_class.new(json) } + + let(:json) { {} } + + describe '#[]' do + context 'when setting is not set' do + it 'returns default value' do + expect(subject[:always_send_emails]).to be false + end + end + + context 'when setting is set' do + let(:json) { { default_language: 'fr' } } + + it 'returns value' do + expect(subject[:default_language]).to eq 'fr' + end + end + + context 'when setting was not defined' do + it 'raises error' do + expect { subject[:foo] }.to raise_error UserSettings::KeyError + end + end + end + + describe '#[]=' do + context 'when value matches type' do + before do + subject[:always_send_emails] = true + end + + it 'updates value' do + expect(subject[:always_send_emails]).to be true + end + end + + context 'when value needs to be type-cast' do + before do + subject[:always_send_emails] = '1' + end + + it 'updates value with a type-cast' do + expect(subject[:always_send_emails]).to be true + end + end + end + + describe '#update' do + before do + subject.update(always_send_emails: true, default_language: 'fr', default_privacy: nil) + end + + it 'updates values' do + expect(subject[:always_send_emails]).to be true + expect(subject[:default_language]).to eq 'fr' + end + + it 'does not set values that are nil' do + expect(subject.as_json).to_not include(default_privacy: nil) + end + end + + describe '#as_json' do + let(:json) { { default_language: 'fr' } } + + it 'returns hash' do + expect(subject.as_json).to eq json + end + end + + describe '.keys' do + it 'returns an array' do + expect(described_class.keys).to be_a Array + end + end + + describe '.definition_for' do + context 'when key is defined' do + it 'returns a setting' do + expect(described_class.definition_for(:always_send_emails)).to be_a UserSettings::Setting + end + end + + context 'when key is not defined' do + it 'returns nil' do + expect(described_class.definition_for(:foo)).to be_nil + end + end + end + + describe '.definition_for?' do + context 'when key is defined' do + it 'returns true' do + expect(described_class.definition_for?(:always_send_emails)).to be true + end + end + + context 'when key is not defined' do + it 'returns false' do + expect(described_class.definition_for?(:foo)).to be false + end + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3e7b59f170b..ab883927a58 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -313,9 +313,9 @@ RSpec.describe User, type: :model do end describe 'settings' do - it 'is instance of Settings::ScopedSettings' do + it 'is instance of UserSettings' do user = Fabricate(:user) - expect(user.settings).to be_a Settings::ScopedSettings + expect(user.settings).to be_a UserSettings end end @@ -379,16 +379,6 @@ RSpec.describe User, type: :model do end end - it_behaves_like 'Settings-extended' do - def create! - User.create!(account: Fabricate(:account, user: nil), email: 'foo@mastodon.space', password: 'abcd1234', agreement: true) - end - - def fabricate - Fabricate(:user) - end - end - describe 'token_for_app' do let(:user) { Fabricate(:user) } let(:app) { Fabricate(:application, owner: user) } diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb index c58cebbfb8a..616a7aa2042 100644 --- a/spec/services/notify_service_spec.rb +++ b/spec/services/notify_service_spec.rb @@ -54,7 +54,8 @@ RSpec.describe NotifyService, type: :service do let(:type) { :mention } before do - user.settings.interactions = user.settings.interactions.merge('must_be_following_dm' => enabled) + user.settings.update('interactions.must_be_following_dm': enabled) + user.save end context 'if recipient is supposed to be following sender' do @@ -155,8 +156,8 @@ RSpec.describe NotifyService, type: :service do before do ActionMailer::Base.deliveries.clear - notification_emails = user.settings.notification_emails - user.settings.notification_emails = notification_emails.merge('follow' => enabled) + user.settings.update('notification_emails.follow': enabled) + user.save end context 'when email notification is enabled' do diff --git a/spec/services/report_service_spec.rb b/spec/services/report_service_spec.rb index 9d81bd97140..452400f7229 100644 --- a/spec/services/report_service_spec.rb +++ b/spec/services/report_service_spec.rb @@ -96,7 +96,8 @@ RSpec.describe ReportService, type: :service do before do ActionMailer::Base.deliveries.clear - source_account.user.settings.notification_emails['report'] = true + source_account.user.settings['notification_emails.report'] = true + source_account.user.save end it 'does not send an e-mail' do