chats 0.5.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +171 -0
- data/README.md +40 -7
- data/app/assets/stylesheets/chats.css +131 -6
- data/app/controllers/chats/conversations_controller.rb +106 -11
- data/app/helpers/chats/engine_helper.rb +15 -0
- data/app/javascript/chats/dock_controller.js +15 -1
- data/app/javascript/chats/split_controller.js +131 -0
- data/app/javascript/chats/thread_controller.js +5 -0
- data/app/views/chats/conversations/_conversation_pane.html.erb +53 -0
- data/app/views/chats/conversations/_conversation_row.html.erb +20 -4
- data/app/views/chats/conversations/_empty_pane.html.erb +23 -0
- data/app/views/chats/conversations/_group.html.erb +20 -4
- data/app/views/chats/conversations/_thread.html.erb +22 -8
- data/app/views/chats/conversations/_thread_header.html.erb +83 -0
- data/app/views/chats/conversations/index.html.erb +81 -14
- data/app/views/chats/conversations/pane.html.erb +14 -0
- data/app/views/chats/conversations/show.html.erb +11 -65
- data/app/views/chats/dock/_dock.html.erb +2 -1
- data/config/importmap.rb +1 -0
- data/config/locales/en.yml +4 -0
- data/config/locales/es.yml +4 -0
- data/docs/screenshots/split-inbox-empty.png +0 -0
- data/docs/screenshots/split-inbox-phone-list.png +0 -0
- data/docs/screenshots/split-inbox-phone-thread.png +0 -0
- data/docs/screenshots/split-inbox.png +0 -0
- data/lib/chats/configuration.rb +36 -0
- data/lib/chats/models/conversation.rb +10 -2
- data/lib/chats/models/message.rb +19 -3
- data/lib/chats/version.rb +1 -1
- data/lib/generators/chats/templates/initializer.rb +17 -0
- metadata +11 -3
- data/chats-0.3.1.gem +0 -0
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
re-renders this page per-viewer — see Chats::Broadcasts for why refreshes
|
|
4
4
|
beat surgical row patches here.
|
|
5
5
|
|
|
6
|
+
Two panes (`config.inbox_layout = :split`, the default): the list in a
|
|
7
|
+
<nav> on the left, and on the right the `chats_conversation` frame with
|
|
8
|
+
the open conversation (/messages/:id renders this same template, see
|
|
9
|
+
ConversationsController#show) or the empty pane (/messages). chats.css
|
|
10
|
+
decides with one media query: at least 700px wide both panes show; below
|
|
11
|
+
that the page is the list (/messages) or the thread (/messages/:id), as
|
|
12
|
+
with `:stacked`, which renders the list alone and the thread on its own
|
|
13
|
+
page (show.html.erb). Every request renders the same markup; only CSS
|
|
14
|
+
knows the width, so it works before JavaScript runs and without it.
|
|
15
|
+
|
|
16
|
+
A row is a real link to /messages/:id. On a wide screen it targets the
|
|
17
|
+
frame: the server answers with the pane alone (pane.html.erb) and the
|
|
18
|
+
frame's data-turbo-action="advance" moves the address bar and history
|
|
19
|
+
there. A reload, a deep link or back/forward renders the whole page.
|
|
20
|
+
The frame is `disabled` until chats--split enables it, and only while
|
|
21
|
+
the screen is wide: on a phone, and without JavaScript, the same link
|
|
22
|
+
opens the thread page. The pane keeps its draft, scroll and stream
|
|
23
|
+
through every refresh of the list (see _conversation_pane).
|
|
24
|
+
|
|
6
25
|
Rows come from Chats::Inbox and are either a Chats::Conversation or a
|
|
7
26
|
Chats::InboxGroup (every direct thread with a `inbox: :grouped` messager,
|
|
8
27
|
stacked into one row).
|
|
@@ -31,6 +50,9 @@
|
|
|
31
50
|
`.restore` visit that honors this. Requires the host layout to
|
|
32
51
|
`yield :head` (the Rails convention); harmless no-op otherwise.
|
|
33
52
|
https://turbo.hotwired.dev/handbook/building#opting-out-of-caching %>
|
|
53
|
+
<%# The page's title, for a layout that shows `yield(:title)`: the inbox's,
|
|
54
|
+
or the open conversation's (chats_page_title, config.page_title). %>
|
|
55
|
+
<% content_for :title, chats_page_title(@conversation) %>
|
|
34
56
|
<% content_for :head do %>
|
|
35
57
|
<meta name="turbo-cache-control" content="no-cache">
|
|
36
58
|
<% end %>
|
|
@@ -39,8 +61,9 @@
|
|
|
39
61
|
<%= turbo_stream_from chats_current_messager, :chats_inbox %>
|
|
40
62
|
</div>
|
|
41
63
|
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
<% split = chats_two_panes? %>
|
|
65
|
+
<% inbox_empty = @rows.none? && params[:q].blank? %>
|
|
66
|
+
<% list = capture do %>
|
|
44
67
|
<header class="chats-inbox__header">
|
|
45
68
|
<h1 class="chats-inbox__title"><%= t("chats.inbox.title") %></h1>
|
|
46
69
|
</header>
|
|
@@ -53,15 +76,17 @@
|
|
|
53
76
|
<% end %>
|
|
54
77
|
|
|
55
78
|
<% if Chats.config.search && !@inbox.filtered? %>
|
|
56
|
-
<%#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
<%# No `action` (so a bare tag.form: form_with always writes one): a form
|
|
80
|
+
without one submits to the page's own URL (the HTML standard, and
|
|
81
|
+
Turbo too), whatever that is NOW — /messages,
|
|
82
|
+
/messages/ (the engine answers both), or /messages/:id after a row
|
|
83
|
+
opened a conversation in the pane and moved the address bar. So the
|
|
84
|
+
URL a search puts in the address bar is still this page to Turbo,
|
|
85
|
+
the next refresh morphs it instead of re-rendering it from scratch,
|
|
86
|
+
and on the two-pane page the open conversation stays open. %>
|
|
87
|
+
<%= tag.form method: :get,
|
|
62
88
|
class: "chats-search",
|
|
63
|
-
|
|
64
|
-
html: { role: "search" },
|
|
89
|
+
role: "search",
|
|
65
90
|
data: {
|
|
66
91
|
controller: "chats--debounced-submit",
|
|
67
92
|
action: "input->chats--debounced-submit#queue search->chats--debounced-submit#queue",
|
|
@@ -88,12 +113,17 @@
|
|
|
88
113
|
<ul class="chats-inbox__list">
|
|
89
114
|
<% @rows.each do |row| %>
|
|
90
115
|
<% if row.is_a?(Chats::InboxGroup) %>
|
|
91
|
-
<%= render "chats/conversations/group", group: row, viewer: chats_current_messager
|
|
116
|
+
<%= render "chats/conversations/group", group: row, viewer: chats_current_messager,
|
|
117
|
+
pane: split, current: @conversation,
|
|
118
|
+
filter: chats_inbox_filter %>
|
|
92
119
|
<% else %>
|
|
93
120
|
<%= render "chats/conversations/conversation_row",
|
|
94
121
|
conversation: row,
|
|
95
122
|
viewer: chats_current_messager,
|
|
96
|
-
unread_count: @unread_counts.fetch(row.id, 0)
|
|
123
|
+
unread_count: @unread_counts.fetch(row.id, 0),
|
|
124
|
+
pane: split,
|
|
125
|
+
current: @conversation,
|
|
126
|
+
filter: chats_inbox_filter %>
|
|
97
127
|
<% end %>
|
|
98
128
|
<% end %>
|
|
99
129
|
</ul>
|
|
@@ -102,7 +132,8 @@
|
|
|
102
132
|
<p class="chats-empty__title"><%= t("chats.inbox.no_results_title") %></p>
|
|
103
133
|
<p class="chats-empty__hint"><%= t("chats.inbox.no_results_hint", query: params[:q]) %></p>
|
|
104
134
|
</div>
|
|
105
|
-
<%
|
|
135
|
+
<% elsif !split %>
|
|
136
|
+
<%# (Two panes: the empty pane says it, once — chats/conversations/_empty_pane.) %>
|
|
106
137
|
<div class="chats-empty">
|
|
107
138
|
<span class="chats-empty__icon" aria-hidden="true">💬</span>
|
|
108
139
|
<p class="chats-empty__title"><%= t("chats.inbox.empty_title") %></p>
|
|
@@ -111,4 +142,40 @@
|
|
|
111
142
|
</div>
|
|
112
143
|
<% end %>
|
|
113
144
|
<% end %>
|
|
114
|
-
|
|
145
|
+
<% end %>
|
|
146
|
+
|
|
147
|
+
<%# data-chats-dock-hide: the messaging dock hides itself on this page. %>
|
|
148
|
+
<% if split %>
|
|
149
|
+
<%= tag.div class: "chats chats-inbox chats-split",
|
|
150
|
+
data: {
|
|
151
|
+
chats_dock_hide: true,
|
|
152
|
+
controller: "chats--split",
|
|
153
|
+
action: "turbo:before-prefetch->chats--split#prefetch " \
|
|
154
|
+
"turbo:frame-load->chats--split#opened " \
|
|
155
|
+
"chats:read->chats--split#threadRead " \
|
|
156
|
+
"turbo:before-morph-attribute->chats--split#keepEnabled"
|
|
157
|
+
} do %>
|
|
158
|
+
<nav class="chats-split__list" aria-label="<%= t("chats.inbox.title") %>" data-chats--split-target="list">
|
|
159
|
+
<%= list %>
|
|
160
|
+
</nav>
|
|
161
|
+
|
|
162
|
+
<%= turbo_frame_tag Chats::ConversationsController::PANE_FRAME,
|
|
163
|
+
class: "chats-split__conversation",
|
|
164
|
+
target: "_top",
|
|
165
|
+
disabled: true,
|
|
166
|
+
data: { turbo_action: "advance", "chats--split-target": "frame" } do %>
|
|
167
|
+
<% if @conversation %>
|
|
168
|
+
<%= render "chats/conversations/conversation_pane",
|
|
169
|
+
conversation: @conversation, participant: @participant,
|
|
170
|
+
messages: @messages, more: @more_messages, first_unread_id: @first_unread_id,
|
|
171
|
+
counterpart: chats_counterpart, read_pending: @read_pending %>
|
|
172
|
+
<% else %>
|
|
173
|
+
<%= render "chats/conversations/empty_pane", inbox_empty: inbox_empty, inbox: @inbox %>
|
|
174
|
+
<% end %>
|
|
175
|
+
<% end %>
|
|
176
|
+
<% end %>
|
|
177
|
+
<% else %>
|
|
178
|
+
<div class="chats chats-inbox" data-chats-dock-hide>
|
|
179
|
+
<%= list %>
|
|
180
|
+
</div>
|
|
181
|
+
<% end %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<%# The answer to a row click on the two-pane messaging page: the request
|
|
2
|
+
comes from the page's `chats_conversation` frame (Turbo-Frame header),
|
|
3
|
+
so the answer is that frame with the conversation pane in it and
|
|
4
|
+
nothing else — no list, turbo-rails' minimal frame layout
|
|
5
|
+
(ConversationsController#show). The pane carries its title
|
|
6
|
+
(data-title), which chats--split gives the tab.
|
|
7
|
+
Turbo swaps it into the frame and, the frame being
|
|
8
|
+
data-turbo-action="advance", moves the address bar to /messages/:id. %>
|
|
9
|
+
<%= turbo_frame_tag Chats::ConversationsController::PANE_FRAME do %>
|
|
10
|
+
<%= render "chats/conversations/conversation_pane",
|
|
11
|
+
conversation: @conversation, participant: @participant,
|
|
12
|
+
messages: @messages, more: @more_messages, first_unread_id: @first_unread_id,
|
|
13
|
+
counterpart: chats_counterpart, read_pending: @read_pending %>
|
|
14
|
+
<% end %>
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
<%# The thread page
|
|
2
|
-
(chats/conversations/
|
|
3
|
-
|
|
1
|
+
<%# The thread page under `config.inbox_layout = :stacked`: its header
|
|
2
|
+
(chats/conversations/_thread_header) over the shared thread body
|
|
3
|
+
(chats/conversations/_thread, which a floating dock window and the
|
|
4
|
+
two-pane page's conversation pane render too — see that partial for how
|
|
5
|
+
the live thread works). Under the default `:split`, /messages/:id is
|
|
6
|
+
the two-pane page instead (index.html.erb). %>
|
|
7
|
+
<% content_for :title, chats_page_title(@conversation) %>
|
|
4
8
|
<%= chats_styles %>
|
|
5
9
|
|
|
6
10
|
<%= render layout: "chats/conversations/thread",
|
|
@@ -9,67 +13,9 @@
|
|
|
9
13
|
messages: @messages,
|
|
10
14
|
more: @more_messages,
|
|
11
15
|
first_unread_id: @first_unread_id,
|
|
12
|
-
counterpart: chats_counterpart
|
|
16
|
+
counterpart: chats_counterpart,
|
|
17
|
+
read_pending: @read_pending
|
|
13
18
|
} do %>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<span aria-hidden="true">‹</span>
|
|
17
|
-
<% end %>
|
|
18
|
-
|
|
19
|
-
<%# The counterpart is already resolved for the title, the profile link
|
|
20
|
-
and the stack link — reuse it here instead of asking again. %>
|
|
21
|
-
<% if chats_counterpart %>
|
|
22
|
-
<%= chats_messager_avatar(chats_counterpart) %>
|
|
23
|
-
<% else %>
|
|
24
|
-
<%= chats_conversation_avatar(@conversation, chats_current_messager) %>
|
|
25
|
-
<% end %>
|
|
26
|
-
|
|
27
|
-
<div class="chats-thread__identity">
|
|
28
|
-
<%# Both arms wrap the name in .chats-thread__name: the <h1> is a flex
|
|
29
|
-
row (the badge is its second child), and only a real element can
|
|
30
|
-
carry the ellipsis. %>
|
|
31
|
-
<h1 class="chats-thread__title">
|
|
32
|
-
<% if chats_counterpart %>
|
|
33
|
-
<%= chats_messager_name(chats_counterpart, css_class: "chats-thread__name") %>
|
|
34
|
-
<%= chats_verified_badge(chats_counterpart) %>
|
|
35
|
-
<% else %>
|
|
36
|
-
<span class="chats-thread__name"><%= @conversation.title_for(chats_current_messager) %></span>
|
|
37
|
-
<% end %>
|
|
38
|
-
</h1>
|
|
39
|
-
<% if @conversation.subject_label %>
|
|
40
|
-
<p class="chats-thread__subject"><%= @conversation.subject_label %></p>
|
|
41
|
-
<% end %>
|
|
42
|
-
<%# A stacked counterpart's thread is one of many: offer the way back
|
|
43
|
-
to the whole stack (chats' filtered inbox, or the host's own
|
|
44
|
-
screen via `group_path:`). %>
|
|
45
|
-
<% if Chats.grouped_inbox?(chats_counterpart) %>
|
|
46
|
-
<p class="chats-thread__see-all">
|
|
47
|
-
<%= link_to t("chats.thread.see_all"), chats_group_path_for(chats_counterpart), class: "chats-thread__see-all-link" %>
|
|
48
|
-
</p>
|
|
49
|
-
<% end %>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
<details class="chats-menu">
|
|
53
|
-
<summary class="chats-menu__trigger" aria-label="<%= t("chats.thread.options") %>">⋯</summary>
|
|
54
|
-
<div class="chats-menu__panel">
|
|
55
|
-
<% if @participant&.muted? %>
|
|
56
|
-
<%= button_to t("chats.thread.unmute"), unmute_conversation_path(@conversation), method: :post, class: "chats-menu__item" %>
|
|
57
|
-
<% else %>
|
|
58
|
-
<%= button_to t("chats.thread.mute"), mute_conversation_path(@conversation), method: :post, class: "chats-menu__item" %>
|
|
59
|
-
<% end %>
|
|
60
|
-
<% if @conversation.group? %>
|
|
61
|
-
<%= button_to t("chats.thread.leave"), leave_conversation_path(@conversation), method: :post, class: "chats-menu__item chats-menu__item--danger" %>
|
|
62
|
-
<% end %>
|
|
63
|
-
<%# Slot: the host's own thread actions (block, report, archive…).
|
|
64
|
-
`blockable` is false for messagers declared
|
|
65
|
-
`acts_as_messager blockable: false`, so safety affordances
|
|
66
|
-
disappear against a support desk without any class check. %>
|
|
67
|
-
<%= chats_slot :conversation_header_actions,
|
|
68
|
-
conversation: @conversation,
|
|
69
|
-
viewer: chats_current_messager,
|
|
70
|
-
counterpart: chats_counterpart,
|
|
71
|
-
blockable: chats_blockable?(chats_counterpart) %>
|
|
72
|
-
</div>
|
|
73
|
-
</details>
|
|
74
|
-
</header>
|
|
19
|
+
<%= render "chats/conversations/thread_header",
|
|
20
|
+
conversation: @conversation, participant: @participant, counterpart: chats_counterpart %>
|
|
75
21
|
<% end %>
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"input->chats--dock#typed " \
|
|
50
50
|
"click@document->chats--dock#delegate " \
|
|
51
51
|
"turbo:before-stream-render@document->chats--dock#beforeStreamRender " \
|
|
52
|
-
"turbo:load@document->chats--dock#pageChanged turbo:render@document->chats--dock#pageChanged"
|
|
52
|
+
"turbo:load@document->chats--dock#pageChanged turbo:render@document->chats--dock#pageChanged " \
|
|
53
|
+
"turbo:frame-load@document->chats--dock#pageFrameLoaded"
|
|
53
54
|
} do %>
|
|
54
55
|
<%= turbo_stream_from viewer, :chats_dock %>
|
|
55
56
|
<%= render "chats/dock/signal" %>
|
data/config/importmap.rb
CHANGED
|
@@ -17,6 +17,7 @@ pin "controllers/chats/debounced_submit_controller", to: "chats/debounced_submit
|
|
|
17
17
|
pin "controllers/chats/refresh_inbox_controller", to: "chats/refresh_inbox_controller.js"
|
|
18
18
|
pin "controllers/chats/dock_controller", to: "chats/dock_controller.js"
|
|
19
19
|
pin "controllers/chats/unread_badge_controller", to: "chats/unread_badge_controller.js"
|
|
20
|
+
pin "controllers/chats/split_controller", to: "chats/split_controller.js"
|
|
20
21
|
|
|
21
22
|
# Not a controller: what the inbox, thread, badge and dock controllers share
|
|
22
23
|
# to catch up on broadcasts they missed. Outside controllers/ so the eager
|
data/config/locales/en.yml
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
en:
|
|
2
2
|
chats:
|
|
3
|
+
page_title:
|
|
4
|
+
inbox: "Messages"
|
|
5
|
+
conversation: "%{name} · Messages"
|
|
3
6
|
inbox:
|
|
4
7
|
title: "Messages"
|
|
5
8
|
search_placeholder: "Search conversations"
|
|
@@ -8,6 +11,7 @@ en:
|
|
|
8
11
|
no_results_title: "No results"
|
|
9
12
|
no_results_hint: "Nothing matched “%{query}”."
|
|
10
13
|
no_messages: "No messages yet"
|
|
14
|
+
select_hint: "Select a conversation"
|
|
11
15
|
filtered_by: "Conversations with %{name}"
|
|
12
16
|
clear_filter: "Show all conversations"
|
|
13
17
|
group_count:
|
data/config/locales/es.yml
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
es:
|
|
2
2
|
chats:
|
|
3
|
+
page_title:
|
|
4
|
+
inbox: "Mensajes"
|
|
5
|
+
conversation: "%{name} · Mensajes"
|
|
3
6
|
inbox:
|
|
4
7
|
title: "Mensajes"
|
|
5
8
|
search_placeholder: "Buscar conversaciones"
|
|
@@ -8,6 +11,7 @@ es:
|
|
|
8
11
|
no_results_title: "Sin resultados"
|
|
9
12
|
no_results_hint: "No hay nada que coincida con «%{query}»."
|
|
10
13
|
no_messages: "Sin mensajes todavía"
|
|
14
|
+
select_hint: "Selecciona una conversación"
|
|
11
15
|
filtered_by: "Conversaciones con %{name}"
|
|
12
16
|
clear_filter: "Ver todas las conversaciones"
|
|
13
17
|
group_count:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/chats/configuration.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Chats
|
|
|
15
15
|
class Configuration
|
|
16
16
|
ATTACHMENT_MODES = [false, :images, :any].freeze
|
|
17
17
|
DELETION_MODES = [false, :soft, :hard].freeze
|
|
18
|
+
INBOX_LAYOUTS = %i[split stacked].freeze
|
|
18
19
|
|
|
19
20
|
# --- Host integration -----------------------------------------------------
|
|
20
21
|
|
|
@@ -92,6 +93,15 @@ module Chats
|
|
|
92
93
|
# threads.
|
|
93
94
|
attr_accessor :inbox_limit
|
|
94
95
|
|
|
96
|
+
# How the inbox and a conversation share the screen. `:split` (the
|
|
97
|
+
# default): on screens at least 700px wide, /messages is two panes, the
|
|
98
|
+
# conversation list on the left and the open conversation on the right,
|
|
99
|
+
# and /messages/:id is that same page with the conversation open
|
|
100
|
+
# (LinkedIn's messaging page); narrower, the list and the thread are
|
|
101
|
+
# separate pages. `:stacked`: separate pages at every width, as before
|
|
102
|
+
# 0.6.
|
|
103
|
+
attr_reader :inbox_layout
|
|
104
|
+
|
|
95
105
|
# Per-sender send throttle, enforced with Rails 8's built-in controller
|
|
96
106
|
# `rate_limit` when available (feature-detected; on Rails 7.1 it's a
|
|
97
107
|
# no-op). Shape: `{ to: Integer, within: ActiveSupport::Duration }`.
|
|
@@ -164,6 +174,14 @@ module Chats
|
|
|
164
174
|
# suppress the badge on some messagers without touching the models.
|
|
165
175
|
attr_reader :verified_badge
|
|
166
176
|
|
|
177
|
+
# ->(conversation, viewer) { String } — the title of the messaging page:
|
|
178
|
+
# the inbox (conversation nil) or a conversation. The bundled views
|
|
179
|
+
# give it to `content_for :title` (a layout doing `yield(:title)` shows
|
|
180
|
+
# it), and the two-pane page gives the tab the conversation's when a
|
|
181
|
+
# row opens it in the pane. nil (the default) is the localized
|
|
182
|
+
# `chats.page_title.inbox` / `chats.page_title.conversation`.
|
|
183
|
+
attr_reader :page_title
|
|
184
|
+
|
|
167
185
|
def initialize
|
|
168
186
|
@messager_class = "User"
|
|
169
187
|
@parent_controller = "::ApplicationController"
|
|
@@ -187,6 +205,7 @@ module Chats
|
|
|
187
205
|
@max_attachments_per_message = 4
|
|
188
206
|
@send_rate_limit = { to: 60, within: 60 } # 60 messages per minute per sender
|
|
189
207
|
@inbox_limit = 200
|
|
208
|
+
@inbox_layout = :split
|
|
190
209
|
|
|
191
210
|
@encrypt_messages = false
|
|
192
211
|
|
|
@@ -199,6 +218,7 @@ module Chats
|
|
|
199
218
|
@messager_url = ->(_messager) { nil }
|
|
200
219
|
@message_signature = nil
|
|
201
220
|
@verified_badge = nil
|
|
221
|
+
@page_title = nil
|
|
202
222
|
|
|
203
223
|
@messager_display_name = lambda do |messager|
|
|
204
224
|
messager.try(:display_name) || messager.try(:name) ||
|
|
@@ -244,6 +264,18 @@ module Chats
|
|
|
244
264
|
@deletion = normalized
|
|
245
265
|
end
|
|
246
266
|
|
|
267
|
+
def inbox_layout=(value)
|
|
268
|
+
normalized = value.respond_to?(:to_sym) ? value.to_sym : value
|
|
269
|
+
unless INBOX_LAYOUTS.include?(normalized)
|
|
270
|
+
raise ConfigurationError, "inbox_layout must be one of #{INBOX_LAYOUTS.inspect}, got #{value.inspect}"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
@inbox_layout = normalized
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Whether the inbox and the open conversation are one two-pane page.
|
|
277
|
+
def split_inbox? = inbox_layout == :split
|
|
278
|
+
|
|
247
279
|
def send_rate_limit=(value)
|
|
248
280
|
if value.nil?
|
|
249
281
|
@send_rate_limit = nil
|
|
@@ -312,6 +344,10 @@ module Chats
|
|
|
312
344
|
@verified_badge = value.nil? ? nil : ensure_callable(value, "verified_badge")
|
|
313
345
|
end
|
|
314
346
|
|
|
347
|
+
def page_title=(value)
|
|
348
|
+
@page_title = value.nil? ? nil : ensure_callable(value, "page_title")
|
|
349
|
+
end
|
|
350
|
+
|
|
315
351
|
def messager_display_name=(value)
|
|
316
352
|
@messager_display_name = ensure_callable(value, "messager_display_name")
|
|
317
353
|
end
|
|
@@ -449,11 +449,19 @@ module Chats
|
|
|
449
449
|
field.to_s == "title" && title.present?
|
|
450
450
|
end
|
|
451
451
|
|
|
452
|
+
# Whether anything was removed (see Chats::Message#remove_reported_field!):
|
|
453
|
+
# false for a title already gone, decided under the row lock.
|
|
452
454
|
def remove_reported_field!(field)
|
|
453
455
|
return false unless field.to_s == "title"
|
|
454
456
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
+
with_lock do
|
|
458
|
+
next false if title.blank?
|
|
459
|
+
|
|
460
|
+
update!(title: nil)
|
|
461
|
+
true
|
|
462
|
+
end
|
|
463
|
+
rescue ActiveRecord::RecordNotFound
|
|
464
|
+
false
|
|
457
465
|
end
|
|
458
466
|
|
|
459
467
|
def report_visible_to?(viewer, field: nil)
|
data/lib/chats/models/message.rb
CHANGED
|
@@ -356,12 +356,28 @@ module Chats
|
|
|
356
356
|
# A moderator removing a reported message body = the soft-delete
|
|
357
357
|
# tombstone path, so the thread shows "Message deleted" instead of a
|
|
358
358
|
# hole, and attachments are purged with it.
|
|
359
|
+
#
|
|
360
|
+
# Returns whether anything was removed: true only when it actually
|
|
361
|
+
# removed something (the tombstone, or the row under `deletion: :hard`);
|
|
362
|
+
# false for a message already deleted (by its sender, by an earlier
|
|
363
|
+
# decision), already gone, or whose body is already blank. moderate
|
|
364
|
+
# 1.0.0.beta3 and later take that as the truth and refuse a removal that
|
|
365
|
+
# removed nothing, rather than record "content removed" with a statement
|
|
366
|
+
# of reasons that would be false; earlier versions ignore the return
|
|
367
|
+
# value. Decided under the message's row lock, read again under it: two
|
|
368
|
+
# decisions taken at once cannot both see it standing.
|
|
359
369
|
def remove_reported_field!(field)
|
|
360
370
|
return false unless field.to_s == "body"
|
|
361
371
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
372
|
+
with_lock do
|
|
373
|
+
next false if deleted? || body.blank?
|
|
374
|
+
|
|
375
|
+
# Trust & Safety outranks a product lock: a closed conversation must
|
|
376
|
+
# never be a place reported content can hide.
|
|
377
|
+
soft_delete!(enforce_lock: false)
|
|
378
|
+
end
|
|
379
|
+
rescue ActiveRecord::RecordNotFound
|
|
380
|
+
false # hard-deleted meanwhile: nothing left to remove
|
|
365
381
|
end
|
|
366
382
|
|
|
367
383
|
# Only people *in* the conversation may report a message (a message
|
data/lib/chats/version.rb
CHANGED
|
@@ -84,6 +84,23 @@ Chats.configure do |config|
|
|
|
84
84
|
#
|
|
85
85
|
# config.inbox_limit = 200
|
|
86
86
|
#
|
|
87
|
+
# The messaging page's layout. :split (the default): from 700px wide,
|
|
88
|
+
# /messages is two panes, LinkedIn-style: the conversation list on the
|
|
89
|
+
# left, the open conversation on the right (/messages/:id is that same
|
|
90
|
+
# page, its conversation open); narrower, the list and the thread are
|
|
91
|
+
# separate pages. :stacked keeps them separate pages at every width.
|
|
92
|
+
# (Views you ejected with `rails g chats:views` keep rendering as they are.)
|
|
93
|
+
#
|
|
94
|
+
# config.inbox_layout = :split
|
|
95
|
+
#
|
|
96
|
+
# The messaging page's title (the bundled views give it to
|
|
97
|
+
# `content_for :title`, and the two-pane page to the tab when a row opens
|
|
98
|
+
# a conversation). nil: "Messages" / "Bob · Messages" (chats.page_title.*).
|
|
99
|
+
#
|
|
100
|
+
# config.page_title = ->(conversation, viewer) {
|
|
101
|
+
# conversation ? "#{conversation.title_for(viewer)} | MyApp" : "Messages | MyApp"
|
|
102
|
+
# }
|
|
103
|
+
#
|
|
87
104
|
# Encrypt message bodies at rest (ActiveRecord Encryption; requires
|
|
88
105
|
# `bin/rails db:encryption:init`). Body search degrades when enabled.
|
|
89
106
|
#
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chats
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-19 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -138,18 +138,23 @@ files:
|
|
|
138
138
|
- app/javascript/chats/dock_controller.js
|
|
139
139
|
- app/javascript/chats/recovery.js
|
|
140
140
|
- app/javascript/chats/refresh_inbox_controller.js
|
|
141
|
+
- app/javascript/chats/split_controller.js
|
|
141
142
|
- app/javascript/chats/thread_controller.js
|
|
142
143
|
- app/javascript/chats/unread_badge_controller.js
|
|
144
|
+
- app/views/chats/conversations/_conversation_pane.html.erb
|
|
143
145
|
- app/views/chats/conversations/_conversation_row.html.erb
|
|
146
|
+
- app/views/chats/conversations/_empty_pane.html.erb
|
|
144
147
|
- app/views/chats/conversations/_group.html.erb
|
|
145
148
|
- app/views/chats/conversations/_history.html.erb
|
|
146
149
|
- app/views/chats/conversations/_locked_composer.html.erb
|
|
147
150
|
- app/views/chats/conversations/_messages_page.html.erb
|
|
148
151
|
- app/views/chats/conversations/_read_state.html.erb
|
|
149
152
|
- app/views/chats/conversations/_thread.html.erb
|
|
153
|
+
- app/views/chats/conversations/_thread_header.html.erb
|
|
150
154
|
- app/views/chats/conversations/badge.turbo_stream.erb
|
|
151
155
|
- app/views/chats/conversations/dock.html.erb
|
|
152
156
|
- app/views/chats/conversations/index.html.erb
|
|
157
|
+
- app/views/chats/conversations/pane.html.erb
|
|
153
158
|
- app/views/chats/conversations/refresh.turbo_stream.erb
|
|
154
159
|
- app/views/chats/conversations/show.html.erb
|
|
155
160
|
- app/views/chats/conversations/window.html.erb
|
|
@@ -168,7 +173,6 @@ files:
|
|
|
168
173
|
- app/views/chats/messages/locked.turbo_stream.erb
|
|
169
174
|
- app/views/chats/shared/_unread_badge.html.erb
|
|
170
175
|
- app/views/chats/shared/_verified_badge.html.erb
|
|
171
|
-
- chats-0.3.1.gem
|
|
172
176
|
- config/importmap.rb
|
|
173
177
|
- config/locales/en.yml
|
|
174
178
|
- config/locales/es.yml
|
|
@@ -183,6 +187,10 @@ files:
|
|
|
183
187
|
- docs/screenshots/dock-phone-panel.png
|
|
184
188
|
- docs/screenshots/dock-phone-window.png
|
|
185
189
|
- docs/screenshots/inbox.png
|
|
190
|
+
- docs/screenshots/split-inbox-empty.png
|
|
191
|
+
- docs/screenshots/split-inbox-phone-list.png
|
|
192
|
+
- docs/screenshots/split-inbox-phone-thread.png
|
|
193
|
+
- docs/screenshots/split-inbox.png
|
|
186
194
|
- docs/screenshots/system-note-action.png
|
|
187
195
|
- docs/screenshots/thread-actions-phone.png
|
|
188
196
|
- gemfiles/rails_7.1.gemfile
|
data/chats-0.3.1.gem
DELETED
|
Binary file
|