Including models, migration, controllers, views & locales.
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
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"
@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])
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
##
--- /dev/null
+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
# 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
#
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
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
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
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
--- /dev/null
+# == 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
<li class="nav-item">
<%= link_to t(".oauth2_authorizations"), oauth_authorized_applications_path, :class => "nav-link #{'active' if controller_name == 'oauth2_authorized_applications'}" %>
</li>
+ <% if current_user.muted_users.any? %>
+ <li class="nav-item">
+ <%= link_to t(".muted_users"), user_mutes_path, :class => "nav-link #{'active' if controller_name == 'user_mutes'}" %>
+ </li>
+ <% end %>
</ul>
<% end %>
<% content_for :heading do %>
<h1><%= t("users.show.my messages") %></h1>
<ul class="nav nav-tabs">
- <% { ".my_inbox" => inbox_messages_path, ".my_outbox" => outbox_messages_path }.each do |i18n_key, path| %>
+ <% { ".my_inbox" => inbox_messages_path, ".my_outbox" => outbox_messages_path, ".muted_messages" => muted_messages_path }.each do |i18n_key, path| %>
+ <% next if path == muted_messages_path && current_user.muted_messages.none? %>
+
<li class="nav-item">
<% if path == active_link_path %>
<a class="nav-link active"><%= t(i18n_key) %></a>
<%= button_to t(".unread_button"), message_mark_path(message_summary, :mark => "unread"), :remote => true, :class => "btn btn-sm btn-primary", :form => { :class => "inbox-mark-unread", :hidden => !message_summary.message_read? } %>
<%= button_to t(".read_button"), message_mark_path(message_summary, :mark => "read"), :remote => true, :class => "btn btn-sm btn-primary", :form => { :class => "inbox-mark-read", :hidden => message_summary.message_read? } %>
<%= button_to t(".destroy_button"), message_path(message_summary, :referer => request.fullpath), :method => :delete, :remote => true, :class => "btn btn-sm btn-danger", :form_class => "inbox-destroy" %>
+ <% if message_summary.muted? %>
+ <%= button_to t(".unmute_button"), message_unmute_path(message_summary), :method => :patch, :class => "btn btn-sm btn-secondary" %>
+ <% end %>
</td>
</tr>
--- /dev/null
+<% content_for :head do %>
+ <%= javascript_include_tag "messages" %>
+<% end %>
+
+<%= render :partial => "heading", :locals => { :active_link_path => muted_messages_path } %>
+
+<h4><%= t ".messages", :count => current_user.muted_messages.size %></h4>
+
+<%= render :partial => "messages_table", :locals => { :columns => %w[from subject date], :messages => current_user.muted_messages, :inner_partial => "message_summary" } %>
--- /dev/null
+<% content_for :heading do %>
+ <h1><%= t ".my_muted_users" %></h1>
+<% end %>
+
+<%= render :partial => "settings_menu" %>
+
+<h4>
+ <%= t ".you_have_muted_n_users", :count => @muted_users.size %>
+</h4>
+<p>
+ <%= t ".user_mute_explainer" %>
+ <em><%= t ".user_mute_admins_and_moderators" %></em>
+</p>
+
+<% if @muted_users.any? %>
+ <table class="table">
+ <thead>
+ <tr>
+ <th><%= t ".table.thead.muted_user" %></th>
+ <th class="d-flex justify-content-end"><%= t ".table.thead.actions" %></th>
+ </tr>
+ </thead>
+ <tbody>
+ <% @muted_users.each do |user| %>
+ <tr>
+ <td>
+ <%= user_thumbnail_tiny user %>
+ <%= link_to user.display_name, user_path(user) %>
+ </td>
+ <td class="text-nowrap text-end">
+ <%= link_to t(".table.tbody.unmute"), user_mute_path(user), :method => :delete, :class => "btn btn-sm btn-primary" %>
+ <%= link_to t(".table.tbody.send_message"), new_message_path(user), :class => "btn btn-sm btn-secondary" %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+<% end %>
<%= report_link(t(".report"), @user) %>
</li>
<% end %>
+
+ <% if current_user and UserMute.exists?(owner: current_user, subject: @user) %>
+ <li>
+ <%= link_to t(".destroy_mute"), user_mute_path(@user), :method => :delete %>
+ </li>
+ <% elsif current_user %>
+ <li>
+ <%= link_to t(".create_mute"), user_mute_path(@user), :method => :post, :title => t("user_mutes.index.user_mute_explainer") %>
+ </li>
+ <% end %>
</ul>
</nav>
<% end %>
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"
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"
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}"
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:
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:
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"
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
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}"
# 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
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
--- /dev/null
+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
--- /dev/null
+class AddMutedFlagToMessages < ActiveRecord::Migration[7.0]
+ def change
+ add_column :messages, :muted, :boolean, :default => false, :null => false
+ end
+end
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
);
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: -
--
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: -
--
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: -
--
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: -
--
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: -
--
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: -
--
('20231117170422'),
('20231101222146'),
('20231029151516'),
+('20231010203028'),
+('20231010201451'),
('20231010194809'),
('20231007141103'),
('20230830115220'),
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