Skip to content

Commit 449a82b

Browse files
committed
Mention and team visibility improvements
Add settings both for user mentions (anybody, only team members) and teams (private, visible, public) so people can have better visibility/spam control.
1 parent 3c01a65 commit 449a82b

16 files changed

Lines changed: 491 additions & 33 deletions

File tree

app/assets/stylesheets/components/settings.css

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,3 +329,56 @@
329329
margin-left: var(--spacing-2);
330330
font-style: italic;
331331
}
332+
333+
/* Radio button groups */
334+
.settings-page .radio-group {
335+
display: flex;
336+
flex-direction: column;
337+
gap: var(--spacing-3);
338+
}
339+
340+
.settings-page .radio-group label {
341+
display: flex;
342+
align-items: flex-start;
343+
gap: var(--spacing-3);
344+
cursor: pointer;
345+
padding: var(--spacing-3) var(--spacing-4);
346+
border: var(--border-width) solid var(--color-border);
347+
border-radius: var(--border-radius-md);
348+
background: var(--color-bg-card);
349+
transition: border-color var(--transition-fast), background-color var(--transition-fast);
350+
}
351+
352+
.settings-page .radio-group label:hover {
353+
border-color: var(--color-primary-300);
354+
background: var(--color-bg-hover);
355+
}
356+
357+
.settings-page .radio-group label:has(input:checked) {
358+
border-color: var(--color-primary-500);
359+
background: var(--color-primary-50);
360+
}
361+
362+
.settings-page .radio-group input[type="radio"] {
363+
width: auto;
364+
margin: var(--spacing-1) 0 0 0;
365+
padding: 0;
366+
flex-shrink: 0;
367+
}
368+
369+
.settings-page .radio-group .radio-text {
370+
flex: 1;
371+
}
372+
373+
.settings-page .radio-group .radio-text strong {
374+
display: block;
375+
color: var(--color-text-primary);
376+
font-weight: var(--font-weight-semibold);
377+
}
378+
379+
.settings-page .radio-group .radio-description {
380+
display: block;
381+
color: var(--color-text-secondary);
382+
font-size: var(--font-size-sm);
383+
margin-top: var(--spacing-1);
384+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module Settings
4+
class PreferencesController < Settings::BaseController
5+
def update
6+
if current_user.update(preferences_params)
7+
redirect_to settings_profile_path, notice: "Preferences updated"
8+
else
9+
redirect_to settings_profile_path, alert: current_user.errors.full_messages.to_sentence
10+
end
11+
end
12+
13+
private
14+
15+
def preferences_params
16+
params.require(:user).permit(:mention_restriction)
17+
end
18+
end
19+
end

app/controllers/settings/teams_controller.rb

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
module Settings
44
class TeamsController < Settings::BaseController
5-
before_action :set_team, only: [:show, :destroy]
6-
before_action :require_team_member!, only: [:show]
7-
before_action :require_team_admin!, only: [:destroy]
8-
skip_before_action :require_authentication, only: [:index]
5+
before_action :set_team, only: [:show, :update, :destroy]
6+
before_action :require_team_accessible!, only: [:show]
7+
before_action :require_team_admin!, only: [:update, :destroy]
8+
skip_before_action :require_authentication, only: [:index, :show]
99

1010
def index
1111
@your_teams = user_signed_in? ? current_user.teams.includes(team_members: :user) : []
@@ -14,8 +14,9 @@ def index
1414

1515
def show
1616
@team_members = @team.team_members.includes(:user)
17+
@is_member = user_signed_in? && @team.member?(current_user)
1718
@can_manage = user_signed_in? && @team.admin?(current_user)
18-
@can_invite = user_signed_in? && @team.admin?(current_user)
19+
@can_invite = @can_manage
1920
end
2021

2122
def create
@@ -29,6 +30,14 @@ def create
2930
redirect_to settings_teams_path, alert: e.record.errors.full_messages.to_sentence
3031
end
3132

33+
def update
34+
if @team.update(team_update_params)
35+
redirect_to settings_team_path(@team), notice: "Team settings updated"
36+
else
37+
redirect_to settings_team_path(@team), alert: @team.errors.full_messages.to_sentence
38+
end
39+
end
40+
3241
def destroy
3342
@team.destroy
3443
redirect_to settings_teams_path, notice: "Team deleted"
@@ -48,19 +57,24 @@ def team_params
4857
params.require(:team).permit(:name)
4958
end
5059

60+
def team_update_params
61+
params.require(:team).permit(:visibility)
62+
end
63+
5164
def require_team_admin!
5265
unless user_signed_in? && @team.admin?(current_user)
5366
redirect_to settings_team_path(@team), alert: "Admins only" and return
5467
end
5568
end
5669

57-
def require_team_member!
58-
unless user_signed_in?
70+
def require_team_accessible!
71+
return if @team.accessible_to?(current_user)
72+
73+
if user_signed_in?
74+
render_404
75+
else
5976
redirect_to new_session_path, alert: "Please sign in"
60-
return
6177
end
62-
63-
render_404 unless @team.member?(current_user)
6478
end
6579
end
6680
end

app/models/team.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ class Team < ApplicationRecord
44
has_many :team_members, dependent: :destroy
55
has_many :users, through: :team_members
66

7+
# Visibility levels:
8+
# - private: only members can see/access the team, only members can mention
9+
# - visible: anyone can see/access the team, only members can mention
10+
# - open: anyone can see/access the team, anyone can mention
11+
enum :visibility, { private: "private", visible: "visible", open: "open" }, default: :private, prefix: true
12+
713
validates :name, presence: true
814
validates :name, format: { with: /\A[a-zA-Z0-9_\-\.]+\z/ }
915

@@ -25,6 +31,16 @@ def last_admin?(team_member)
2531
team_members.role_admin.count == 1
2632
end
2733

34+
def accessible_to?(user)
35+
return true if visibility_visible? || visibility_open?
36+
member?(user)
37+
end
38+
39+
def mentionable_by?(user)
40+
return true if visibility_open?
41+
member?(user)
42+
end
43+
2844
private
2945

3046
def reserve_name

app/models/user.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,25 @@ class User < ApplicationRecord
1414
has_many :topic_stars, dependent: :destroy
1515
has_many :starred_topics, through: :topic_stars, source: :topic
1616

17+
enum :mention_restriction, { anyone: "anyone", teammates_only: "teammates_only" }, default: :anyone
18+
1719
scope :active, -> { where(deleted_at: nil) }
1820

1921
def primary_alias
2022
person&.default_alias
2123
end
2224

25+
def mentionable_by?(mentioner)
26+
return false unless mentioner
27+
return true if anyone?
28+
shares_team_with?(mentioner)
29+
end
30+
31+
def shares_team_with?(other_user)
32+
return false unless other_user
33+
team_ids.intersect?(other_user.team_ids)
34+
end
35+
2336
validates :username, format: { with: /\A[a-zA-Z0-9_\-\.]+\z/, allow_blank: true }
2437
validates :username, presence: true, on: :registration
2538

app/services/note_builder.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,26 @@ def resolve_mentions(names)
103103
when "Team" then teams[reservation.owner_id]
104104
end
105105
raise Error, "Unknown mention: @#{name}" unless record
106+
validate_mention_permission!(record, name)
106107
record
107108
end
108109

109110
mentionables.compact.uniq
110111
end
111112

113+
def validate_mention_permission!(mentionable, name)
114+
case mentionable
115+
when User
116+
unless mentionable.mentionable_by?(author)
117+
raise Error, "You cannot mention @#{name} (only their teammates can mention them)"
118+
end
119+
when Team
120+
unless mentionable.mentionable_by?(author)
121+
raise Error, "You cannot mention @#{name} (only team members can mention this team)"
122+
end
123+
end
124+
end
125+
112126
def fan_out!(note:, mentionables:, mark_author_read:)
113127
recipient_ids = recipients_for(note, mentionables:)
114128
payload = {

app/views/settings/profiles/show.html.slim

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,21 @@
1010
= f.label :username
1111
= f.text_field :username, placeholder: "your_name", value: current_user.username
1212
= f.submit "Save username", class: "button-primary"
13+
14+
.settings-section
15+
h2 Mention Settings
16+
p.settings-hint Control who can @mention you in notes.
17+
= form_with model: current_user, url: settings_preferences_path, method: :patch, local: true do |f|
18+
.form-group
19+
.radio-group
20+
label
21+
= f.radio_button :mention_restriction, "anyone"
22+
span.radio-text
23+
strong Anyone
24+
span.radio-description Any user can @mention you in notes
25+
label
26+
= f.radio_button :mention_restriction, "teammates_only"
27+
span.radio-text
28+
strong Teammates only
29+
span.radio-description Only users who share a team with you can @mention you
30+
= f.submit "Save", class: "button-primary"

app/views/settings/teams/show.html.slim

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,43 @@
55
.team-actions
66
= button_to "Delete team", settings_team_path(@team), method: :delete, data: { turbo_confirm: "Delete this team?" }, class: "button-danger"
77

8+
.team-settings
9+
h2 Team Settings
10+
.visibility-setting
11+
h3 Visibility
12+
- if @can_manage
13+
= form_with model: @team, url: settings_team_path(@team), method: :patch, local: true do |f|
14+
.form-group
15+
.radio-group
16+
label
17+
= f.radio_button :visibility, "private"
18+
span.radio-text
19+
strong Private
20+
span.radio-description Only team members can see this team and mention @#{@team.name}
21+
label
22+
= f.radio_button :visibility, "visible"
23+
span.radio-text
24+
strong Visible
25+
span.radio-description Anyone can see this team, but only members can mention @#{@team.name}
26+
label
27+
= f.radio_button :visibility, "open"
28+
span.radio-text
29+
strong Open
30+
span.radio-description Anyone can see this team and mention @#{@team.name}
31+
= f.submit "Save", class: "button-primary"
32+
- elsif @is_member
33+
p.settings-hint
34+
- case @team.visibility
35+
- when "private"
36+
| This team is <strong>private</strong>. Only members can see it and mention @#{@team.name}.
37+
- when "visible"
38+
| This team is <strong>visible</strong>. Anyone can see it, but only members can mention @#{@team.name}.
39+
- when "open"
40+
| This team is <strong>open</strong>. Anyone can see it and mention @#{@team.name}.
41+
- unless @can_manage
42+
br
43+
| Only admins can change this setting.
44+
845
.team-members
946
h2 Members
1047
- if @team_members.any?
@@ -21,13 +58,14 @@
2158
- else
2259
p No members yet.
2360

24-
.invite-section
25-
h2 Add member
26-
- unless @can_invite
27-
p.settings-hint Only team admins can add members.
28-
= form_with url: settings_team_team_members_path(@team), method: :post, local: true do |f|
29-
fieldset disabled=(!@can_invite)
30-
.form-group
31-
= label_tag :username, "Username"
32-
= text_field_tag :username, nil, required: true, placeholder: "existing_username"
33-
= f.submit "Add", class: "button-primary"
61+
- if @is_member
62+
.invite-section
63+
h2 Add member
64+
- unless @can_invite
65+
p.settings-hint Only team admins can add members.
66+
= form_with url: settings_team_team_members_path(@team), method: :post, local: true do |f|
67+
fieldset disabled=(!@can_invite)
68+
.form-group
69+
= label_tag :username, "Username"
70+
= text_field_tag :username, nil, required: true, placeholder: "existing_username"
71+
= f.submit "Add", class: "button-primary"

config/routes.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@
2828
resource :import, only: [:show, :create]
2929
resource :deletion, only: [:show, :create]
3030

31-
resources :teams, only: [:index, :show, :create, :destroy] do
31+
resources :teams, only: [:index, :show, :create, :update, :destroy] do
3232
resources :team_members, only: [:create, :destroy]
3333
end
3434

3535
resource :username, only: [:update]
36+
resource :preferences, only: [:update]
3637
patch "password/current", to: "passwords#update_current", as: :update_current_password
3738
resources :emails, only: [:create, :destroy] do
3839
post :primary, on: :member
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class AddVisibilityToTeams < ActiveRecord::Migration[8.0]
2+
def change
3+
create_enum :team_visibility, %w[private visible open]
4+
add_column :teams, :visibility, :enum, enum_type: :team_visibility, default: "private", null: false
5+
end
6+
end

0 commit comments

Comments
 (0)