Class: Chats::Participant
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Chats::Participant
- Defined in:
- lib/chats/models/participant.rb
Overview
A messager’s seat in a conversation. The messager is polymorphic — any ‘acts_as_messager` model can sit here (a User, an Organization, a support Agent) — and ALL per-member state lives on this row:
role "member" | "owner" (group creator/admin)
last_read_at the read horizon (see "Read state" below)
muted_at notifications muted (the gem still delivers messages;
hosts consult `notifiable?` in their notifier hook)
left_at soft-left groups (history kept, no new messages seen)
last_notified_at bookkeeping for "notify once per unread burst"
debounced notifications (see #should_notify?)
Read state: a horizon, not per-message receipts
We deliberately store ONE timestamp per participant instead of a per-message receipts table. A message is unread for you iff ‘created_at > your last_read_at`; you “read” a conversation by advancing the horizon. This gives unread counts, unread badges and “Seen” indicators with zero extra writes per message (a receipts table writes N rows per message per N participants — the classic chat-schema scaling trap), and it’s exactly how Basecamp’s Campfire models it. If a future use case truly needs per-message receipts (e.g. per-member “read by 7/9” in large groups), they can be added as a new table without breaking this API.
Constant Summary collapse
- ROLES =
%w[member owner].freeze
Instance Method Summary collapse
- #active? ⇒ Boolean
- #display_name ⇒ Object
- #leave! ⇒ Object
- #left? ⇒ Boolean
- #mark_notified!(at: Time.current) ⇒ Object
- #mute! ⇒ Object
- #muted? ⇒ Boolean
-
#notifiable_for?(message) ⇒ Boolean
Should the host notify this participant about
message? Encapsulates the etiquette every messaging product implements so each host doesn’t re-derive it: don’t notify yourself, the muted, the departed — and for debounced email digests, don’t notify twice for the same unread burst. - #owner? ⇒ Boolean
-
#read!(at: Time.current) ⇒ Object
Advance the read horizon to now and tell everyone who cares: - the conversation stream gets a fresh read-state payload (powers the “Seen” indicator on the other side, when read receipts are on) - this messager’s OWN inbox + badge refresh (their other devices/tabs should drop the unread highlight too) - the HOST gets a ‘:conversation_read` notifier event — but only when the horizon actually swallowed unread content.
-
#should_notify? ⇒ Boolean
For “email me only once until I come back” digests: true when there’s something unread AND we haven’t already notified since the last read.
- #unmute! ⇒ Object
- #unread? ⇒ Boolean
- #unread_count ⇒ Object
-
#unread_messages ⇒ Object
Messages this participant hasn’t seen: anything newer than their read horizon, excluding their own messages and deleted tombstones.
Instance Method Details
#active? ⇒ Boolean
48 |
# File 'lib/chats/models/participant.rb', line 48 def active? = left_at.nil? |
#display_name ⇒ Object
51 52 53 |
# File 'lib/chats/models/participant.rb', line 51 def display_name Chats.display_name_for() end |
#leave! ⇒ Object
106 107 108 |
# File 'lib/chats/models/participant.rb', line 106 def leave! update!(left_at: Time.current) end |
#left? ⇒ Boolean
47 |
# File 'lib/chats/models/participant.rb', line 47 def left? = left_at.present? |
#mark_notified!(at: Time.current) ⇒ Object
132 133 134 |
# File 'lib/chats/models/participant.rb', line 132 def mark_notified!(at: Time.current) update!(last_notified_at: at) end |
#mute! ⇒ Object
103 |
# File 'lib/chats/models/participant.rb', line 103 def mute! = update!(muted_at: Time.current) |
#muted? ⇒ Boolean
49 |
# File 'lib/chats/models/participant.rb', line 49 def muted? = muted_at.present? |
#notifiable_for?(message) ⇒ Boolean
Should the host notify this participant about message? Encapsulates the etiquette every messaging product implements so each host doesn’t re-derive it: don’t notify yourself, the muted, the departed — and for debounced email digests, don’t notify twice for the same unread burst.
116 117 118 119 120 121 |
# File 'lib/chats/models/participant.rb', line 116 def notifiable_for?() return false if left? || muted? return false if .sender == true end |
#owner? ⇒ Boolean
46 |
# File 'lib/chats/models/participant.rb', line 46 def owner? = role == "owner" |
#read!(at: Time.current) ⇒ Object
Advance the read horizon to now and tell everyone who cares:
- the conversation stream gets a fresh read-state payload (powers the
"Seen" indicator on the other side, when read receipts are on)
- this messager's OWN inbox + badge refresh (their other devices/tabs
should drop the unread highlight too)
- the HOST gets a `:conversation_read` notifier event — but only when
the horizon actually swallowed unread content. Hosts use it to keep
external notification surfaces truthful (e.g. mark this chat's rows
read in a notification center the moment the thread is read, so a
bell badge doesn't keep advertising messages the user has already
seen). Same notify hook as :message_created; error-isolated.
91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/chats/models/participant.rb', line 91 def read!(at: Time.current) return self if last_read_at && last_read_at >= at had_unread = unread? update!(last_read_at: at) broadcast_read_state if Chats.config.read_receipts Chats::Broadcasts.refresh_inbox_of() Chats::Broadcasts.update_badge_of() Chats.notify(:conversation_read, conversation: conversation, participant: self) if had_unread self end |
#should_notify? ⇒ Boolean
For “email me only once until I come back” digests: true when there’s something unread AND we haven’t already notified since the last read. Pair with ‘mark_notified!` after actually sending.
126 127 128 129 130 |
# File 'lib/chats/models/participant.rb', line 126 def should_notify? return false unless unread? last_notified_at.nil? || last_notified_at < (last_read_at || Conversation::EPOCH) end |
#unmute! ⇒ Object
104 |
# File 'lib/chats/models/participant.rb', line 104 def unmute! = update!(muted_at: nil) |
#unread? ⇒ Boolean
76 77 78 |
# File 'lib/chats/models/participant.rb', line 76 def unread? .exists? end |
#unread_count ⇒ Object
72 73 74 |
# File 'lib/chats/models/participant.rb', line 72 def unread_count .count end |
#unread_messages ⇒ Object
Messages this participant hasn’t seen: anything newer than their read horizon, excluding their own messages and deleted tombstones. The explicit IS NULL leg keeps senderless SYSTEM messages counted — SQL’s three-valued logic would otherwise drop them (NOT(NULL = …) is NULL, not TRUE). Same fix as Conversation.unread_by.
62 63 64 65 66 67 68 69 70 |
# File 'lib/chats/models/participant.rb', line 62 def conversation..visible .where("chats_messages.created_at > ?", last_read_at || Conversation::EPOCH) .where( "chats_messages.sender_type IS NULL OR " \ "NOT (chats_messages.sender_type = ? AND chats_messages.sender_id = ?)", , .to_s ) end |