Skip to content

Commit db15e3c

Browse files
authored
Feature/message opt in (#955)
This pull request introduces an opt-in mechanism for platform members to receive direct messages, tightening permissions around who can be added as participants in new or updated conversations. It centralizes participant permission logic, updates error handling and feedback, and adds localization for new messaging rules. The changes affect controller logic, model preferences, policy enforcement, user interface, and translations, ensuring members have explicit control over their messaging privacy. **Messaging Permissions & Participant Filtering:** * Added a `receive_messages_from_members` preference to `BetterTogether::Person`, defaulting to false, allowing members to opt in to receive messages from non-managers. * Centralized participant permission logic in `ConversationPolicy#permitted_participants`, restricting regular members to only message platform managers or opted-in members. * Updated controller logic in `ConversationsController#create` and `#update` to filter participant IDs using the new policy and provide error feedback if only disallowed participants are submitted. [[1]](diffhunk://#diff-6b6a5b6094bf58416efbcacae28ee7f545bd56152102b55f2c10441efd0b0cd1L28-R54) [[2]](diffhunk://#diff-6b6a5b6094bf58416efbcacae28ee7f545bd56152102b55f2c10441efd0b0cd1L48-R107) [[3]](diffhunk://#diff-6b6a5b6094bf58416efbcacae28ee7f545bd56152102b55f2c10441efd0b0cd1L150-R230) [[4]](diffhunk://#diff-6b6a5b6094bf58416efbcacae28ee7f545bd56152102b55f2c10441efd0b0cd1L181-R246) **User Interface & Feedback:** * Added toggle switch to the person form for opting in to receive messages from platform members, with explanatory text. * Improved error display in the conversation form and added conditional messaging button to person profiles based on permissions. [[1]](diffhunk://#diff-d60c0c0b5850cdcc8d34927696501c783d1131819557241680c537bc15009b21L2-R4) [[2]](diffhunk://#diff-f8131ad40c5e33f27d8e421b69d10a7b8bb421c5b60311a8158df354bdfbabedR35-R40) **Localization & Error Messaging:** * Added error messages and hints in English, Spanish, and French for cases where no permitted participants are available, and for the new opt-in feature. [[1]](diffhunk://#diff-44438ce218f5287c58d0017f965d888715635d94280669896f75841fbd7b4cd7R716-R718) [[2]](diffhunk://#diff-44438ce218f5287c58d0017f965d888715635d94280669896f75841fbd7b4cd7R1193) [[3]](diffhunk://#diff-44438ce218f5287c58d0017f965d888715635d94280669896f75841fbd7b4cd7R1404-R1407) [[4]](diffhunk://#diff-44438ce218f5287c58d0017f965d888715635d94280669896f75841fbd7b4cd7R1797) [[5]](diffhunk://#diff-44438ce218f5287c58d0017f965d888715635d94280669896f75841fbd7b4cd7R1840-R1841) [[6]](diffhunk://#diff-4bbf4ee302c9607c80408361708b8b9fde3ee7afc5b505bfd69d429dd433f915R719-R721) [[7]](diffhunk://#diff-4bbf4ee302c9607c80408361708b8b9fde3ee7afc5b505bfd69d429dd433f915R1197) [[8]](diffhunk://#diff-4bbf4ee302c9607c80408361708b8b9fde3ee7afc5b505bfd69d429dd433f915R1412-R1415) [[9]](diffhunk://#diff-4bbf4ee302c9607c80408361708b8b9fde3ee7afc5b505bfd69d429dd433f915R1792) [[10]](diffhunk://#diff-4bbf4ee302c9607c80408361708b8b9fde3ee7afc5b505bfd69d429dd433f915R1836-R1837) [[11]](diffhunk://#diff-2c5ab6165f7efe573a84107e0e51102ad47cefb0c65629759d7458eee14326e7R724-R726) [[12]](diffhunk://#diff-2c5ab6165f7efe573a84107e0e51102ad47cefb0c65629759d7458eee14326e7R1203) [[13]](diffhunk://#diff-2c5ab6165f7efe573a84107e0e51102ad47cefb0c65629759d7458eee14326e7R1421-R1424) [[14]](diffhunk://#diff-2c5ab6165f7efe573a84107e0e51102ad47cefb0c65629759d7458eee14326e7R1824) [[15]](diffhunk://#diff-2c5ab6165f7efe573a84107e0e51102ad47cefb0c65629759d7458eee14326e7R1868-R1869) **Testing:** * Added model and policy specs to verify opt-in preference behavior and participant filtering. [[1]](diffhunk://#diff-3d46edd17462b82a6311eda5d720295228aca57aceb70d33513f87fa7e0c90d3R1-R16) [[2]](diffhunk://#diff-1d3db2f8c3a004bd5b4eb7c0deb75d92c8e5990f471dc6a43d7abfdac9bed667R1-R41)
2 parents 8398ef3 + 9fe4132 commit db15e3c

File tree

13 files changed

+358
-38
lines changed

13 files changed

+358
-38
lines changed

app/controllers/better_together/conversations_controller.rb

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,33 @@ def new
2525
end
2626

2727
def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
28-
@conversation = Conversation.new(conversation_params.merge(creator: helpers.current_person))
28+
# Check if user supplied only disallowed participants
29+
submitted_any = conversation_params[:participant_ids].present?
30+
filtered_params = conversation_params_filtered
31+
filtered_empty = Array(filtered_params[:participant_ids]).blank?
32+
33+
@conversation = Conversation.new(filtered_params.merge(creator: helpers.current_person))
2934

3035
authorize @conversation
3136

32-
if @conversation.save
37+
if submitted_any && filtered_empty
38+
@conversation.errors.add(:conversation_participants,
39+
t('better_together.conversations.errors.no_permitted_participants'))
40+
respond_to do |format|
41+
format.turbo_stream do
42+
render turbo_stream: turbo_stream.update(
43+
'form_errors',
44+
partial: 'layouts/better_together/errors',
45+
locals: { object: @conversation }
46+
), status: :unprocessable_entity
47+
end
48+
format.html do
49+
# Ensure sidebar has data when rendering the new template
50+
set_conversations
51+
render :new, status: :unprocessable_entity
52+
end
53+
end
54+
elsif @conversation.save
3355
@conversation.participants << helpers.current_person
3456

3557
respond_to do |format|
@@ -45,15 +67,44 @@ def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
4567
locals: { object: @conversation }
4668
)
4769
end
48-
format.html { render :new }
70+
format.html do
71+
# Ensure sidebar has data when rendering the new template
72+
set_conversations
73+
render :new
74+
end
4975
end
5076
end
5177
end
5278

53-
def update # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
79+
def update # rubocop:todo Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
5480
authorize @conversation
5581
ActiveRecord::Base.transaction do # rubocop:todo Metrics/BlockLength
56-
if @conversation.update(conversation_params)
82+
submitted_any = conversation_params[:participant_ids].present?
83+
filtered_params = conversation_params_filtered
84+
filtered_empty = Array(filtered_params[:participant_ids]).blank?
85+
86+
if submitted_any && filtered_empty
87+
@conversation.errors.add(:conversation_participants,
88+
t('better_together.conversations.errors.no_permitted_participants'))
89+
respond_to do |format|
90+
format.turbo_stream do
91+
render turbo_stream: turbo_stream.update(
92+
'form_errors',
93+
partial: 'layouts/better_together/errors',
94+
locals: { object: @conversation }
95+
), status: :unprocessable_entity
96+
end
97+
format.html do
98+
# Ensure sidebar has data when rendering the show template
99+
set_conversations
100+
# Ensure messages variables are set for the show template
101+
@messages = @conversation.messages.with_all_rich_text
102+
.includes(sender: [:string_translations]).order(:created_at)
103+
@message = @conversation.messages.build
104+
render :show, status: :unprocessable_entity
105+
end
106+
end
107+
elsif @conversation.update(filtered_params)
57108
@messages = @conversation.messages.with_all_rich_text.includes(sender: [:string_translations])
58109
.order(:created_at)
59110
@message = @conversation.messages.build
@@ -147,26 +198,40 @@ def leave_conversation # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
147198
private
148199

149200
def available_participants
150-
participants = Person.all
151-
152-
unless helpers.current_person.permitted_to?('manage_platform')
153-
# only allow messaging platform mangers unless you are a platform_manager
154-
participants = participants.where(id: platform_manager_ids)
155-
end
156-
157-
participants
201+
# Delegate to policy to centralize participant permission logic
202+
ConversationPolicy.new(helpers.current_user, Conversation.new).permitted_participants
158203
end
159204

160205
def conversation_params
161206
params.require(:conversation).permit(:title, participant_ids: [])
162207
end
163208

164-
def set_conversation
165-
@conversation = helpers.current_person.conversations.includes(participants: [
166-
:string_translations,
167-
:contact_detail,
168-
{ profile_image_attachment: :blob }
169-
]).find(params[:id])
209+
# Ensure participant_ids only include people the agent is allowed to message.
210+
# If none remain, keep it empty; creator is always added after create.
211+
def conversation_params_filtered # rubocop:todo Metrics/AbcSize
212+
permitted = ConversationPolicy.new(helpers.current_user, Conversation.new).permitted_participants
213+
permitted_ids = permitted.pluck(:id)
214+
# Always allow the current person (creator/participant) to appear in the list
215+
permitted_ids << helpers.current_person.id if helpers.current_person
216+
cp = conversation_params.dup
217+
if cp[:participant_ids].present?
218+
cp[:participant_ids] = Array(cp[:participant_ids]).map(&:presence).compact & permitted_ids
219+
end
220+
cp
221+
end
222+
223+
def set_conversation # rubocop:todo Metrics/MethodLength
224+
scope = helpers.current_person.conversations.includes(participants: [
225+
:string_translations,
226+
:contact_detail,
227+
{ profile_image_attachment: :blob }
228+
])
229+
@conversation = scope.find(params[:id])
230+
@set_conversation ||= Conversation.includes(participants: [
231+
:string_translations,
232+
:contact_detail,
233+
{ profile_image_attachment: :blob }
234+
]).find(params[:id])
170235
end
171236

172237
def set_conversations
@@ -178,9 +243,6 @@ def set_conversations
178243
]).order(updated_at: :desc).distinct(:id)
179244
end
180245

