Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/assets/stylesheets/application/messages.css
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,11 @@
color: var(--color-text-muted, #666);
font-size: 0.95em;
}

&[data-unread="true"] .txt-muted {
color: var(--color-text);
font-weight: 800;
}
}

.thread__avatar {
Expand Down
5 changes: 3 additions & 2 deletions app/controllers/rooms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def ensure_can_administer

def find_messages
messages = @room.messages
.with_threads
.with_rich_text_body_and_embeds
.with_attached_attachment
.preload(creator: :avatar_attachment)
Expand All @@ -81,13 +82,13 @@ def find_messages
if @room.thread? && @room.parent_message.present?
if result.empty?
# Empty thread - show just the parent message
result = [@room.parent_message]
result = [ @room.parent_message ]
elsif result.any?
# Thread has messages - prepend parent if we're showing the first message
first_thread_message = @room.messages.ordered.first
messages_array = result.to_a
if first_thread_message && messages_array.first.id == first_thread_message.id
result = [@room.parent_message] + messages_array
result = [ @room.parent_message ] + messages_array
end
end
end
Expand Down
24 changes: 24 additions & 0 deletions app/models/membership.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ class Membership < ApplicationRecord
belongs_to :room
belongs_to :user

def self.broadcast_involvement_to(user_id:, room_id:, involvement:)
ActionCable.server.broadcast "user_#{user_id}_involvements", { roomId: room_id, involvement: involvement }
end

has_many :unread_notifications, ->(membership) {
scope = since(membership.unread_at || Time.current)

Expand Down Expand Up @@ -50,6 +54,7 @@ class Membership < ApplicationRecord
}

after_update_commit { user.reset_remote_connections if deactivated? }
after_update_commit :promote_thread_memberships, if: :starred_parent_room?
after_destroy_commit { user.reset_remote_connections }

enum involvement: %w[ invisible nothing mentions everything ].index_by(&:itself), _prefix: :involved_in
Expand Down Expand Up @@ -123,6 +128,25 @@ def ensure_receives_mentions!

private

def starred_parent_room?
saved_change_to_involvement? && involved_in_everything? && !room.thread?
end

def promote_thread_memberships
thread_ids = []
ApplicationRecord.transaction do
room.threads.find_each do |thread|
updated = thread.memberships.where(user_id: user_id, involvement: "invisible")
.update_all(involvement: "mentions", updated_at: Time.current)
thread_ids << thread.id if updated.positive?
end
end
# Manual broadcast since update_all bypasses callbacks
thread_ids.each do |tid|
Membership.broadcast_involvement_to(user_id: user_id, room_id: tid, involvement: "mentions")
end
end

def broadcast_read
ActionCable.server.broadcast "user_#{user_id}_reads", { room_id: room_id }
end
Expand Down
8 changes: 8 additions & 0 deletions app/models/room.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ def display_name(for_user: nil)
end
end

def unread_for?(user)
memberships
.where(user_id: user.id)
.where.not(involvement: :invisible)
.where.not(unread_at: nil)
.exists?
end

private
def set_sortable_name
self.sortable_name = name.to_s.gsub(/[[:^ascii:]\p{So}]/, "").strip.downcase
Expand Down
21 changes: 21 additions & 0 deletions app/models/rooms/thread.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,32 @@
class Rooms::Thread < Room
validates_presence_of :parent_message

after_create_commit :promote_starred_users_to_visible

def default_involvement(user: nil)
if user.present? && (user == creator || user == parent_message&.creator)
"everything"
else
"invisible"
end
end

private
def promote_starred_users_to_visible
return unless parent_message&.room

user_ids = parent_message.room.memberships.active.involved_in_everything.pluck(:user_id)
scope = memberships.where(user_id: user_ids, involvement: "invisible")
affected_user_ids = scope.pluck(:user_id)
return if affected_user_ids.empty?

ApplicationRecord.transaction do
scope.update_all(involvement: "mentions", updated_at: Time.current)
end

# Manual broadcast since update_all bypasses callbacks
affected_user_ids.each do |uid|
Membership.broadcast_involvement_to(user_id: uid, room_id: id, involvement: "mentions")
end
end
end
2 changes: 1 addition & 1 deletion app/views/messages/_threads.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<% ordered_participants = participant_users.map { |id| participants[id] }.compact %>
<% reply_count = thread.messages_count || thread.messages.active.count %>

<%= link_to room_path(thread), class: "thread__link flex-inline align-center gap", data: { turbo_frame: "_top", updated_at: thread.last_active_at.iso8601 } do %>
<%= link_to room_path(thread), class: "thread__link flex-inline align-center gap", data: { turbo_frame: "_top", updated_at: thread.last_active_at.iso8601, unread: thread.unread_for?(Current.user) } do %>
<div class="thread__avatars flex-inline gap">
<% ordered_participants.each do |user| %>
<figure class="avatar thread__avatar flex-item-no-shrink" data-stop-propagation="true">
Expand Down
10 changes: 10 additions & 0 deletions campfire-mods.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ For us, threads work like message boards in Basecamp, and they've been great at
- https://github.com/antiwork/smallbets/compare/7333c40abc545c1900d4e23cfcef0fb557b2290e...9ad6e5d0a57957907a5911a30778212dabfa5e48


## Unread thread indicators
Members can see which threads have unread messages by looking for a bold "Last reply" timestamp. This only shows for threads they participate in (authored parent message, replied, mentioned, or starred the parent room). Users who star a room are automatically added as participants in all threads in that room.

- [`app/models/room.rb`](app/models/room.rb) - `unread_for?(user)` method
- [`app/models/rooms/thread.rb`](app/models/rooms/thread.rb) - Promotion callback
- [`app/models/membership.rb`](app/models/membership.rb) - Starring promotion + broadcasts
- [`app/views/messages/_threads.html.erb`](app/views/messages/_threads.html.erb) - Data attribute
- [`app/assets/stylesheets/application/messages.css`](app/assets/stylesheets/application/messages.css) - Styling


## Block pings
Members can block users from sending them direct messages. Admins can monitor which members are getting blocked.

Expand Down