turbo_chat 0.1.0
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 +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +741 -0
- data/app/assets/config/chat_gem_manifest.js +6 -0
- data/app/assets/javascripts/chat_gem/application.js +4 -0
- data/app/assets/javascripts/chat_gem/lifecycle_events.js +93 -0
- data/app/assets/javascripts/chat_gem/messages.js +442 -0
- data/app/assets/javascripts/chat_gem/realtime.js +398 -0
- data/app/assets/javascripts/chat_gem/shared.js +488 -0
- data/app/assets/stylesheets/chat_gem/application.css +741 -0
- data/app/controllers/chat_gem/application_controller.rb +41 -0
- data/app/controllers/chat_gem/chat_memberships_controller.rb +81 -0
- data/app/controllers/chat_gem/chat_messages_controller.rb +144 -0
- data/app/controllers/chat_gem/chats_controller/event_payload_support.rb +58 -0
- data/app/controllers/chat_gem/chats_controller/invitation_support.rb +31 -0
- data/app/controllers/chat_gem/chats_controller.rb +125 -0
- data/app/helpers/chat_gem/application_helper/config_support.rb +41 -0
- data/app/helpers/chat_gem/application_helper/mention_support/entry_builder.rb +55 -0
- data/app/helpers/chat_gem/application_helper/mention_support/permission_support.rb +28 -0
- data/app/helpers/chat_gem/application_helper/mention_support/token_builder.rb +49 -0
- data/app/helpers/chat_gem/application_helper/mention_support.rb +80 -0
- data/app/helpers/chat_gem/application_helper/message_rendering.rb +165 -0
- data/app/helpers/chat_gem/application_helper/participant_support.rb +81 -0
- data/app/helpers/chat_gem/application_helper.rb +12 -0
- data/app/models/chat_gem/application_record.rb +5 -0
- data/app/models/chat_gem/chat.rb +127 -0
- data/app/models/chat_gem/chat_membership.rb +136 -0
- data/app/models/chat_gem/chat_message/blocked_words_moderation.rb +120 -0
- data/app/models/chat_gem/chat_message/body_length_validation.rb +20 -0
- data/app/models/chat_gem/chat_message/broadcasting.rb +61 -0
- data/app/models/chat_gem/chat_message/formatting.rb +81 -0
- data/app/models/chat_gem/chat_message/mention_validation.rb +85 -0
- data/app/models/chat_gem/chat_message/signals.rb +61 -0
- data/app/models/chat_gem/chat_message.rb +40 -0
- data/app/views/chat_gem/chat_messages/_chat_message.html.erb +1 -0
- data/app/views/chat_gem/chat_messages/_form.html.erb +22 -0
- data/app/views/chat_gem/chat_messages/_message.html.erb +83 -0
- data/app/views/chat_gem/chat_messages/_signal.html.erb +3 -0
- data/app/views/chat_gem/chat_messages/_signals.html.erb +24 -0
- data/app/views/chat_gem/chat_messages/index.html.erb +1 -0
- data/app/views/chat_gem/chats/index.html.erb +51 -0
- data/app/views/chat_gem/chats/new.html.erb +13 -0
- data/app/views/chat_gem/chats/show.html.erb +95 -0
- data/app/views/layouts/chat_gem/application.html.erb +20 -0
- data/config/routes.rb +16 -0
- data/db/migrate/20260215000000_create_chat_gem_chats.rb +8 -0
- data/db/migrate/20260215000001_create_chat_gem_chat_memberships.rb +19 -0
- data/db/migrate/20260215000002_create_chat_gem_chat_messages.rb +14 -0
- data/db/migrate/20260218000011_add_closed_at_to_chat_gem_chats.rb +6 -0
- data/db/migrate/20260218000012_add_custom_role_key_to_chat_memberships.rb +6 -0
- data/db/migrate/20260218000013_add_invitation_accepted_to_chat_gem_chat_memberships.rb +5 -0
- data/lib/chat_gem/configuration.rb +242 -0
- data/lib/chat_gem/engine.rb +29 -0
- data/lib/chat_gem/model_extensions/chat_participant.rb +45 -0
- data/lib/chat_gem/moderation.rb +194 -0
- data/lib/chat_gem/permission.rb +193 -0
- data/lib/chat_gem/signals.rb +26 -0
- data/lib/chat_gem/version.rb +3 -0
- data/lib/chat_gem.rb +24 -0
- data/lib/generators/chat_gem/install/install_generator.rb +18 -0
- data/lib/generators/chat_gem/install/templates/chat_gem.rb +36 -0
- data/lib/generators/turbo_chat/install/install_generator.rb +18 -0
- data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +36 -0
- data/lib/tasks/chat_gem_tasks.rake +1 -0
- data/lib/tasks/turbo_chat_tasks.rake +10 -0
- data/lib/turbo_chat/version.rb +5 -0
- data/lib/turbo_chat.rb +24 -0
- data/turbo_chat.gemspec +31 -0
- metadata +155 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
(function (namespace) {
|
|
2
|
+
var constants = {
|
|
3
|
+
SIGNAL_TTL_SECONDS: 12,
|
|
4
|
+
SIGNAL_START_DELAY_MS: 750,
|
|
5
|
+
SIGNAL_IDLE_GRACE_MS: 2500,
|
|
6
|
+
SIGNAL_HEARTBEAT_MS: 4000,
|
|
7
|
+
SIGNAL_RETREAT_MS: 180,
|
|
8
|
+
MENTION_MAX_RESULTS: 8,
|
|
9
|
+
MENTION_BLUR_HIDE_DELAY_MS: 120,
|
|
10
|
+
MEMBER_MENTION_TOKEN_PATTERN: /^@[a-z0-9_]{1,32}$/i,
|
|
11
|
+
ROLE_MENTION_TOKEN_PATTERN: /^@[A-Z][A-Z0-9_]{0,31}$/
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
namespace.constants = constants;
|
|
15
|
+
|
|
16
|
+
function csrfToken() {
|
|
17
|
+
var tag = document.querySelector("meta[name='csrf-token']");
|
|
18
|
+
return tag ? tag.content : "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function renderTurboStreamResponse(response) {
|
|
22
|
+
if (!response) {
|
|
23
|
+
return Promise.resolve();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return response.text().then(function (body) {
|
|
27
|
+
if (!body) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof Turbo !== "undefined" && body.indexOf("<turbo-stream") !== -1) {
|
|
32
|
+
Turbo.renderStreamMessage(body);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function datasetFlagEnabled(node, key) {
|
|
38
|
+
if (!node || !node.dataset) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return node.dataset[key] === "true";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseJsonObject(raw) {
|
|
46
|
+
if (!raw) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
var parsed = JSON.parse(raw);
|
|
52
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return parsed;
|
|
57
|
+
} catch (_error) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseMentionOptions(raw) {
|
|
63
|
+
if (!raw) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(raw);
|
|
70
|
+
} catch (_error) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!Array.isArray(parsed)) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var seen = {};
|
|
79
|
+
var options = [];
|
|
80
|
+
|
|
81
|
+
parsed.forEach(function (entry) {
|
|
82
|
+
if (!entry || typeof entry !== "object") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
var token = String(entry.token || "").trim();
|
|
87
|
+
if (!token || token.charAt(0) !== "@" || /\s/.test(token)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var dedupeKey = token.toLowerCase();
|
|
92
|
+
if (seen[dedupeKey]) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
seen[dedupeKey] = true;
|
|
97
|
+
options.push({
|
|
98
|
+
token: token,
|
|
99
|
+
label: String(entry.label || token),
|
|
100
|
+
kind: String(entry.kind || "member")
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return options;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseMentionTokens(raw) {
|
|
108
|
+
if (!raw) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var parsed = raw;
|
|
113
|
+
if (typeof raw === "string") {
|
|
114
|
+
var trimmed = raw.trim();
|
|
115
|
+
if (!trimmed) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (trimmed.charAt(0) === "[") {
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(trimmed);
|
|
122
|
+
} catch (_error) {
|
|
123
|
+
parsed = trimmed.split(",");
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
parsed = trimmed.split(",");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
var source = Array.isArray(parsed) ? parsed : [parsed];
|
|
131
|
+
var seen = {};
|
|
132
|
+
var tokens = [];
|
|
133
|
+
|
|
134
|
+
source.forEach(function (entry) {
|
|
135
|
+
var token = String(entry || "").trim();
|
|
136
|
+
if (!token || token.charAt(0) !== "@" || /\s/.test(token)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
var dedupeKey = token.toLowerCase();
|
|
141
|
+
if (seen[dedupeKey]) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
seen[dedupeKey] = true;
|
|
146
|
+
tokens.push(token);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return tokens;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function mentionKind(token) {
|
|
153
|
+
var mentionToken = String(token || "").trim();
|
|
154
|
+
if (!mentionToken) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (mentionToken.toLowerCase() === "@all") {
|
|
159
|
+
return "group";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (constants.ROLE_MENTION_TOKEN_PATTERN.test(mentionToken)) {
|
|
163
|
+
return "role";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (constants.MEMBER_MENTION_TOKEN_PATTERN.test(mentionToken)) {
|
|
167
|
+
return "member";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function emptyMentionAutocomplete() {
|
|
174
|
+
return {
|
|
175
|
+
hideMenu: function () {},
|
|
176
|
+
updateMenu: function () {},
|
|
177
|
+
handleKeydown: function () {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function setupMentionAutocomplete(input, options) {
|
|
184
|
+
if (!input) {
|
|
185
|
+
return emptyMentionAutocomplete();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
var mentionOptions = Array.isArray(options && options.mentionOptions) ? options.mentionOptions : [];
|
|
189
|
+
var menuHost = options && options.menuHost ? options.menuHost : input.parentNode;
|
|
190
|
+
var menuClassName = options && options.menuClassName ? options.menuClassName : "chat-mentions-menu";
|
|
191
|
+
if (!mentionOptions.length || !menuHost) {
|
|
192
|
+
return emptyMentionAutocomplete();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
var mentionMenu = null;
|
|
196
|
+
var mentionMatches = [];
|
|
197
|
+
var mentionActiveIndex = 0;
|
|
198
|
+
|
|
199
|
+
function ensureMentionMenu() {
|
|
200
|
+
if (mentionMenu) {
|
|
201
|
+
return mentionMenu;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
mentionMenu = document.createElement("div");
|
|
205
|
+
mentionMenu.className = menuClassName;
|
|
206
|
+
mentionMenu.hidden = true;
|
|
207
|
+
menuHost.appendChild(mentionMenu);
|
|
208
|
+
return mentionMenu;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function hideMentionMenu() {
|
|
212
|
+
if (!mentionMenu) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
mentionMenu.hidden = true;
|
|
217
|
+
mentionMenu.classList.remove("chat-mentions-menu--open");
|
|
218
|
+
mentionMenu.innerHTML = "";
|
|
219
|
+
mentionMatches = [];
|
|
220
|
+
mentionActiveIndex = 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function setMentionActiveIndex(index) {
|
|
224
|
+
if (!mentionMatches.length) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
mentionActiveIndex = ((index % mentionMatches.length) + mentionMatches.length) % mentionMatches.length;
|
|
229
|
+
if (!mentionMenu) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
mentionMenu.querySelectorAll(".chat-mentions-item").forEach(function (item, itemIndex) {
|
|
234
|
+
item.classList.toggle("chat-mentions-item--active", itemIndex === mentionActiveIndex);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function mentionContext() {
|
|
239
|
+
var caret = input.selectionStart;
|
|
240
|
+
if (typeof caret !== "number") {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
var beforeCaret = input.value.slice(0, caret);
|
|
245
|
+
var atIndex = beforeCaret.lastIndexOf("@");
|
|
246
|
+
if (atIndex < 0) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
var previousCharacter = atIndex > 0 ? beforeCaret.charAt(atIndex - 1) : "";
|
|
251
|
+
if (/[a-zA-Z0-9_]/.test(previousCharacter)) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
var query = beforeCaret.slice(atIndex + 1);
|
|
256
|
+
if (!/^[a-zA-Z0-9_]*$/.test(query)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
start: atIndex,
|
|
262
|
+
end: caret,
|
|
263
|
+
query: query
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function matchingMentionOptions(query) {
|
|
268
|
+
var normalizedQuery = String(query || "").toLowerCase();
|
|
269
|
+
return mentionOptions
|
|
270
|
+
.filter(function (option) {
|
|
271
|
+
return option.token.slice(1).toLowerCase().indexOf(normalizedQuery) === 0;
|
|
272
|
+
})
|
|
273
|
+
.slice(0, constants.MENTION_MAX_RESULTS);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function insertMentionOption(option) {
|
|
277
|
+
var context = mentionContext();
|
|
278
|
+
if (!context) {
|
|
279
|
+
hideMentionMenu();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
var before = input.value.slice(0, context.start);
|
|
284
|
+
var after = input.value.slice(context.end);
|
|
285
|
+
var needsTrailingSpace = !after.match(/^[\s,.!?;:)]/);
|
|
286
|
+
var insertion = option.token + (needsTrailingSpace ? " " : "");
|
|
287
|
+
input.value = before + insertion + after;
|
|
288
|
+
|
|
289
|
+
var caretPosition = before.length + insertion.length;
|
|
290
|
+
input.setSelectionRange(caretPosition, caretPosition);
|
|
291
|
+
input.focus();
|
|
292
|
+
|
|
293
|
+
hideMentionMenu();
|
|
294
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function renderMentionMenu() {
|
|
298
|
+
if (!mentionMatches.length) {
|
|
299
|
+
hideMentionMenu();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
var menu = ensureMentionMenu();
|
|
304
|
+
menu.innerHTML = "";
|
|
305
|
+
mentionMatches.forEach(function (option, index) {
|
|
306
|
+
var item = document.createElement("button");
|
|
307
|
+
item.type = "button";
|
|
308
|
+
item.className = "chat-mentions-item";
|
|
309
|
+
item.setAttribute("data-chat-mention-index", String(index));
|
|
310
|
+
if (index === mentionActiveIndex) {
|
|
311
|
+
item.classList.add("chat-mentions-item--active");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
var token = document.createElement("span");
|
|
315
|
+
token.className = "chat-mentions-item__token";
|
|
316
|
+
token.textContent = option.token;
|
|
317
|
+
|
|
318
|
+
var label = document.createElement("span");
|
|
319
|
+
label.className = "chat-mentions-item__label";
|
|
320
|
+
label.textContent = option.label;
|
|
321
|
+
|
|
322
|
+
item.appendChild(token);
|
|
323
|
+
item.appendChild(label);
|
|
324
|
+
item.addEventListener("mousedown", function (event) {
|
|
325
|
+
event.preventDefault();
|
|
326
|
+
});
|
|
327
|
+
item.addEventListener("click", function () {
|
|
328
|
+
insertMentionOption(option);
|
|
329
|
+
});
|
|
330
|
+
menu.appendChild(item);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
menu.hidden = false;
|
|
334
|
+
menu.classList.add("chat-mentions-menu--open");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function selectActiveMention() {
|
|
338
|
+
if (!mentionMatches.length) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
insertMentionOption(mentionMatches[mentionActiveIndex]);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function updateMentionMenu() {
|
|
347
|
+
var context = mentionContext();
|
|
348
|
+
if (!context) {
|
|
349
|
+
hideMentionMenu();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
mentionMatches = matchingMentionOptions(context.query);
|
|
354
|
+
if (!mentionMatches.length) {
|
|
355
|
+
hideMentionMenu();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
mentionActiveIndex = 0;
|
|
360
|
+
renderMentionMenu();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function handleMentionKeydown(event) {
|
|
364
|
+
if (!mentionMenu || mentionMenu.hidden || !mentionMatches.length) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (event.key === "ArrowDown") {
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
setMentionActiveIndex(mentionActiveIndex + 1);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (event.key === "ArrowUp") {
|
|
375
|
+
event.preventDefault();
|
|
376
|
+
setMentionActiveIndex(mentionActiveIndex - 1);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
381
|
+
event.preventDefault();
|
|
382
|
+
return selectActiveMention();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (event.key === "Escape") {
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
hideMentionMenu();
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
hideMenu: hideMentionMenu,
|
|
396
|
+
updateMenu: updateMentionMenu,
|
|
397
|
+
handleKeydown: handleMentionKeydown
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function scrollLastMessageIntoView(container) {
|
|
402
|
+
if (!container) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
var lastMessage = container.lastElementChild;
|
|
407
|
+
if (!lastMessage) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Keep the last message fully visible with a tiny breathing space.
|
|
412
|
+
var lastBottom = lastMessage.offsetTop + lastMessage.offsetHeight;
|
|
413
|
+
var targetScrollTop = Math.max(0, lastBottom - container.clientHeight + signalOverlayOffset(container) + 2);
|
|
414
|
+
container.scrollTop = targetScrollTop;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function signalOverlayOffset(container) {
|
|
418
|
+
if (!container || typeof window === "undefined") {
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
var chatWindow = container.closest(".chat-window");
|
|
423
|
+
if (!chatWindow) {
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
var cssOffset = window.getComputedStyle(chatWindow).getPropertyValue("--chat-signal-offset");
|
|
428
|
+
var parsedOffset = parseFloat(cssOffset);
|
|
429
|
+
if (isNaN(parsedOffset) || parsedOffset <= 0) {
|
|
430
|
+
return 0;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return parsedOffset;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function prefersReducedMotion() {
|
|
437
|
+
return (
|
|
438
|
+
typeof window !== "undefined" &&
|
|
439
|
+
typeof window.matchMedia === "function" &&
|
|
440
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function scrollMessageIntoView(container, messageNode) {
|
|
445
|
+
if (!container || !messageNode) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
var containerRect = container.getBoundingClientRect();
|
|
450
|
+
var messageRect = messageNode.getBoundingClientRect();
|
|
451
|
+
var topPadding = 12;
|
|
452
|
+
var bottomPadding = 14 + signalOverlayOffset(container);
|
|
453
|
+
var aboveVisibleArea = messageRect.top < containerRect.top + topPadding;
|
|
454
|
+
var belowVisibleArea = messageRect.bottom > containerRect.bottom - bottomPadding;
|
|
455
|
+
if (!aboveVisibleArea && !belowVisibleArea) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
var delta = aboveVisibleArea
|
|
460
|
+
? messageRect.top - (containerRect.top + topPadding)
|
|
461
|
+
: messageRect.bottom - (containerRect.bottom - bottomPadding);
|
|
462
|
+
var nextTop = Math.max(0, container.scrollTop + delta);
|
|
463
|
+
|
|
464
|
+
if (typeof container.scrollTo === "function") {
|
|
465
|
+
container.scrollTo({
|
|
466
|
+
top: nextTop,
|
|
467
|
+
behavior: prefersReducedMotion() ? "auto" : "smooth"
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
container.scrollTop = nextTop;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
namespace.csrfToken = csrfToken;
|
|
476
|
+
namespace.renderTurboStreamResponse = renderTurboStreamResponse;
|
|
477
|
+
namespace.datasetFlagEnabled = datasetFlagEnabled;
|
|
478
|
+
namespace.parseJsonObject = parseJsonObject;
|
|
479
|
+
namespace.parseMentionOptions = parseMentionOptions;
|
|
480
|
+
namespace.parseMentionTokens = parseMentionTokens;
|
|
481
|
+
namespace.mentionKind = mentionKind;
|
|
482
|
+
namespace.emptyMentionAutocomplete = emptyMentionAutocomplete;
|
|
483
|
+
namespace.setupMentionAutocomplete = setupMentionAutocomplete;
|
|
484
|
+
namespace.scrollLastMessageIntoView = scrollLastMessageIntoView;
|
|
485
|
+
namespace.signalOverlayOffset = signalOverlayOffset;
|
|
486
|
+
namespace.prefersReducedMotion = prefersReducedMotion;
|
|
487
|
+
namespace.scrollMessageIntoView = scrollMessageIntoView;
|
|
488
|
+
})(window.ChatGemUI = window.ChatGemUI || {});
|