181-
def platform_manager_ids
182-
role = BetterTogether::Role.find_by(identifier: 'platform_manager')
183-
BetterTogether::PersonPlatformMembership.where(role_id: role.id).pluck(:member_id)
184-
end
246+
# platform_manager_ids now inferred by policy; kept here only if needed elsewhere
185247
end
186248
end

app/models/better_together/person.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def self.primary_community_delegation_attrs
7171
store_attributes :preferences do
7272
locale String, default: I18n.default_locale.to_s
7373
time_zone String, default: ENV.fetch('APP_TIME_ZONE', 'Newfoundland')
74+
receive_messages_from_members Boolean, default: false
7475
end
7576

7677
store_attributes :notification_preferences do

app/policies/better_together/conversation_policy.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ def leave_conversation?
2323
user.present? && record.participants.size > 1
2424
end
2525

26+
# Returns the people that the agent is permitted to message
27+
def permitted_participants
28+
if permitted_to?('manage_platform')
29+
BetterTogether::Person.all
30+
else
31+
role = BetterTogether::Role.find_by(identifier: 'platform_manager')
32+
manager_ids = BetterTogether::PersonPlatformMembership.where(role_id: role.id).pluck(:member_id)
33+
BetterTogether::Person.where(id: manager_ids)
34+
.or(BetterTogether::Person.where('preferences @> ?',
35+
{ receive_messages_from_members: true }.to_json))
36+
.distinct
37+
end
38+
end
39+
2640
class Scope < ApplicationPolicy::Scope
2741
end
2842
end

