From efc61f13159f2cf4e2ba542bac40ca84a57480da Mon Sep 17 00:00:00 2001 From: Gregory Igelmund Date: Wed, 13 Dec 2023 13:06:28 -0500 Subject: [PATCH] Add basic structures for UserMute and Message muting logic Including models, migration, controllers, views & locales. --- app/abilities/ability.rb | 3 +- app/controllers/messages_controller.rb | 26 ++++++- app/controllers/user_mutes_controller.rb | 45 +++++++++++ app/models/message.rb | 19 +++++ app/models/user.rb | 8 +- app/models/user_mute.rb | 34 +++++++++ app/views/application/_settings_menu.html.erb | 5 ++ app/views/messages/_heading.html.erb | 4 +- app/views/messages/_message_summary.html.erb | 3 + app/views/messages/muted.html.erb | 9 +++ app/views/user_mutes/index.html.erb | 38 ++++++++++ app/views/users/show.html.erb | 10 +++ config/locales/en.yml | 48 ++++++++++-- config/routes.rb | 9 +++ .../20231010201451_create_user_mutes.rb | 15 ++++ ...231010203028_add_muted_flag_to_messages.rb | 5 ++ db/structure.sql | 75 ++++++++++++++++++- script/deliver-message | 2 +- 18 files changed, 346 insertions(+), 12 deletions(-) create mode 100644 app/controllers/user_mutes_controller.rb create mode 100644 app/models/user_mute.rb create mode 100644 app/views/messages/muted.html.erb create mode 100644 app/views/user_mutes/index.html.erb create mode 100644 db/migrate/20231010201451_create_user_mutes.rb create mode 100644 db/migrate/20231010203028_add_muted_flag_to_messages.rb diff --git a/app/abilities/ability.rb b/app/abilities/ability.rb index f9348f68e..b9da5d08a 100644 --- a/app/abilities/ability.rb +++ b/app/abilities/ability.rb @@ -47,13 +47,14 @@ class Ability can [:show], :dashboard can [:new, :create, :edit, :update, :comment, :subscribe, :unsubscribe], DiaryEntry can [:make_friend, :remove_friend], Friendship - can [:new, :create, :reply, :show, :inbox, :outbox, :mark, :destroy], Message + can [:new, :create, :reply, :show, :inbox, :outbox, :muted, :mark, :unmute, :destroy], Message can [:close, :reopen], Note can [:show, :edit, :update], :preference can [:edit, :update], :profile can [:new, :create], Report can [:mine, :new, :create, :edit, :update, :destroy], Trace can [:account, :go_public], User + can [:index, :create, :destroy], UserMute if user.moderator? can [:hide, :unhide, :hidecomment, :unhidecomment], DiaryEntry diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index adb53b43b..2ca86fc02 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -47,7 +47,7 @@ class MessagesController < ApplicationController render :action => "new" elsif @message.save flash[:notice] = t ".message_sent" - UserMailer.message_notification(@message).deliver_later + UserMailer.message_notification(@message).deliver_later if @message.notify_recipient? redirect_to :action => :inbox else @title = t "messages.new.title" @@ -107,6 +107,13 @@ class MessagesController < ApplicationController @title = t ".title" end + # Display the list of muted messages received by the user. + def muted + @title = t ".title" + + redirect_to inbox_messages_path if current_user.muted_messages.none? + end + # Set the message as being read or unread. def mark @message = Message.where(:recipient => current_user).or(Message.where(:sender => current_user)).find(params[:message_id]) @@ -127,6 +134,23 @@ class MessagesController < ApplicationController render :action => "no_such_message", :status => :not_found end + # Moves message into Inbox by unsetting the muted-flag + def unmute + message = current_user.muted_messages.find(params[:message_id]) + + if message.unmute + flash[:notice] = t(".notice") + else + flash[:error] = t(".error") + end + + if current_user.muted_messages.none? + redirect_to inbox_messages_path + else + redirect_to muted_messages_path + end + end + private ## diff --git a/app/controllers/user_mutes_controller.rb b/app/controllers/user_mutes_controller.rb new file mode 100644 index 000000000..2068ab6a3 --- /dev/null +++ b/app/controllers/user_mutes_controller.rb @@ -0,0 +1,45 @@ +class UserMutesController < ApplicationController + include UserMethods + + layout "site" + + before_action :authorize_web + before_action :set_locale + + authorize_resource + + before_action :lookup_user, :only => [:create, :destroy] + before_action :check_database_readable + before_action :check_database_writable, :only => [:create, :destroy] + + def index + @muted_users = current_user.muted_users + @title = t ".title" + + redirect_to edit_account_path unless @muted_users.any? + end + + def create + user_mute = current_user.mutes.build(:subject => @user) + + if user_mute.save + flash[:notice] = t(".notice", :name => user_mute.subject.display_name) + else + flash[:error] = t(".error", :name => user_mute.subject.display_name, :full_message => user_mute.errors.full_messages.to_sentence.humanize) + end + + redirect_back_or_to user_mutes_path(current_user) + end + + def destroy + user_mute = current_user.mutes.find_by!(:subject => @user) + + if user_mute.destroy + flash[:notice] = t(".notice", :name => user_mute.subject.display_name) + else + flash[:error] = t(".error") + end + + redirect_back_or_to user_mutes_path(current_user) + end +end diff --git a/app/models/message.rb b/app/models/message.rb index 7c12769d3..665e2d721 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -12,6 +12,7 @@ # to_user_visible :boolean default(TRUE), not null # from_user_visible :boolean default(TRUE), not null # body_format :enum default("markdown"), not null +# muted :boolean default(FALSE), not null # # Indexes # @@ -32,6 +33,10 @@ class Message < ApplicationRecord validates :body, :sent_on, :presence => true validates :title, :body, :characters => true + scope :muted, -> { where(:muted => true) } + + before_create :set_muted + def self.from_mail(mail, from, to) if mail.multipart? if mail.text_part @@ -65,4 +70,18 @@ class Message < ApplicationRecord sha256 << id.to_s Base64.urlsafe_encode64(sha256.digest)[0, 8] end + + def notify_recipient? + !muted? + end + + def unmute + update(:muted => false) + end + + private + + def set_muted + self.muted ||= UserMute.active?(:owner => recipient, :subject => sender) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 1942a25cc..5790d81e5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -51,9 +51,10 @@ class User < ApplicationRecord has_many :diary_comments, -> { order(:created_at => :desc) }, :inverse_of => :user has_many :diary_entry_subscriptions, :class_name => "DiaryEntrySubscription" has_many :diary_subscriptions, :through => :diary_entry_subscriptions, :source => :diary_entry - has_many :messages, -> { where(:to_user_visible => true).order(:sent_on => :desc).preload(:sender, :recipient) }, :foreign_key => :to_user_id - has_many :new_messages, -> { where(:to_user_visible => true, :message_read => false).order(:sent_on => :desc) }, :class_name => "Message", :foreign_key => :to_user_id + has_many :messages, -> { where(:to_user_visible => true, :muted => false).order(:sent_on => :desc).preload(:sender, :recipient) }, :foreign_key => :to_user_id + has_many :new_messages, -> { where(:to_user_visible => true, :muted => false, :message_read => false).order(:sent_on => :desc) }, :class_name => "Message", :foreign_key => :to_user_id has_many :sent_messages, -> { where(:from_user_visible => true).order(:sent_on => :desc).preload(:sender, :recipient) }, :class_name => "Message", :foreign_key => :from_user_id + has_many :muted_messages, -> { where(:to_user_visible => true, :muted => true).order(:sent_on => :desc).preload(:sender, :recipient) }, :class_name => "Message", :foreign_key => :to_user_id has_many :friendships, -> { joins(:befriendee).where(:users => { :status => %w[active confirmed] }) } has_many :friends, :through => :friendships, :source => :befriendee has_many :tokens, :class_name => "UserToken", :dependent => :destroy @@ -75,6 +76,9 @@ class User < ApplicationRecord has_many :blocks_created, :class_name => "UserBlock", :foreign_key => :creator_id, :inverse_of => :creator has_many :blocks_revoked, :class_name => "UserBlock", :foreign_key => :revoker_id, :inverse_of => :revoker + has_many :mutes, -> { order(:created_at => :desc) }, :class_name => "UserMute", :foreign_key => :owner_id, :inverse_of => :owner + has_many :muted_users, :through => :mutes, :source => :subject + has_many :roles, :class_name => "UserRole" has_many :issues, :class_name => "Issue", :foreign_key => :reported_user_id, :inverse_of => :reported_user diff --git a/app/models/user_mute.rb b/app/models/user_mute.rb new file mode 100644 index 000000000..9bee39b8d --- /dev/null +++ b/app/models/user_mute.rb @@ -0,0 +1,34 @@ +# == Schema Information +# +# Table name: user_mutes +# +# id :bigint(8) not null, primary key +# owner_id :bigint(8) not null +# subject_id :bigint(8) not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_user_mutes_on_owner_id_and_subject_id (owner_id,subject_id) UNIQUE +# +# Foreign Keys +# +# fk_rails_... (owner_id => users.id) +# fk_rails_... (subject_id => users.id) +# +class UserMute < ApplicationRecord + belongs_to :owner, :class_name => "User" + belongs_to :subject, :class_name => "User" + + validates :subject, :uniqueness => { :scope => :owner_id, :message => :is_already_muted } + + def self.active?(owner:, subject:) + !subject.administrator? && + !subject.moderator? && + exists?( + :owner => owner, + :subject => subject + ) + end +end diff --git a/app/views/application/_settings_menu.html.erb b/app/views/application/_settings_menu.html.erb index 9ce9755a2..8477a11a0 100644 --- a/app/views/application/_settings_menu.html.erb +++ b/app/views/application/_settings_menu.html.erb @@ -14,5 +14,10 @@ + <% if current_user.muted_users.any? %> + + <% end %> <% end %> diff --git a/app/views/messages/_heading.html.erb b/app/views/messages/_heading.html.erb index 12c77d3f5..90995ed88 100644 --- a/app/views/messages/_heading.html.erb +++ b/app/views/messages/_heading.html.erb @@ -3,7 +3,9 @@ <% content_for :heading do %>

