Skip to content

Commit 4df093a

Browse files
committed
Allow removal of unwanted mentions
Mainly for team admins to moderate team tags, but also for users to remove spam. For now, we only delete the mention entry, we do not allow to change the note otherwise, even for team admins.
1 parent 0d0546f commit 4df093a

6 files changed

Lines changed: 207 additions & 1 deletion

File tree

app/assets/stylesheets/components/notes.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,38 @@
9090
}
9191

9292
.note-mention {
93+
display: inline-flex;
94+
align-items: center;
95+
gap: var(--spacing-1);
9396
background: var(--color-gray-50);
9497
padding: 2px 8px;
9598
border-radius: var(--border-radius-sm);
9699
font-size: var(--font-size-xs);
97100
color: var(--color-text-primary);
98101
}
99102

103+
.mention-remove-button {
104+
display: inline-flex;
105+
align-items: center;
106+
justify-content: center;
107+
width: 14px;
108+
height: 14px;
109+
padding: 0;
110+
margin-left: 2px;
111+
background: var(--color-gray-200);
112+
border: none;
113+
border-radius: 50%;
114+
color: var(--color-text-secondary);
115+
font-size: 8px;
116+
cursor: pointer;
117+
transition: background-color var(--transition-fast), color var(--transition-fast);
118+
}
119+
120+
.mention-remove-button:hover {
121+
background: var(--color-danger-bg-hover);
122+
color: var(--color-danger-text);
123+
}
124+
100125
.note-edit-toggle summary {
101126
cursor: pointer;
102127
color: var(--color-text-link);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
class NoteMentionsController < ApplicationController
4+
before_action :require_authentication
5+
before_action :set_mention
6+
7+
def destroy
8+
authorize_removal!
9+
return if performed?
10+
11+
@mention.destroy
12+
13+
redirect_back fallback_location: topic_path(@mention.note.topic), notice: "Mention removed"
14+
end
15+
16+
private
17+
18+
def set_mention
19+
@mention = NoteMention.find(params[:id])
20+
end
21+
22+
def authorize_removal!
23+
mentionable = @mention.mentionable
24+
25+
case mentionable
26+
when User
27+
unless mentionable.id == current_user.id
28+
redirect_back fallback_location: topic_path(@mention.note.topic), alert: "You can only remove your own mentions"
29+
end
30+
when Team
31+
unless mentionable.admin?(current_user)
32+
redirect_back fallback_location: topic_path(@mention.note.topic), alert: "Only team admins can remove team mentions"
33+
end
34+
else
35+
redirect_back fallback_location: topic_path(@mention.note.topic), alert: "Cannot remove this mention"
36+
end
37+
end
38+
end

app/helpers/application_helper.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,18 @@ def note_mention_label(mention)
7575
"@unknown"
7676
end
7777
end
78+
79+
def can_remove_note_mention?(mention, user)
80+
return false unless user
81+
mentionable = mention.mentionable
82+
83+
case mentionable
84+
when User
85+
mentionable.id == user.id
86+
when Team
87+
mentionable.admin?(user)
88+
else
89+
false
90+
end
91+
end
7892
end

app/views/notes/_note.html.slim

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
.note-mentions
1717
span.note-mentions-label Mentions:
1818
- note.note_mentions.each do |mention|
19-
span.note-mention = note_mention_label(mention)
19+
span.note-mention
20+
= note_mention_label(mention)
21+
- if can_remove_note_mention?(mention, current_user)
22+
= button_to note_mention_path(mention), method: :delete, class: "mention-remove-button", title: "Remove this mention", form: { data: { turbo_confirm: "Remove yourself from this note's mentions?" } } do
23+
i.fa-solid.fa-xmark
2024

2125
- if current_user&.id == note.author_id
2226
.note-actions-row

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
post :mark_all_read, on: :collection
6161
end
6262
resources :notes, only: [:create, :update, :destroy]
63+
resources :note_mentions, only: [:destroy]
6364
get "stats", to: "stats#show", as: :stats
6465
get "stats/data", to: "stats#data", as: :stats_data
6566

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
require "rails_helper"
2+
3+
RSpec.describe "NoteMentions", type: :request do
4+
def sign_in(email:, password: "secret")
5+
post session_path, params: { email: email, password: password }
6+
expect(response).to redirect_to(root_path)
7+
end
8+
9+
def attach_verified_alias(user, email:, primary: true)
10+
al = create(:alias, user: user, email: email)
11+
if primary && user.person&.default_alias_id.nil?
12+
user.person.update!(default_alias_id: al.id)
13+
end
14+
Alias.by_email(email).update_all(verified_at: Time.current)
15+
al
16+
end
17+
18+
let!(:topic) { create(:topic) }
19+
let!(:author) { create(:user, password: "secret", password_confirmation: "secret") }
20+
let!(:mentioned_user) { create(:user, password: "secret", password_confirmation: "secret") }
21+
let!(:other_user) { create(:user, password: "secret", password_confirmation: "secret") }
22+
let!(:team) { create(:team) }
23+
let!(:team_admin) { create(:user, password: "secret", password_confirmation: "secret") }
24+
let!(:team_member) { create(:user, password: "secret", password_confirmation: "secret") }
25+
26+
let!(:note) { Note.create!(topic: topic, author: author, body: "A note") }
27+
let!(:user_mention) { NoteMention.create!(note: note, mentionable: mentioned_user) }
28+
let!(:team_mention) { NoteMention.create!(note: note, mentionable: team) }
29+
30+
before do
31+
create(:team_member, team: team, user: team_admin, role: "admin")
32+
create(:team_member, team: team, user: team_member, role: "member")
33+
end
34+
35+
describe "DELETE /note_mentions/:id" do
36+
context "when user removes their own mention" do
37+
before do
38+
attach_verified_alias(mentioned_user, email: "mentioned@example.com")
39+
sign_in(email: "mentioned@example.com")
40+
end
41+
42+
it "allows the mentioned user to remove their own mention" do
43+
expect {
44+
delete note_mention_path(user_mention)
45+
}.to change(NoteMention, :count).by(-1)
46+
47+
expect(response).to redirect_to(topic_path(topic))
48+
expect(flash[:notice]).to eq("Mention removed")
49+
end
50+
end
51+
52+
context "when user tries to remove another user's mention" do
53+
before do
54+
attach_verified_alias(other_user, email: "other@example.com")
55+
sign_in(email: "other@example.com")
56+
end
57+
58+
it "prevents removing another user's mention" do
59+
expect {
60+
delete note_mention_path(user_mention)
61+
}.not_to change(NoteMention, :count)
62+
63+
expect(response).to redirect_to(topic_path(topic))
64+
expect(flash[:alert]).to eq("You can only remove your own mentions")
65+
end
66+
end
67+
68+
context "when team admin removes team mention" do
69+
before do
70+
attach_verified_alias(team_admin, email: "teamadmin@example.com")
71+
sign_in(email: "teamadmin@example.com")
72+
end
73+
74+
it "allows team admin to remove team mention" do
75+
expect {
76+
delete note_mention_path(team_mention)
77+
}.to change(NoteMention, :count).by(-1)
78+
79+
expect(response).to redirect_to(topic_path(topic))
80+
expect(flash[:notice]).to eq("Mention removed")
81+
end
82+
end
83+
84+
context "when team member (non-admin) tries to remove team mention" do
85+
before do
86+
attach_verified_alias(team_member, email: "teammember@example.com")
87+
sign_in(email: "teammember@example.com")
88+
end
89+
90+
it "prevents non-admin team members from removing team mention" do
91+
expect {
92+
delete note_mention_path(team_mention)
93+
}.not_to change(NoteMention, :count)
94+
95+
expect(response).to redirect_to(topic_path(topic))
96+
expect(flash[:alert]).to eq("Only team admins can remove team mentions")
97+
end
98+
end
99+
100+
context "when non-team member tries to remove team mention" do
101+
before do
102+
attach_verified_alias(other_user, email: "other@example.com")
103+
sign_in(email: "other@example.com")
104+
end
105+
106+
it "prevents non-members from removing team mention" do
107+
expect {
108+
delete note_mention_path(team_mention)
109+
}.not_to change(NoteMention, :count)
110+
111+
expect(response).to redirect_to(topic_path(topic))
112+
expect(flash[:alert]).to eq("Only team admins can remove team mentions")
113+
end
114+
end
115+
116+
context "when not signed in" do
117+
it "requires authentication" do
118+
delete note_mention_path(user_mention)
119+
120+
expect(response).to redirect_to(new_session_path)
121+
end
122+
end
123+
end
124+
end

0 commit comments

Comments
 (0)