app/views/better_together/conversations/_form.html.erb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<%= form_with(model: conversation, data: { turbo: false, controller: 'better_together--form-validation' }) do |form| %>
2-
<%= turbo_frame_tag 'form_errors' %>
2+
<%= turbo_frame_tag 'form_errors' do %>
3+
<%= render 'layouts/better_together/errors', object: conversation %>
4+
<% end %>
35

46
<div class="mb-3">
57
<%= form.label :participant_ids, t('.add_participants') %>
@@ -15,4 +17,4 @@
1517
<div>
1618
<%= form.submit class: 'btn btn-sm btn-primary' %>
1719
</div>
18-
<% end %>
20+
<% end %>

app/views/better_together/people/_form.html.erb

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,20 @@
117117
<%= language_select_field(form:, selected_locale: person.locale) %>
118118
<small class="form-text text-muted"><%= t('helpers.hint.person.locale') %></small>
119119
</div>
120-
<div class="mb-3">
121-
<p><%= t('better_together.people.notification_preferences') %></p>
122-
<%= render partial: 'better_together/shared/fields/toggle_switch', locals: {form:, attr: :notify_by_email} %>
123-
<small class="form-text text-muted"><%= t('helpers.hint.person.notify_by_email') %></small>
124-
</div>
125-
<div class="mb-3">
126-
<%= render partial: 'better_together/shared/fields/toggle_switch', locals: {form:, attr: :show_conversation_details} %>
127-
<small class="form-text text-muted"><%= t('helpers.hint.person.show_conversation_details') %></small>
128-
</div>
120+
<div class="mb-3">
121+
<p><%= t('better_together.people.notification_preferences') %></p>
122+
<%= render partial: 'better_together/shared/fields/toggle_switch', locals: {form:, attr: :notify_by_email} %>
123+
<small class="form-text text-muted"><%= t('helpers.hint.person.notify_by_email') %></small>
124+
</div>
125+
<div class="mb-3">
126+
<%= render partial: 'better_together/shared/fields/toggle_switch', locals: {form:, attr: :show_conversation_details} %>
127+
<small class="form-text text-muted"><%= t('helpers.hint.person.show_conversation_details') %></small>
128+
</div>
129+
<div class="mb-3">
130+
<p><%= t('better_together.people.allow_messages_from_members') %></p>
131+
<%= render partial: 'better_together/shared/fields/toggle_switch', locals: {form:, attr: :receive_messages_from_members} %>
132+
<small class="form-text text-muted"><%= t('helpers.hint.person.allow_messages_from_members') %></small>
133+
</div>
129134
</div>
130135