<%= t("users.show.my messages") %>

<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index c68e67622..7ff2bfd06 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -40,6 +40,12 @@ en: messages: invalid_email_address: does not appear to be a valid e-mail address email_address_not_routable: is not routable + models: + user_mute: + attributes: + subject: + format: "%{message}" + is_already_muted: "is already muted" # Translates all the model names, which is used in error handling on the website models: acl: "Access Control List" @@ -1673,8 +1679,6 @@ en: messages: inbox: title: "Inbox" - my_inbox: "My Inbox" - my_outbox: "My Outbox" messages: "You have %{new_messages} and %{old_messages}" new_messages: one: "%{count} new message" @@ -1695,6 +1699,7 @@ en: read_button: "Mark as read" reply_button: "Reply" destroy_button: "Delete" + unmute_button: "Move to Inbox" new: title: "Send message" send_message_to_html: "Send a new message to %{name}" @@ -1708,14 +1713,17 @@ en: body: "Sorry there is no message with that id." outbox: title: "Outbox" - my_inbox: "My Inbox" - my_outbox: "My Outbox" actions: "Actions" messages: one: "You have %{count} sent message" other: "You have %{count} sent messages" no_sent_messages_html: "You have no sent messages yet. Why not get in touch with some of the %{people_mapping_nearby_link}?" people_mapping_nearby: "people mapping nearby" + muted: + title: "Muted Messages" + messages: + one: "%{count} muted message" + other: "You have %{count} muted messages" reply: wrong_user: "You are logged in as `%{user}' but the message you have asked to reply to was not sent to that user. Please login as the correct user in order to reply." show: @@ -1727,12 +1735,16 @@ en: wrong_user: "You are logged in as `%{user}' but the message you have asked to read was not sent by or to that user. Please login as the correct user in order to read it." sent_message_summary: destroy_button: "Delete" - heading: + heading: my_inbox: "My Inbox" my_outbox: "My Outbox" + muted_messages: "Muted messages" mark: as_read: "Message marked as read" as_unread: "Message marked as unread" + unmute: + notice: "Message has been moved to Inbox" + error: "The message could not be moved to the Inbox." destroy: destroyed: "Message deleted" passwords: @@ -2562,6 +2574,7 @@ en: oauth1_settings: OAuth 1 settings oauth2_applications: OAuth 2 applications oauth2_authorizations: OAuth 2 authorizations + muted_users: Muted Users oauth: authorize: title: "Authorize access to your account" @@ -2749,6 +2762,8 @@ en: my_dashboard: My Dashboard blocks on me: Blocks on Me blocks by me: Blocks by Me + create_mute: Mute this User + destroy_mute: Unmute this User edit_profile: Edit Profile send message: Send Message diary: Diary @@ -2939,6 +2954,29 @@ en: showing_page: "Page %{page}" next: "Next »" previous: "« Previous" + user_mutes: + index: + title: "Muted Users" + my_muted_users: "My muted users" + you_have_muted_n_users: + one: "You have muted %{count} User" + other: "You have muted %{count} users" + user_mute_explainer: "Messages of muted users are moved into a separate Inbox and you won't receive email notifications." + user_mute_admins_and_moderators: "You can mute Admins and Moderators but their messages will not be muted." + table: + thead: + muted_user: "Muted User" + actions: "Actions" + tbody: + unmute: "Unmute" + send_message: "Send message" + + create: + notice: "You muted %{name}." + error: "%{name} could not be muted. %{full_message}." + destroy: + notice: "You unmuted %{name}." + error: "User could not be unmuted. Please try again." notes: index: title: "Notes submitted or commented on by %{user}" diff --git a/config/routes.rb b/config/routes.rb index 2b67e360e..110a67a49 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -282,9 +282,12 @@ OpenStreetMap::Application.routes.draw do # messages resources :messages, :only => [:create, :show, :destroy] do post :mark + patch :unmute + match :reply, :via => [:get, :post] collection do get :inbox + get :muted get :outbox end end @@ -293,6 +296,12 @@ OpenStreetMap::Application.routes.draw do get "/message/new/:display_name" => "messages#new", :as => "new_message" get "/message/read/:message_id", :to => redirect(:path => "/messages/%{message_id}") + # muting users + scope "/user/:display_name" do + resource :user_mute, :only => [:create, :destroy], :path => "mute" + end + resources :user_mutes, :only => [:index] + # oauth admin pages (i.e: for setting up new clients, etc...) scope "/user/:display_name" do resources :oauth_clients diff --git a/db/migrate/20231010201451_create_user_mutes.rb b/db/migrate/20231010201451_create_user_mutes.rb new file mode 100644 index 000000000..8cb6ff87e --- /dev/null +++ b/db/migrate/20231010201451_create_user_mutes.rb @@ -0,0 +1,15 @@ +class CreateUserMutes < ActiveRecord::Migration[7.0] + def change + create_table :user_mutes do |t| + t.references :owner, :null => false, :index => false + t.references :subject, :null => false, :index => false + + t.timestamps + + t.foreign_key :users, :column => :owner_id + t.foreign_key :users, :column => :subject_id + + t.index [:owner_id, :subject_id], :unique => true + end + end +end diff --git a/db/migrate/20231010203028_add_muted_flag_to_messages.rb b/db/migrate/20231010203028_add_muted_flag_to_messages.rb new file mode 100644 index 000000000..e517aea86 --- /dev/null +++ b/db/migrate/20231010203028_add_muted_flag_to_messages.rb @@ -0,0 +1,5 @@ +class AddMutedFlagToMessages < ActiveRecord::Migration[7.0] + def change + add_column :messages, :muted, :boolean, :default => false, :null => false + end +end diff --git a/db/structure.sql b/db/structure.sql index ba60918f0..0563417cd 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -951,7 +951,8 @@ CREATE TABLE public.messages ( to_user_id bigint NOT NULL, to_user_visible boolean DEFAULT true NOT NULL, from_user_visible boolean DEFAULT true NOT NULL, - body_format public.format_enum DEFAULT 'markdown'::public.format_enum NOT NULL + body_format public.format_enum DEFAULT 'markdown'::public.format_enum NOT NULL, + muted boolean DEFAULT false NOT NULL ); @@ -1454,6 +1455,38 @@ CREATE SEQUENCE public.user_blocks_id_seq ALTER SEQUENCE public.user_blocks_id_seq OWNED BY public.user_blocks.id; +-- +-- Name: user_mutes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_mutes ( + id bigint NOT NULL, + owner_id bigint NOT NULL, + subject_id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: user_mutes_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_mutes_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_mutes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_mutes_id_seq OWNED BY public.user_mutes.id; + + -- -- Name: user_preferences; Type: TABLE; Schema: public; Owner: - -- @@ -1835,6 +1868,13 @@ ALTER TABLE ONLY public.reports ALTER COLUMN id SET DEFAULT nextval('public.repo ALTER TABLE ONLY public.user_blocks ALTER COLUMN id SET DEFAULT nextval('public.user_blocks_id_seq'::regclass); +-- +-- Name: user_mutes id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_mutes ALTER COLUMN id SET DEFAULT nextval('public.user_mutes_id_seq'::regclass); + + -- -- Name: user_roles id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2216,6 +2256,14 @@ ALTER TABLE ONLY public.user_blocks ADD CONSTRAINT user_blocks_pkey PRIMARY KEY (id); +-- +-- Name: user_mutes user_mutes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_mutes + ADD CONSTRAINT user_mutes_pkey PRIMARY KEY (id); + + -- -- Name: user_preferences user_preferences_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2734,6 +2782,13 @@ CREATE INDEX index_reports_on_user_id ON public.reports USING btree (user_id); CREATE INDEX index_user_blocks_on_user_id ON public.user_blocks USING btree (user_id); +-- +-- Name: index_user_mutes_on_owner_id_and_subject_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_user_mutes_on_owner_id_and_subject_id ON public.user_mutes USING btree (owner_id, subject_id); + + -- -- Name: messages_from_user_id_idx; Type: INDEX; Schema: public; Owner: - -- @@ -3107,6 +3162,14 @@ ALTER TABLE ONLY public.oauth_access_grants ADD CONSTRAINT fk_rails_330c32d8d9 FOREIGN KEY (resource_owner_id) REFERENCES public.users(id) NOT VALID; +-- +-- Name: user_mutes fk_rails_591dad3359; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_mutes + ADD CONSTRAINT fk_rails_591dad3359 FOREIGN KEY (owner_id) REFERENCES public.users(id); + + -- -- Name: oauth_access_tokens fk_rails_732cb83ab7; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -3155,6 +3218,14 @@ ALTER TABLE ONLY public.oauth_applications ADD CONSTRAINT fk_rails_cc886e315a FOREIGN KEY (owner_id) REFERENCES public.users(id) NOT VALID; +-- +-- Name: user_mutes fk_rails_e9dd4fb6c3; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_mutes + ADD CONSTRAINT fk_rails_e9dd4fb6c3 FOREIGN KEY (subject_id) REFERENCES public.users(id); + + -- -- Name: oauth_access_tokens fk_rails_ee63f25419; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -3514,6 +3585,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20231117170422'), ('20231101222146'), ('20231029151516'), +('20231010203028'), +('20231010201451'), ('20231010194809'), ('20231007141103'), ('20230830115220'), diff --git a/script/deliver-message b/script/deliver-message index 28d755b24..81de3ef58 100755 --- a/script/deliver-message +++ b/script/deliver-message @@ -33,6 +33,6 @@ mail = Mail.new($stdin.read message = Message.from_mail(mail, from, to) message.save! -UserMailer.message_notification(message).deliver +UserMailer.message_notification(message).deliver if message.notify_recipient? exit 0 -- 2.39.5