diff --git a/app/controllers/admin/tags/moderators_controller.rb b/app/controllers/admin/tags/moderators_controller.rb index 42988cfc7..64a15d01e 100644 --- a/app/controllers/admin/tags/moderators_controller.rb +++ b/app/controllers/admin/tags/moderators_controller.rb @@ -11,25 +11,37 @@ module Admin def create user = User.find_by(id: tag_params[:user_id]) - if user&.update(email_tag_mod_newsletter: true) + unless user + flash[:error] = "Error: User ID ##{tag_params[:user_id]} was not found" + return redirect_to edit_admin_tag_path(params[:tag_id]) + end + + notification_setting = user.notification_setting + if notification_setting.update(email_tag_mod_newsletter: true) TagModerators::Add.call([user.id], [params[:tag_id]]) flash[:success] = "#{user.username} was added as a tag moderator!" else flash[:error] = "Error: User ID ##{tag_params[:user_id]} was not found, - or their account has errors: #{user&.errors_as_sentence}" + or their account has errors: #{notification_setting.errors_as_sentence}" end redirect_to edit_admin_tag_path(params[:tag_id]) end def destroy user = User.find_by(id: tag_params[:user_id]) + unless user + flash[:error] = "Error: User ID ##{tag_params[:user_id]} was not found" + return redirect_to edit_admin_tag_path(params[:tag_id]) + end + + notification_setting = user.notification_setting tag = Tag.find_by(id: params[:tag_id]) - if user&.update(email_tag_mod_newsletter: false) + if notification_setting.update(email_tag_mod_newsletter: false) TagModerators::Remove.call(user, tag) flash[:success] = "@#{user.username} - ID ##{user.id} was removed as a tag moderator." else flash[:error] = "Error: User ID ##{tag_params[:user_id]} was not found, - or their account has errors: #{user&.errors_as_sentence}" + or their account has errors: #{notification_setting.errors_as_sentence}" end redirect_to edit_admin_tag_path(tag.id) end diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index e3208b088..f2c491352 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -218,7 +218,7 @@ class ArticlesController < ApplicationController def base_editor_assigments @user = current_user - @version = @user.editor_version if @user + @version = @user.setting.editor_version if @user @organizations = @user&.organizations @tag = Tag.find_by(name: params[:template]) @prefill = params[:prefill].to_s.gsub("\\n ", "\n").gsub("\\n", "\n") diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb index e130b9b03..35842d49b 100644 --- a/app/controllers/chat_channels_controller.rb +++ b/app/controllers/chat_channels_controller.rb @@ -137,7 +137,7 @@ class ChatChannelsController < ApplicationController valid_listing = Listing.where(user_id: params[:user_id], contact_via_connect: true).limit(1) authorize ChatChannel - if chat_recipient.inbox_type == "open" || valid_listing.length == 1 + if chat_recipient.setting.inbox_type == "open" || valid_listing.length == 1 chat = ChatChannels::CreateWithUsers.call(users: [current_user, chat_recipient], channel_type: "direct") message_markdown = params[:message] message = Message.new( diff --git a/app/controllers/email_subscriptions_controller.rb b/app/controllers/email_subscriptions_controller.rb index 6286acaf7..c8dc37d3b 100644 --- a/app/controllers/email_subscriptions_controller.rb +++ b/app/controllers/email_subscriptions_controller.rb @@ -4,7 +4,7 @@ class EmailSubscriptionsController < ApplicationController if verified_params[:expires_at] > Time.current user = User.find(verified_params[:user_id]) - user.update(verified_params[:email_type] => false) + user.notification_setting.update(verified_params[:email_type] => false) @email_type = preferred_email_name.fetch(verified_params[:email_type], "this list") else render "invalid_token" diff --git a/app/controllers/incoming_webhooks/mailchimp_unsubscribes_controller.rb b/app/controllers/incoming_webhooks/mailchimp_unsubscribes_controller.rb index e9fe0f523..717200db1 100644 --- a/app/controllers/incoming_webhooks/mailchimp_unsubscribes_controller.rb +++ b/app/controllers/incoming_webhooks/mailchimp_unsubscribes_controller.rb @@ -18,7 +18,7 @@ module IncomingWebhooks def create not_authorized unless valid_secret? user = User.find_by!(email: params.dig(:data, :email)) - user.update(email_type => false) + user.notification_setting.update(email_type => false) end private diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 3f9b580be..0a789d9ae 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -1,6 +1,8 @@ class ProfilesController < ApplicationController before_action :authenticate_user! + ALLOWED_USER_PARAMS = %i[name email username profile_image].freeze + ALLOWED_USERS_SETTING_PARAMS = %i[display_email_on_profile brand_color1 brand_color2].freeze def update update_result = Profiles::Update.call(current_user, update_params) @@ -13,7 +15,8 @@ class ProfilesController < ApplicationController flash[:error] = "Error: #{update_result.errors_as_sentence}" render template: "users/edit", locals: { user: update_params[:user], - profile: update_params[:profile] + profile: update_params[:profile], + users_setting: update_params[:users_setting] } end end @@ -21,6 +24,8 @@ class ProfilesController < ApplicationController private def update_params - params.permit(profile: Profile.attributes + Profile.static_fields, user: ALLOWED_USER_PARAMS) + params.permit(profile: Profile.attributes + Profile.static_fields, + user: ALLOWED_USER_PARAMS, + users_setting: ALLOWED_USERS_SETTING_PARAMS) end end diff --git a/app/controllers/reactions_controller.rb b/app/controllers/reactions_controller.rb index aef615f8e..ca8047a78 100644 --- a/app/controllers/reactions_controller.rb +++ b/app/controllers/reactions_controller.rb @@ -90,7 +90,7 @@ class ReactionsController < ApplicationController result = "create" - if category == "readinglist" && current_user.experience_level + if category == "readinglist" && current_user.setting.experience_level rate_article(reaction) end @@ -146,7 +146,7 @@ class ReactionsController < ApplicationController group: "experience_level", user_id: current_user.id, context: "readinglist_reaction", - rating: current_user.experience_level) + rating: current_user.setting.experience_level) end def clear_moderator_reactions(id, type, mod, category) diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index e83559cda..c8fe7c1ea 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -80,6 +80,7 @@ class RegistrationsController < Devise::RegistrationsController def build_devise_resource build_resource(sign_up_params) resource.registered_at = Time.current + resource.build_setting(editor_version: "v2") resource.remote_profile_image_url = Users::ProfileImageGenerator.call if resource.remote_profile_image_url.blank? check_allowed_email(resource) if resource.email.present? resource.save if resource.email.present? diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 0bcaa6d76..876b68a35 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -344,7 +344,7 @@ class StoriesController < ApplicationController sameAs: user_same_as, image: Images::Profile.call(@user.profile_image_url, length: 320), name: @user.name, - email: @user.email_public ? @user.email : nil, + email: @user.setting.display_email_on_profile ? @user.email : nil, jobTitle: @user.employment_title.presence, description: @user.summary.presence || "404 bio not found", worksFor: [user_works_for].compact, diff --git a/app/controllers/users/notification_settings_controller.rb b/app/controllers/users/notification_settings_controller.rb new file mode 100644 index 000000000..ce4d6918b --- /dev/null +++ b/app/controllers/users/notification_settings_controller.rb @@ -0,0 +1,63 @@ +module Users + class NotificationSettingsController < ApplicationController + before_action :raise_suspended + before_action :authenticate_user! + after_action :verify_authorized + + ALLOWED_PARAMS = %i[email_badge_notifications + email_comment_notifications + email_community_mod_newsletter + email_connect_messages + email_digest_periodic + email_follower_notifications + email_membership_newsletter + email_mention_notifications + email_newsletter + email_tag_mod_newsletter + email_unread_notifications + mobile_comment_notifications + mod_roundrobin_notifications + reaction_notifications + welcome_notifications].freeze + + def update + authorize current_user, policy_class: UserPolicy + + if current_user.notification_setting.update(users_notification_setting_params) + flash[:settings_notice] = "Your notification settings have been updated." + else + Honeycomb.add_field("error", current_user.notification_setting.errors.messages.compact_blank) + Honeycomb.add_field("errored", true) + flash[:error] = current_user.notification_setting.errors_as_sentence + end + redirect_to user_settings_path(:notifications) + end + + def onboarding_notifications_checkbox_update + authorize User + + if params[:notifications] + permitted_params = %i[email_newsletter email_digest_periodic] + current_user.notification_setting.assign_attributes(params[:notifications].permit(permitted_params)) + end + + current_user.saw_onboarding = true + success = current_user.notification_setting.save + render_update_response(success, current_user.notification_setting.errors_as_sentence) + end + + private + + def render_update_response(success, errors = nil) + status = success ? 200 : 422 + + respond_to do |format| + format.json { render json: { errors: errors }, status: status } + end + end + + def users_notification_setting_params + params.require(:users_notification_setting).permit(ALLOWED_PARAMS) + end + end +end diff --git a/app/controllers/users/settings_controller.rb b/app/controllers/users/settings_controller.rb new file mode 100644 index 000000000..8c05de693 --- /dev/null +++ b/app/controllers/users/settings_controller.rb @@ -0,0 +1,55 @@ +module Users + class SettingsController < ApplicationController + before_action :raise_suspended + before_action :authenticate_user! + after_action :verify_authorized + + ALLOWED_PARAMS = %i[config_theme + config_font + config_navbar + display_announcements + display_sponsors + editor_version + experience_level + feed_fetched_at + feed_mark_canonical + feed_referential_link + feed_url + inbox_guidelines + inbox_type + permit_adjacent_sponsors].freeze + + def update + authorize current_user, policy_class: UserPolicy + users_setting = current_user.setting + tab = params["users_setting"]["tab"] || "profile" + + if users_setting.update(users_setting_params) + import_articles_from_feed(users_setting) + + if users_setting.experience_level.present? + cookies.permanent[:user_experience_level] = users_setting.experience_level.to_s + end + current_user.touch(:profile_updated_at) + flash[:settings_notice] = "Your config has been updated. Refresh to see all changes." + else + Honeycomb.add_field("error", users_setting.errors.messages.compact_blank) + Honeycomb.add_field("errored", true) + flash[:error] = users_setting.errors_as_sentence + end + redirect_to user_settings_path(tab) + end + + private + + def import_articles_from_feed(users_setting) + return if users_setting.feed_url.blank? + + Feeds::ImportArticlesWorker.perform_async(users_setting.user_id) + end + + def users_setting_params + params.require(:users_setting).permit(ALLOWED_PARAMS) + end + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a8d6b0792..01acd4297 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -11,6 +11,7 @@ class UsersController < ApplicationController before_action :initialize_stripe, only: %i[edit] ALLOWED_USER_PARAMS = %i[last_onboarding_page username].freeze + ALLOWED_ONBOARDING_PARAMS = %i[checked_code_of_conduct checked_terms_and_conditions].freeze INDEX_ATTRIBUTES_FOR_SERIALIZATION = %i[id name username summary profile_image].freeze private_constant :INDEX_ATTRIBUTES_FOR_SERIALIZATION REMOVE_IDENTITY_ERROR = "An error occurred. Please try again or send an email to: %s".freeze @@ -35,6 +36,7 @@ class UsersController < ApplicationController return redirect_to sign_up_path end set_user + set_users_setting_and_notification_setting set_current_tab(params["tab"] || "profile") handle_settings_tab end @@ -42,6 +44,7 @@ class UsersController < ApplicationController # PATCH/PUT /users/:id.:format def update set_current_tab(params["user"]["tab"]) + set_users_setting_and_notification_setting @user.assign_attributes(permitted_attributes(@user)) @@ -51,14 +54,13 @@ class UsersController < ApplicationController import_articles_from_feed(@user) notice = "Your profile was successfully updated." - if config_changed? - notice = "Your config has been updated. Refresh to see all changes." - end if @user.export_requested? notice += " The export will be emailed to you shortly." ExportContentWorker.perform_async(@user.id, @user.email) end - cookies.permanent[:user_experience_level] = @user.experience_level.to_s if @user.experience_level.present? + if @user.setting.experience_level.present? + cookies.permanent[:user_experience_level] = @user.setting.experience_level.to_s + end flash[:settings_notice] = notice @user.touch(:profile_updated_at) redirect_to "/settings/#{@tab}" @@ -185,10 +187,7 @@ class UsersController < ApplicationController def onboarding_checkbox_update if params[:user] - permitted_params = %i[ - checked_code_of_conduct checked_terms_and_conditions email_newsletter email_digest_periodic - ] - current_user.assign_attributes(params[:user].permit(permitted_params)) + current_user.assign_attributes(params[:user].permit(ALLOWED_ONBOARDING_PARAMS)) end current_user.saw_onboarding = true @@ -360,12 +359,15 @@ class UsersController < ApplicationController authorize @user end - def set_current_tab(current_tab = "profile") - @tab = current_tab + def set_users_setting_and_notification_setting + return unless @user + + @users_setting = @user.setting + @users_notification_setting = @user.notification_setting end - def config_changed? - params[:user].include?(:config_theme) + def set_current_tab(current_tab = "profile") + @tab = current_tab end def destroy_request_in_progress? @@ -373,7 +375,7 @@ class UsersController < ApplicationController end def import_articles_from_feed(user) - return if user.feed_url.blank? + return if user.setting.feed_url.blank? Feeds::ImportArticlesWorker.perform_async(user.id) end diff --git a/app/decorators/user_decorator.rb b/app/decorators/user_decorator.rb index fc2904fc9..b68bd35c8 100644 --- a/app/decorators/user_decorator.rb +++ b/app/decorators/user_decorator.rb @@ -36,35 +36,31 @@ class UserDecorator < ApplicationDecorator end def enriched_colors - if bg_color_hex.blank? || text_color_hex.blank? + if setting.brand_color1.blank? || setting.brand_color2.blank? { bg: assigned_color[:bg], text: assigned_color[:text] } else { - bg: bg_color_hex, - text: text_color_hex + bg: setting.brand_color1, + text: setting.brand_color2 } end end - def config_font_name - config_font.gsub("default", Settings::UserExperience.default_font) - end - def config_body_class body_class = [ - config_theme.tr("_", "-"), - "#{config_font_name.tr('_', '-')}-article-body", + setting.config_theme.tr("_", "-"), + "#{setting.resolved_font_name.tr('_', '-')}-article-body", "trusted-status-#{trusted}", - "#{config_navbar.tr('_', '-')}-header", + "#{setting.config_navbar.tr('_', '-')}-header", ] body_class.join(" ") end def dark_theme? - config_theme == "night_theme" || config_theme == "ten_x_hacker_theme" + setting.config_theme == "night_theme" || setting.config_theme == "ten_x_hacker_theme" end def assigned_color @@ -113,4 +109,8 @@ class UserDecorator < ApplicationDecorator created_at.after?(min_days.days.ago) end + + delegate :display_sponsors, to: :setting + + delegate :display_announcements, to: :setting end diff --git a/app/javascript/onboarding/components/EmailPreferencesForm.jsx b/app/javascript/onboarding/components/EmailPreferencesForm.jsx index 6a2990ea2..3100967d5 100644 --- a/app/javascript/onboarding/components/EmailPreferencesForm.jsx +++ b/app/javascript/onboarding/components/EmailPreferencesForm.jsx @@ -24,13 +24,13 @@ export class EmailPreferencesForm extends Component { onSubmit() { const csrfToken = getContentOfToken('csrf-token'); - fetch('/onboarding_checkbox_update', { + fetch('/onboarding_notifications_checkbox_update', { method: 'PATCH', headers: { 'X-CSRF-Token': csrfToken, 'Content-Type': 'application/json', }, - body: JSON.stringify({ user: this.state }), + body: JSON.stringify({ notifications: this.state }), credentials: 'same-origin', }).then((response) => { if (response.ok) { diff --git a/app/models/badge_achievement.rb b/app/models/badge_achievement.rb index 759f81c87..422fc8218 100644 --- a/app/models/badge_achievement.rb +++ b/app/models/badge_achievement.rb @@ -42,7 +42,7 @@ class BadgeAchievement < ApplicationRecord def send_email_notification return unless user.is_a?(User) - return unless user.email && user.email_badge_notifications + return unless user.email && user.notification_setting.email_badge_notifications BadgeAchievements::SendEmailNotificationWorker.perform_async(id) end diff --git a/app/models/comment.rb b/app/models/comment.rb index 279be0a48..c3c460815 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -299,7 +299,7 @@ class Comment < ApplicationRecord parent_exists? && parent_user.class.name != "Podcast" && parent_user != user && - parent_user.email_comment_notifications && + parent_user.notification_setting.email_comment_notifications && parent_user.email && parent_or_root_article.receive_notifications end diff --git a/app/models/mention.rb b/app/models/mention.rb index 2bbb09a0e..de05b4b87 100644 --- a/app/models/mention.rb +++ b/app/models/mention.rb @@ -17,7 +17,7 @@ class Mention < ApplicationRecord def send_email_notification user = User.find(user_id) - return unless user.email.present? && user.email_mention_notifications + return unless user.email.present? && user.notification_setting.email_mention_notifications Mentions::SendEmailNotificationWorker.perform_async(id) end diff --git a/app/models/message.rb b/app/models/message.rb index 249e154a1..47c1cc325 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -13,7 +13,7 @@ class Message < ApplicationRecord after_create :update_all_has_unopened_messages_statuses def preferred_user_color - color_options = [user.bg_color_hex || "#000000", user.text_color_hex || "#000000"] + color_options = [user.setting.brand_color1 || "#000000", user.setting.brand_color2 || "#000000"] Color::CompareHex.new(color_options).brightness(0.9) end @@ -217,7 +217,7 @@ class Message < ApplicationRecord recipient.chat_channel_memberships.order(last_opened_at: :desc) .first.last_opened_at > 15.hours.ago || chat_channel.last_message_at > 30.minutes.ago || - recipient.email_connect_messages == false + recipient.notification_setting.email_connect_messages == false NotifyMailer.with(message: self).new_message_email.deliver_now end diff --git a/app/models/profile.rb b/app/models/profile.rb index 5e2b0bc41..d341fb7b7 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -26,9 +26,6 @@ class Profile < ApplicationRecord # NOTE: @citizen428 This is a temporary mapping so we don't break DEV during # profile migration/generalization work. MAPPED_ATTRIBUTES = { - brand_color1: :bg_color_hex, - brand_color2: :text_color_hex, - display_email_on_profile: :email_public, education: :education, skills_languages: :mostly_work_with }.with_indifferent_access.freeze diff --git a/app/models/user.rb b/app/models/user.rb index 7b921573a..c8fea3e13 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -36,7 +36,47 @@ class User < ApplicationRecord youtube_url ].freeze - self.ignored_columns = PROFILE_COLUMNS + COLUMNS_NOW_IN_USERS_SETTINGS = %w[ + config_theme + config_font + config_navbar + display_announcements + display_sponsors + editor_version + experience_level + feed_mark_canonical + feed_referential_link + feed_url + inbox_guidelines + inbox_type + permit_adjacent_sponsors + ].freeze + + COLUMNS_NOW_IN_USERS_NOTIFICATION_SETTINGS = %w[ + email_badge_notifications + email_comment_notifications + email_community_mod_newsletter + email_connect_messages + email_digest_periodic + email_follower_notifications + email_membership_newsletter + email_mention_notifications + email_newsletter + email_tag_mod_newsletter + email_unread_notifications + mobile_comment_notifications + mod_roundrobin_notifications + reaction_notifications + welcome_notifications + ].freeze + + INACTIVE_PROFILE_COLUMNS = %w[ + bg_color_hex + text_color_hex + email_public + ].freeze + + self.ignored_columns = PROFILE_COLUMNS + COLUMNS_NOW_IN_USERS_SETTINGS + COLUMNS_NOW_IN_USERS_NOTIFICATION_SETTINGS # NOTE: @citizen428 This is temporary code during profile migration and will # be removed. @@ -53,6 +93,8 @@ class User < ApplicationRecord # Getters and setters for unmapped profile attributes (PROFILE_COLUMNS - Profile::MAPPED_ATTRIBUTES.values).each do |column| + next if INACTIVE_PROFILE_COLUMNS.include?(column) + delegate column, "#{column}=", to: :profile, allow_nil: true end @@ -67,18 +109,9 @@ class User < ApplicationRecord end ANY_ADMIN_ROLES = %i[admin super_admin].freeze - EDITORS = %w[v1 v2].freeze - FONTS = %w[serif sans_serif monospace comic_sans open_dyslexic].freeze - INBOXES = %w[open private].freeze - NAVBARS = %w[default static].freeze - THEMES = %w[default night_theme pink_theme minimal_light_theme ten_x_hacker_theme].freeze USERNAME_MAX_LENGTH = 30 USERNAME_REGEXP = /\A[a-zA-Z0-9_]+\z/.freeze MESSAGES = { - invalid_config_font: "%s is not a valid font selection", - invalid_config_navbar: "%s is not a valid navbar value", - invalid_config_theme: "%s is not a valid theme", - invalid_editor_version: "%s must be either v1 or v2", reserved_username: "username is reserved" }.freeze # follow the syntax in https://interledger.org/rfcs/0026-payment-pointers/#payment-pointer-syntax @@ -90,57 +123,6 @@ class User < ApplicationRecord \z }x.freeze - # Relevant Fields for migration from Users table to Users_Settings table - USER_FIELDS_TO_MIGRATE_TO_USERS_SETTINGS_TABLE = %w[ - config_font - config_navbar - config_theme - display_announcements - display_sponsors - editor_version - experience_level - feed_mark_canonical - feed_referential_link - feed_url - inbox_guidelines - inbox_type - permit_adjacent_sponsors - ].to_set.freeze - - # Relevant Fields for migration from Profiles table to Users_Settings table - PROFILE_FIELDS_TO_MIGRATE_TO_USERS_SETTINGS_TABLE = %w[ - brand_color1 - brand_color2 - display_email_on_profile - ].to_set.freeze - - # Relevant Fields for migration from Users table to Users_Notification_Settings table - USER_FIELDS_TO_MIGRATE_TO_USERS_NOTIFICATION_SETTINGS_TABLE = %w[ - email_badge_notifications - email_comment_notifications - email_community_mod_newsletter - email_connect_messages - email_digest_periodic - email_follower_notifications - email_membership_newsletter - email_mention_notifications - email_newsletter - email_tag_mod_newsletter - email_unread_notifications - mobile_comment_notifications - mod_roundrobin_notifications - reaction_notifications - welcome_notifications - ].to_set.freeze - - USER_SETTINGS_ENUM_FIELDS = %w[ - config_font - config_navbar - config_theme - editor_version - inbox_type - ].to_set.freeze - attr_accessor :scholar_email, :new_note, :note_for_current_role, :user_status, :merge_user_id, :add_credits, :remove_credits, :add_org_credits, :remove_org_credits, :ip_address, :current_password @@ -236,25 +218,12 @@ class User < ApplicationRecord validates :blocked_by_count, presence: true validates :blocking_others_count, presence: true validates :comments_count, presence: true - validates :config_font, inclusion: { in: FONTS + ["default".freeze], message: MESSAGES[:invalid_config_font] } - validates :config_font, presence: true - validates :config_navbar, inclusion: { in: NAVBARS, message: MESSAGES[:invalid_config_navbar] } - validates :config_navbar, presence: true - validates :config_theme, inclusion: { in: THEMES, message: MESSAGES[:invalid_config_theme] } - validates :config_theme, presence: true validates :credits_count, presence: true - validates :editor_version, inclusion: { in: EDITORS, message: MESSAGES[:invalid_editor_version] } validates :email, length: { maximum: 50 }, email: true, allow_nil: true validates :email, uniqueness: { allow_nil: true, case_sensitive: false }, if: :email_changed? - validates :email_digest_periodic, inclusion: { in: [true, false] } - validates :experience_level, numericality: { less_than_or_equal_to: 10 }, allow_blank: true - validates :feed_referential_link, inclusion: { in: [true, false] } - validates :feed_url, length: { maximum: 500 }, allow_nil: true validates :following_orgs_count, presence: true validates :following_tags_count, presence: true validates :following_users_count, presence: true - validates :inbox_guidelines, length: { maximum: 250 }, allow_nil: true - validates :inbox_type, inclusion: { in: INBOXES } validates :name, length: { in: 1..100 } validates :password, length: { in: 8..100 }, allow_nil: true validates :payment_pointer, format: PAYMENT_POINTER_REGEXP, allow_blank: true @@ -269,7 +238,6 @@ class User < ApplicationRecord validates :username, uniqueness: { case_sensitive: false, message: lambda do |_obj, data| "#{data[:value]} is taken." end }, if: :username_changed? - validates :welcome_notifications, inclusion: { in: [true, false] } # add validators for provider related usernames Authentication::Providers.username_fields.each do |username_field| @@ -285,7 +253,6 @@ class User < ApplicationRecord validate :non_banished_username, :username_changed? validate :unique_including_orgs_and_podcasts, if: :username_changed? - validate :validate_feed_url, if: :feed_url_changed? validate :can_send_confirmation_email validate :update_rate_limit # NOTE: when updating the password on a Devise enabled model, the :encrypted_password @@ -293,9 +260,6 @@ class User < ApplicationRecord validate :password_matches_confirmation, if: :encrypted_password_changed? alias_attribute :public_reactions_count, :reactions_count - alias_attribute :subscribed_to_welcome_notifications?, :welcome_notifications - alias_attribute :subscribed_to_mod_roundrobin_notifications?, :mod_roundrobin_notifications - alias_attribute :subscribed_to_email_follower_notifications?, :email_follower_notifications scope :eager_load_serialized_data, -> { includes(:roles) } scope :registered, -> { where(registered: true) } @@ -332,15 +296,14 @@ class User < ApplicationRecord ), ) } - scope :with_feed, -> { where.not(feed_url: [nil, ""]) } before_validation :check_for_username_change before_validation :downcase_email - before_validation :set_config_input # make sure usernames are not empty, to be able to use the database unique index before_validation :verify_email before_validation :set_username before_validation :strip_payment_pointer + before_create :create_users_settings_and_notification_settings_records before_destroy :unsubscribe_from_newsletters, prepend: true before_destroy :destroy_follows, prepend: true @@ -350,7 +313,6 @@ class User < ApplicationRecord after_create_commit :send_welcome_notification after_commit :subscribe_to_mailchimp_newsletter - after_commit :sync_users_settings_table, :sync_users_notification_settings_table, on: %i[create update] after_commit :bust_cache def self.dev_account @@ -548,7 +510,7 @@ class User < ApplicationRecord return unless registered && email.present? return if Settings::General.mailchimp_api_key.blank? return if saved_changes.key?(:unconfirmed_email) && saved_changes.key?(:confirmation_sent_at) - return unless saved_changes.key?(:email) || saved_changes.key?(:email_newsletter) + return unless saved_changes.key?(:email) Users::SubscribeToMailchimpNewsletterWorker.perform_async(id) end @@ -621,6 +583,18 @@ class User < ApplicationRecord "User:#{id}" end + def subscribed_to_welcome_notifications? + notification_setting.welcome_notifications + end + + def subscribed_to_mod_roundrobin_notifications? + notification_setting.mod_roundrobin_notifications + end + + def subscribed_to_email_follower_notifications? + notification_setting.email_follower_notifications + end + protected # Send emails asynchronously @@ -632,43 +606,9 @@ class User < ApplicationRecord private - def sync_relevant_profile_fields_to_user_settings_table(users_setting_record) - PROFILE_FIELDS_TO_MIGRATE_TO_USERS_SETTINGS_TABLE.each do |field| - # rubocop:disable Layout/LineLength - users_setting_record.assign_attributes(field => profile.public_send(field)) if profile&.public_send(field).present? - # rubocop:enable Layout/LineLength - end - end - - def migrate_users_and_profile_fields_to_users_settings(users_setting_record) - USER_FIELDS_TO_MIGRATE_TO_USERS_SETTINGS_TABLE.each do |field| - if USER_SETTINGS_ENUM_FIELDS.include?(field) - field_enums = Users::Setting.defined_enums[field] - users_setting_record.assign_attributes(field => field_enums[public_send(field).to_sym]) - else - users_setting_record.assign_attributes(field => public_send(field)) - end - end - - sync_relevant_profile_fields_to_user_settings_table(users_setting_record) - - users_setting_record.save - end - - def sync_users_settings_table - users_setting_record = Users::Setting.create_or_find_by(user_id: id) - - migrate_users_and_profile_fields_to_users_settings(users_setting_record) - end - - def sync_users_notification_settings_table - users_notification_setting_record = Users::NotificationSetting.create_or_find_by(user_id: id) - - USER_FIELDS_TO_MIGRATE_TO_USERS_NOTIFICATION_SETTINGS_TABLE.each do |field| - users_notification_setting_record.assign_attributes(field => public_send(field)) - end - - users_notification_setting_record.save + def create_users_settings_and_notification_settings_records + self.setting = Users::Setting.create + self.notification_setting = Users::NotificationSetting.create end def send_welcome_notification @@ -711,12 +651,6 @@ class User < ApplicationRecord self.email = email.downcase if email end - def set_config_input - self.config_theme = config_theme&.tr(" ", "_") - self.config_font = config_font&.tr(" ", "_") - self.config_navbar = config_navbar&.tr(" ", "_") - end - def check_for_username_change return unless username_changed? @@ -736,16 +670,6 @@ class User < ApplicationRecord Users::BustCacheWorker.perform_async(id) end - def validate_feed_url - return if feed_url.blank? - - valid = Feeds::ValidateUrl.call(feed_url) - - errors.add(:feed_url, "is not a valid RSS/Atom feed") unless valid - rescue StandardError => e - errors.add(:feed_url, e.message) - end - def tag_keywords_for_search "#{employer_name}#{mostly_work_with}#{available_for}" end diff --git a/app/models/users/notification_setting.rb b/app/models/users/notification_setting.rb index b6882c62e..49c5716a6 100644 --- a/app/models/users/notification_setting.rb +++ b/app/models/users/notification_setting.rb @@ -1,10 +1,21 @@ module Users class NotificationSetting < ApplicationRecord self.table_name_prefix = "users_" + + belongs_to :user, touch: true + validates :email_digest_periodic, inclusion: { in: [true, false] } alias_attribute :subscribed_to_welcome_notifications?, :welcome_notifications alias_attribute :subscribed_to_mod_roundrobin_notifications?, :mod_roundrobin_notifications alias_attribute :subscribed_to_email_follower_notifications?, :email_follower_notifications + + after_commit :subscribe_to_mailchimp_newsletter + + def subscribe_to_mailchimp_newsletter + return unless email_newsletter + + Users::SubscribeToMailchimpNewsletterWorker.perform_async(user.id) + end end end diff --git a/app/models/users/setting.rb b/app/models/users/setting.rb index 85753e777..893c2717a 100644 --- a/app/models/users/setting.rb +++ b/app/models/users/setting.rb @@ -2,9 +2,9 @@ module Users class Setting < ApplicationRecord self.table_name_prefix = "users_" - belongs_to :user + belongs_to :user, touch: true + scope :with_feed, -> { where.not(feed_url: [nil, ""]) } - # TODO: @msarit Double-check how these suffixes have impacted the rest of the codebase enum editor_version: { v2: 0, v1: 1 }, _suffix: :editor enum config_font: { default: 0, comic_sans: 1, monospace: 2, open_dyslexic: 3, sans_serif: 4, serif: 5 }, _suffix: :font @@ -18,7 +18,23 @@ module Users validates :feed_referential_link, inclusion: { in: [true, false] } validates :feed_url, length: { maximum: 500 }, allow_nil: true validates :inbox_guidelines, length: { maximum: 250 }, allow_nil: true - end - # TODO: @msarit Re-add feed_url validation after updates are pointed directly to users_settings table + validate :validate_feed_url, if: :feed_url_changed? + + def resolved_font_name + config_font.gsub("default", Settings::UserExperience.default_font) + end + + private + + def validate_feed_url + return if feed_url.blank? + + valid = Feeds::ValidateUrl.call(feed_url) + + errors.add(:feed_url, "is not a valid RSS/Atom feed") unless valid + rescue StandardError => e + errors.add(:feed_url, e.message) + end + end end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index a850fbbca..280771ede 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -62,6 +62,10 @@ class UserPolicy < ApplicationPolicy true end + def onboarding_notifications_checkbox_update? + true + end + def update? current_user? end diff --git a/app/services/article_with_video_creation_service.rb b/app/services/article_with_video_creation_service.rb index 6a0ec2f1a..362b6623a 100644 --- a/app/services/article_with_video_creation_service.rb +++ b/app/services/article_with_video_creation_service.rb @@ -28,7 +28,7 @@ class ArticleWithVideoCreationService end def initial_article_with_params(article) - if @current_user.editor_version == "v1" + if @current_user.setting.editor_version == "v1" title = "Unpublished Video ~ #{rand(100_000).to_s(26)}" article.body_markdown = "---\ntitle: #{title}\npublished: false\ndescription: \ntags: \n---\n\n" else diff --git a/app/services/articles/builder.rb b/app/services/articles/builder.rb index fa11d2291..7190d088f 100644 --- a/app/services/articles/builder.rb +++ b/app/services/articles/builder.rb @@ -7,7 +7,7 @@ module Articles @tag = tag @prefill = prefill - @editor_version2 = @user&.editor_version == "v2" + @editor_version2 = @user&.setting&.editor_version == "v2" end def self.call(...) diff --git a/app/services/articles/feeds/large_forem_experimental.rb b/app/services/articles/feeds/large_forem_experimental.rb index 2e1987fb8..4e91dbbef 100644 --- a/app/services/articles/feeds/large_forem_experimental.rb +++ b/app/services/articles/feeds/large_forem_experimental.rb @@ -105,7 +105,8 @@ module Articles end def score_experience_level(article) - - (((article.experience_level_rating - (@user&.experience_level || 5)).abs / 2) * @experience_level_weight) + user_experience_level = @user&.setting&.experience_level || 5 + - (((article.experience_level_rating - user_experience_level).abs / 2) * @experience_level_weight) end def score_comments(article) diff --git a/app/services/email_digest.rb b/app/services/email_digest.rb index 02af23dd6..6ddd2533d 100644 --- a/app/services/email_digest.rb +++ b/app/services/email_digest.rb @@ -25,6 +25,8 @@ class EmailDigest private def get_users - User.registered.where(email_digest_periodic: true).where.not(email: "") + User.registered.joins(:notification_setting) + .where(notification_setting: { email_digest_periodic: true }) + .where.not(email: "") end end diff --git a/app/services/email_digest_article_collector.rb b/app/services/email_digest_article_collector.rb index 756048d20..9f517b323 100644 --- a/app/services/email_digest_article_collector.rb +++ b/app/services/email_digest_article_collector.rb @@ -13,7 +13,7 @@ class EmailDigestArticleCollector return [] unless should_receive_email? articles = if user_has_followings? - experience_level_rating = (@user.experience_level || 5) + experience_level_rating = (@user.setting.experience_level || 5) experience_level_rating_min = experience_level_rating - 3.6 experience_level_rating_max = experience_level_rating + 3.6 diff --git a/app/services/feeds/assemble_article_markdown.rb b/app/services/feeds/assemble_article_markdown.rb index c183dbbac..efab42335 100644 --- a/app/services/feeds/assemble_article_markdown.rb +++ b/app/services/feeds/assemble_article_markdown.rb @@ -20,7 +20,7 @@ module Feeds published: false date: #{@item.published} tags: #{get_tags} - canonical_url: #{@user.feed_mark_canonical ? @feed_source_url : ''} + canonical_url: #{@user.setting.feed_mark_canonical ? @feed_source_url : ''} --- #{assemble_body_markdown} @@ -64,7 +64,7 @@ module Feeds def thorough_parsing(content, feed_url) html_doc = Nokogiri::HTML(content) - find_and_replace_possible_links!(html_doc) if @user.feed_referential_link + find_and_replace_possible_links!(html_doc) if @user.setting.feed_referential_link find_and_replace_picture_tags_with_img!(html_doc) if feed_url&.include?("medium.com") diff --git a/app/services/feeds/import.rb b/app/services/feeds/import.rb index a57a58951..3397a2c91 100644 --- a/app/services/feeds/import.rb +++ b/app/services/feeds/import.rb @@ -6,7 +6,7 @@ module Feeds def initialize(users: nil, earlier_than: nil) # using nil here to avoid an unnecessary table count to check presence - @users = users || User.with_feed + @users = users || User.where(id: Users::Setting.with_feed.select(:user_id)) @earlier_than = earlier_than # NOTE: should these be configurable? Currently they are the result of empiric @@ -64,7 +64,7 @@ module Feeds # TODO: put this in separate service object def fetch_feeds(batch_of_users) - data = batch_of_users.pluck(:id, :feed_url) + data = batch_of_users.joins(:setting).pluck(:id, "users_settings.feed_url") result = Parallel.map(data, in_threads: num_fetchers) do |user_id, url| cleaned_url = url.to_s.strip @@ -149,7 +149,7 @@ module Feeds e, feeds_import_info: { username: user.username, - feed_url: user.feed_url, + feed_url: user.setting&.feed_url, item_count: item_count_error(feed), error: "Feeds::Import::CreateArticleError:#{item.url}" }, diff --git a/app/services/mailchimp/bot.rb b/app/services/mailchimp/bot.rb index 26a0e3602..f588080cb 100644 --- a/app/services/mailchimp/bot.rb +++ b/app/services/mailchimp/bot.rb @@ -25,7 +25,7 @@ module Mailchimp gibbon.lists(Settings::General.mailchimp_newsletter_id).members(target_md5_email).upsert( body: { email_address: user.email, - status: user.email_newsletter ? "subscribed" : "unsubscribed", + status: user.notification_setting.email_newsletter ? "subscribed" : "unsubscribed", merge_fields: { NAME: user.name.to_s, USERNAME: user.username.to_s, @@ -35,7 +35,7 @@ module Mailchimp ARTICLES: user.articles.size, COMMENTS: user.comments.size, ONBOARD_PK: user.onboarding_package_requested.to_s, - EXPERIENCE: user.experience_level || 666 + EXPERIENCE: user.setting.experience_level || 666 } }, ) @@ -68,7 +68,7 @@ module Mailchimp return false unless Settings::General.mailchimp_community_moderators_id.present? && user.has_role?(:trusted) success = false - status = user.email_community_mod_newsletter ? "subscribed" : "unsubscribed" + status = user.notification_setting.email_community_mod_newsletter ? "subscribed" : "unsubscribed" begin gibbon.lists(Settings::General.mailchimp_community_moderators_id).members(target_md5_email).upsert( body: { @@ -98,7 +98,7 @@ module Mailchimp tag_ids = user.roles.where(name: "tag_moderator").pluck(:resource_id) tag_names = Tag.where(id: tag_ids).pluck(:name) - status = user.email_tag_mod_newsletter ? "subscribed" : "unsubscribed" + status = user.notification_setting.email_tag_mod_newsletter ? "subscribed" : "unsubscribed" begin gibbon.lists(Settings::General.mailchimp_tag_moderators_id).members(target_md5_email).upsert( diff --git a/app/services/moderator/manage_activity_and_roles.rb b/app/services/moderator/manage_activity_and_roles.rb index 718314fc8..7b30cb120 100644 --- a/app/services/moderator/manage_activity_and_roles.rb +++ b/app/services/moderator/manage_activity_and_roles.rb @@ -33,9 +33,9 @@ module Moderator def remove_mod_roles @user.remove_role(:trusted) @user.remove_role(:tag_moderator) - @user.update(email_tag_mod_newsletter: false) + @user.notification_setting.update(email_tag_mod_newsletter: false) Mailchimp::Bot.new(user).manage_tag_moderator_list - @user.update(email_community_mod_newsletter: false) + @user.notification_setting.update(email_community_mod_newsletter: false) Mailchimp::Bot.new(user).manage_community_moderator_list end diff --git a/app/services/notifications/moderation.rb b/app/services/notifications/moderation.rb index aab4a4d6d..e7cab242b 100644 --- a/app/services/notifications/moderation.rb +++ b/app/services/notifications/moderation.rb @@ -4,9 +4,9 @@ module Notifications SUPPORTED = [Comment].freeze def self.available_moderators - User.with_role(:trusted) - .where("last_moderation_notification < ?", MODERATORS_AVAILABILITY_DELAY.ago) - .where(mod_roundrobin_notifications: true) + User.with_role(:trusted).joins(:notification_setting) + .where("last_moderation_notification < ?", 3.days.ago) + .where(notification_setting: { mod_roundrobin_notifications: true }) end end end diff --git a/app/services/notifications/new_comment/send.rb b/app/services/notifications/new_comment/send.rb index 38fac21a0..0d495608b 100644 --- a/app/services/notifications/new_comment/send.rb +++ b/app/services/notifications/new_comment/send.rb @@ -35,7 +35,9 @@ module Notifications end # Send PNs using Rpush - respecting users' notificaton delivery settings - targets = User.where(id: user_ids, mobile_comment_notifications: true).ids + targets = User.joins(:notification_setting) + .where(notification_setting: { mobile_comment_notifications: true }).ids + PushNotifications::Send.call( user_ids: targets, title: "@#{comment.user.username}", diff --git a/app/services/profiles/update.rb b/app/services/profiles/update.rb index e58384206..4c418d49f 100644 --- a/app/services/profiles/update.rb +++ b/app/services/profiles/update.rb @@ -24,8 +24,10 @@ module Profiles def initialize(user, updated_attributes) @user = user @profile = user.profile + @users_setting = user.setting @updated_profile_attributes = updated_attributes[:profile] || {} @updated_user_attributes = updated_attributes[:user].to_h || {} + @updated_users_setting_attributes = updated_attributes[:users_setting].to_h || {} @errors = [] @success = false end @@ -38,6 +40,7 @@ module Profiles else errors.concat(@profile.errors.full_messages) errors.concat(@user.errors.full_messages) + errors.concat(@users_setting.errors.full_messages) Honeycomb.add_field("error", errors_as_sentence) Honeycomb.add_field("errored", true) end @@ -62,6 +65,7 @@ module Profiles Profile.transaction do update_profile @user.update!(@updated_user_attributes) + @users_setting.update!(@updated_users_setting_attributes) end true rescue ActiveRecord::RecordInvalid diff --git a/app/services/tag_moderators/add.rb b/app/services/tag_moderators/add.rb index 8c53f0591..9c631fbcf 100644 --- a/app/services/tag_moderators/add.rb +++ b/app/services/tag_moderators/add.rb @@ -49,7 +49,9 @@ module TagModerators end def add_tag_mod_role(user, tag) - user.update(email_tag_mod_newsletter: true) unless user.email_tag_mod_newsletter? + unless user.notification_setting.email_tag_mod_newsletter? + user.notification_setting.update(email_tag_mod_newsletter: true) + end user.add_role(:tag_moderator, tag) Rails.cache.delete("user-#{user.id}/tag_moderators_list") return unless tag_mod_newsletter_enabled? diff --git a/app/services/tag_moderators/add_trusted_role.rb b/app/services/tag_moderators/add_trusted_role.rb index a1577869b..573ffb86a 100644 --- a/app/services/tag_moderators/add_trusted_role.rb +++ b/app/services/tag_moderators/add_trusted_role.rb @@ -4,7 +4,7 @@ module TagModerators return if user.has_role?(:trusted) || user.suspended? user.add_role(:trusted) - user.update(email_community_mod_newsletter: true) + user.notification_setting.update(email_community_mod_newsletter: true) Rails.cache.delete("user-#{user.id}/has_trusted_role") NotifyMailer.with(user: user).trusted_role_email.deliver_now return unless community_mod_newsletter_enabled? diff --git a/app/services/tag_moderators/remove.rb b/app/services/tag_moderators/remove.rb index 518b475dd..32d171af7 100644 --- a/app/services/tag_moderators/remove.rb +++ b/app/services/tag_moderators/remove.rb @@ -2,7 +2,9 @@ module TagModerators class Remove def self.call(user, tag) user.remove_role(:tag_moderator, tag) - user.update(email_tag_mod_newsletter: false) if user.email_tag_mod_newsletter? + if user.notification_setting.email_tag_mod_newsletter? + user.notification_setting.update(email_tag_mod_newsletter: false) + end Rails.cache.delete("user-#{user.id}/tag_moderators_list") return unless tag_mod_newsletter_enabled? diff --git a/app/views/mailers/digest_mailer/digest_email.html.erb b/app/views/mailers/digest_mailer/digest_email.html.erb index f2fdff220..5fd03b07d 100644 --- a/app/views/mailers/digest_mailer/digest_email.html.erb +++ b/app/views/mailers/digest_mailer/digest_email.html.erb @@ -19,7 +19,7 @@
- <% if @user.experience_level == nil %> + <% if @user.setting.experience_level.nil? %> You can now add your experience level to your account in order to receive more relevant content. diff --git a/app/views/shared/_logo_design.html.erb b/app/views/shared/_logo_design.html.erb index 91ec072f5..bb5788edc 100644 --- a/app/views/shared/_logo_design.html.erb +++ b/app/views/shared/_logo_design.html.erb @@ -1,7 +1,12 @@
- <% account.bg_color_hex = user_colors(account)[:bg] if account.bg_color_hex.blank? %> - <% account.text_color_hex = user_colors(account)[:text] if account.text_color_hex.blank? %> + <% if account.class.name == "User" %> + <% account.setting.brand_color1 = user_colors(account)[:bg] if account.setting && account.setting.brand_color1.blank? %> + <% account.setting.brand_color2 = user_colors(account)[:text] if account.setting && account.setting.brand_color2.blank? %> + <% else %> + <% account.bg_color_hex = user_colors(account)[:bg] if account.bg_color_hex.blank? %> + <% account.text_color_hex = user_colors(account)[:text] if account.text_color_hex.blank? %> + <% end %>
<%= f.label :bg_color_hex, "Brand color 1", class: "crayons-field__label" %>

Used for backgrounds, borders etc.

@@ -15,7 +20,11 @@
- <% account.bg_color_hex = user_colors(account)[:text] if account.text_color_hex.blank? %> + <% if account.class.name == "User" %> + <% account.setting.brand_color1 = user_colors(account)[:text] if account.setting && account.setting.brand_color2.blank? %> + <% else %> + <% account.bg_color_hex = user_colors(account)[:text] if account.text_color_hex.blank? %> + <% end %> <%= f.label :text_color_hex, "Brand color 2", class: "crayons-field__label" %>

Used for texts (usually put on Brand color 1).

diff --git a/app/views/shared/_profile_card_content.html.erb b/app/views/shared/_profile_card_content.html.erb index 321bb7de8..b065e81dd 100644 --- a/app/views/shared/_profile_card_content.html.erb +++ b/app/views/shared/_profile_card_content.html.erb @@ -19,7 +19,7 @@ <% if actor.class.name == "User" %>