131136
<!-- Device Permissions Tab -->

app/views/better_together/people/show.html.erb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
destroy_path: policy(@person).destroy? ? person_path(@person) : nil,
3333
destroy_confirm: t('people.confirm_delete'),
3434
destroy_aria_label: 'Delete Profile' %>
35+
<% conversation = BetterTogether::Conversation.new %>
36+
<% if policy(conversation).create? && policy(conversation).permitted_participants.include?(@person) %>
37+
<%= link_to new_conversation_path(conversation: { participant_ids: [@person.id] }), class: 'btn btn-outline-primary btn-sm me-2', 'aria-label' => t('globals.message') do %>
38+
<i class="fas fa-envelope"></i> <%= t('globals.message') %>
39+
<% end %>
40+
<% end %>
3541
</div>
3642
</div>
3743
</div>

bin/ci

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ export RAILS_ENV="${RAILS_ENV:-test}"
55
export NODE_ENV="${NODE_ENV:-test}"
66
export DISABLE_SPRING=1
77

8-
# Ensure DB is ready (uses DATABASE_URL)
9-
cd spec/dummy
8+
# Prepare the dummy app database (uses DATABASE_URL), but run specs from project root
9+
pushd spec/dummy >/dev/null
1010
bundle exec rails db:prepare
11+
popd >/dev/null
1112

1213
# Optional: assets if your specs need them
13-
# bundle exec rails assets:precompile || true
14+
# pushd spec/dummy >/dev/null && bundle exec rails assets:precompile || true; popd >/dev/null || true
1415

15-
# Run specs from dummy
16+
# Run specs from project root
1617
bundle exec rspec --format documentation

config/locales/en.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,9 @@ en:
713713
options_tooltip: Conversation Options
714714
empty:
715715
no_messages: No messages yet. Why not start the conversation?
716+
errors:
717+
no_permitted_participants: You can only add platform managers or members who
718+
have opted in to receive messages.
716719
form:
717720
add_participants: Add participants
718721
create_conversation: Create conversation
@@ -1187,6 +1190,7 @@ en:
11871190
index:
11881191
new_page: New page
11891192
people:
1193+
allow_messages_from_members: Allow messages from platform members
11901194
device_permissions:
11911195
camera: Camera
11921196
location: Location
@@ -1397,6 +1401,10 @@ en:
13971401
content: Content
13981402
conversation: :activerecord.models.conversation
13991403
conversation_participants: Conversation participants
1404+
conversations:
1405+
errors:
1406+
no_permitted_participants: You can only add platform managers or members who
1407+
have opted in to receive messages.
14001408
date:
14011409
abbr_day_names:
14021410
- Sun
@@ -1786,6 +1794,7 @@ en:
17861794
save: Save
17871795
hidden: Hidden
17881796
locale: Locale
1797+
message: Message
17891798
new: New
17901799
'no': 'No'
17911800
no_description: No description
@@ -1828,6 +1837,8 @@ en:
18281837
images:
18291838
cover_image: Cover image
18301839
person:
1840+
allow_messages_from_members: Opt-in to receive messages from people other
1841+
than platform managers.
18311842
cover_image: Upload a cover image to display at the top of the profile.
18321843
description: Provide a brief description or biography.
18331844
locale: Select the preferred language for the person.

config/locales/es.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,9 @@ es:
716716
options_tooltip: Opciones de Conversación
717717
empty:
718718
no_messages: Aún no hay mensajes. ¿Por qué no iniciar la conversación?
719+
errors:
720+
no_permitted_participants: Solo puedes agregar administradores de la plataforma
721+
o miembros que hayan optado por recibir mensajes.
719722
form:
720723
add_participants: Agregar participantes
721724
create_conversation: Crear conversación
@@ -1191,6 +1194,7 @@ es:
11911194
index:
11921195
new_page: Nueva página
11931196
people:
1197+
allow_messages_from_members: Permitir mensajes de miembros de la plataforma
11941198
device_permissions:
11951199
camera: Cámara
11961200
location: Ubicación
@@ -1405,6 +1409,10 @@ es:
14051409
content: Contenido
14061410
conversation: :activerecord.models.conversation
14071411
conversation_participants: Participantes de la conversación
1412+
conversations:
1413+
errors:
1414+
no_permitted_participants: Solo puedes agregar administradores de la plataforma
1415+
o miembros que hayan optado por recibir mensajes.
14081416
date:
14091417
abbr_day_names: '["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]'
14101418
abbr_month_names: '[nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
@@ -1781,6 +1789,7 @@ es:
17811789
save: Guardar
17821790
hidden: Oculto
17831791
locale: Idioma
1792+
message: Mensaje
17841793
new: Nuevo
17851794
'no': 'No'
17861795
no_description: Sin descripción
@@ -1824,6 +1833,8 @@ es:
18241833
images:
18251834
cover_image: Imagen de portada
18261835
person:
1836+
allow_messages_from_members: Optar por recibir mensajes de personas que no
1837+
sean administradores de la plataforma. than platform managers.
18271838
cover_image: Sube una imagen de portada para mostrar en la parte superior
18281839
del perfil.
18291840
description: Proporcione una breve descripción o biografía.

config/locales/fr.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,9 @@ fr:
721721
empty:
722722
no_messages: Aucun message pour l'instant. Pourquoi ne pas commencer la conversation
723723
?
724+
errors:
725+
no_permitted_participants: Vous ne pouvez ajouter que des gestionnaires de
726+
plateforme ou des membres ayant choisi de recevoir des messages.
724727
form:
725728
add_participants: Ajouter des participants
726729
create_conversation: Créer une conversation
@@ -1197,6 +1200,7 @@ fr:
11971200
index:
11981201
new_page: Nouvelle page
11991202
people:
1203+
allow_messages_from_members: Autoriser les messages des membres de la plateforme
12001204
device_permissions:
12011205
camera: Caméra
12021206
location: Localisation
@@ -1414,6 +1418,10 @@ fr:
14141418
content: Contenu
14151419
conversation: :activerecord.models.conversation
14161420
conversation_participants: Participants à la conversation
1421+
conversations:
1422+
errors:
1423+
no_permitted_participants: Vous ne pouvez ajouter que des gestionnaires de plateforme
1424+
ou des membres ayant choisi de recevoir des messages.
14171425
date:
14181426
abbr_day_names:
14191427
- dim
@@ -1813,6 +1821,7 @@ fr:
18131821
save: Enregistrer
18141822
hidden: Caché
18151823
locale: Locale
1824+
message: Message
18161825
new: Nouveau
18171826
'no': Non
18181827
no_description: Pas de description
@@ -1856,6 +1865,8 @@ fr:
18561865
images:
18571866
cover_image: Cover image
18581867
person:
1868+
allow_messages_from_members: Choisir de recevoir des messages provenant de
1869+
personnes autres que les gestionnaires de la plateforme. than platform managers.
18591870
cover_image: Téléchargez une image de couverture à afficher en haut du profil.
18601871
description: Fournissez une brève description ou biographie.
18611872
locale: Sélectionnez la langue préférée pour la personne.

0 commit comments

Comments
 (0)