turbo_chat 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +24 -1
- data/README.md +178 -190
- data/app/assets/config/turbo_chat_manifest.js +3 -0
- data/app/assets/javascripts/turbo_chat/application.js +3 -0
- data/app/assets/javascripts/turbo_chat/invite_picker.js +19 -392
- data/app/assets/javascripts/turbo_chat/member_sync.js +426 -0
- data/app/assets/javascripts/turbo_chat/mentions.js +366 -0
- data/app/assets/javascripts/turbo_chat/messages.js +18 -370
- data/app/assets/javascripts/turbo_chat/realtime.js +3 -10
- data/app/assets/javascripts/turbo_chat/scroll_proxy.js +379 -0
- data/app/assets/javascripts/turbo_chat/shared.js +7 -383
- data/app/assets/stylesheets/turbo_chat/application.css +9 -1646
- data/app/assets/stylesheets/turbo_chat/base.css +84 -0
- data/app/assets/stylesheets/turbo_chat/components.css +193 -0
- data/app/assets/stylesheets/turbo_chat/composer.css +241 -0
- data/app/assets/stylesheets/turbo_chat/layout.css +307 -0
- data/app/assets/stylesheets/turbo_chat/members.css +264 -0
- data/app/assets/stylesheets/turbo_chat/menus.css +172 -0
- data/app/assets/stylesheets/turbo_chat/messages.css +430 -0
- data/app/controllers/turbo_chat/application_controller.rb +3 -7
- data/app/controllers/turbo_chat/chat_memberships_controller.rb +35 -1
- data/app/controllers/turbo_chat/chat_messages_controller.rb +4 -8
- data/app/controllers/turbo_chat/chats_controller.rb +10 -12
- data/app/helpers/turbo_chat/application_helper/config_support.rb +42 -32
- data/app/helpers/turbo_chat/application_helper/mention_support.rb +3 -3
- data/app/helpers/turbo_chat/application_helper/message_rendering.rb +24 -13
- data/app/models/turbo_chat/chat.rb +43 -20
- data/app/models/turbo_chat/chat_membership.rb +1 -1
- data/app/models/turbo_chat/chat_message/blocked_words_moderation.rb +9 -25
- data/app/models/turbo_chat/chat_message/body_length_validation.rb +1 -1
- data/app/models/turbo_chat/chat_message/broadcasting.rb +2 -6
- data/app/models/turbo_chat/chat_message/formatting.rb +3 -7
- data/app/models/turbo_chat/chat_message/mention_validation.rb +1 -1
- data/app/models/turbo_chat/chat_message/signals.rb +1 -1
- data/app/models/turbo_chat/chat_message.rb +3 -8
- data/app/views/turbo_chat/chat_messages/_form.html.erb +9 -9
- data/app/views/turbo_chat/chat_messages/_message.html.erb +2 -2
- data/app/views/turbo_chat/chat_messages/_signals.html.erb +11 -13
- data/app/views/turbo_chat/chat_messages/_system.html.erb +1 -1
- data/app/views/turbo_chat/chats/_invite_form.html.erb +1 -1
- data/app/views/turbo_chat/chats/_member_entries.html.erb +15 -1
- data/app/views/turbo_chat/chats/index.html.erb +1 -1
- data/app/views/turbo_chat/chats/new.html.erb +4 -7
- data/app/views/turbo_chat/chats/show.html.erb +29 -27
- data/config/routes.rb +6 -1
- data/db/migrate/20260325000016_add_chat_mode_to_turbo_chat_chats.rb +6 -0
- data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +8 -0
- data/lib/turbo_chat/configuration/defaults.rb +21 -0
- data/lib/turbo_chat/configuration.rb +105 -0
- data/lib/turbo_chat/moderation/chat_actions.rb +2 -2
- data/lib/turbo_chat/moderation/member_actions.rb +2 -1
- data/lib/turbo_chat/moderation/support.rb +5 -9
- data/lib/turbo_chat/permission/support.rb +6 -2
- data/lib/turbo_chat/permission.rb +1 -5
- data/lib/turbo_chat/signals.rb +1 -1
- data/lib/turbo_chat/version.rb +1 -1
- metadata +13 -2
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
var namespace = (window.TurboChatUI = window.TurboChatUI || {});
|
|
4
|
+
var constants = namespace.constants;
|
|
5
|
+
|
|
6
|
+
function parseMentionOptions(raw) {
|
|
7
|
+
if (!raw) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
var parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(raw);
|
|
14
|
+
} catch (_error) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!Array.isArray(parsed)) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var seen = {};
|
|
23
|
+
var options = [];
|
|
24
|
+
|
|
25
|
+
parsed.forEach(function (entry) {
|
|
26
|
+
if (!entry || typeof entry !== "object") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var token = String(entry.token || "").trim();
|
|
31
|
+
if (!token || token.charAt(0) !== "@" || /\s/.test(token)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var dedupeKey = token.toLowerCase();
|
|
36
|
+
if (seen[dedupeKey]) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
seen[dedupeKey] = true;
|
|
41
|
+
options.push({
|
|
42
|
+
token: token,
|
|
43
|
+
label: String(entry.label || token),
|
|
44
|
+
kind: String(entry.kind || "member")
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return options;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseMentionTokens(raw) {
|
|
52
|
+
if (!raw) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var parsed = raw;
|
|
57
|
+
if (typeof raw === "string") {
|
|
58
|
+
var trimmed = raw.trim();
|
|
59
|
+
if (!trimmed) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (trimmed.charAt(0) === "[") {
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(trimmed);
|
|
66
|
+
} catch (_error) {
|
|
67
|
+
parsed = trimmed.split(",");
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
parsed = trimmed.split(",");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var source = Array.isArray(parsed) ? parsed : [parsed];
|
|
75
|
+
var seen = {};
|
|
76
|
+
var tokens = [];
|
|
77
|
+
|
|
78
|
+
source.forEach(function (entry) {
|
|
79
|
+
var token = String(entry || "").trim();
|
|
80
|
+
if (!token || token.charAt(0) !== "@" || /\s/.test(token)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var dedupeKey = token.toLowerCase();
|
|
85
|
+
if (seen[dedupeKey]) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
seen[dedupeKey] = true;
|
|
90
|
+
tokens.push(token);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return tokens;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mentionKind(token) {
|
|
97
|
+
var mentionToken = String(token || "").trim();
|
|
98
|
+
if (!mentionToken) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (mentionToken.toLowerCase() === "@all") {
|
|
103
|
+
return "group";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (constants.ROLE_MENTION_TOKEN_PATTERN.test(mentionToken)) {
|
|
107
|
+
return "role";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (constants.MEMBER_MENTION_TOKEN_PATTERN.test(mentionToken)) {
|
|
111
|
+
return "member";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function emptyMentionAutocomplete() {
|
|
118
|
+
return {
|
|
119
|
+
hideMenu: function () {},
|
|
120
|
+
updateMenu: function () {},
|
|
121
|
+
handleKeydown: function () {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function setupMentionAutocomplete(input, options) {
|
|
128
|
+
if (!input) {
|
|
129
|
+
return emptyMentionAutocomplete();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
var mentionOptions = Array.isArray(options && options.mentionOptions) ? options.mentionOptions : [];
|
|
133
|
+
var mentionOptionsResolver = options && typeof options.mentionOptionsResolver === "function"
|
|
134
|
+
? options.mentionOptionsResolver
|
|
135
|
+
: null;
|
|
136
|
+
var menuHost = options && options.menuHost ? options.menuHost : input.parentNode;
|
|
137
|
+
var menuClassName = options && options.menuClassName ? options.menuClassName : "chat-mentions-menu";
|
|
138
|
+
if ((!mentionOptions.length && !mentionOptionsResolver) || !menuHost) {
|
|
139
|
+
return emptyMentionAutocomplete();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
var mentionMenu = null;
|
|
143
|
+
var mentionMatches = [];
|
|
144
|
+
var mentionActiveIndex = 0;
|
|
145
|
+
|
|
146
|
+
function availableMentionOptions() {
|
|
147
|
+
if (mentionOptionsResolver) {
|
|
148
|
+
var resolvedMentionOptions = mentionOptionsResolver();
|
|
149
|
+
return Array.isArray(resolvedMentionOptions) ? resolvedMentionOptions : [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return mentionOptions;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ensureMentionMenu() {
|
|
156
|
+
if (mentionMenu) {
|
|
157
|
+
return mentionMenu;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
mentionMenu = document.createElement("div");
|
|
161
|
+
mentionMenu.className = menuClassName;
|
|
162
|
+
mentionMenu.hidden = true;
|
|
163
|
+
menuHost.appendChild(mentionMenu);
|
|
164
|
+
return mentionMenu;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function hideMentionMenu() {
|
|
168
|
+
if (!mentionMenu) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
mentionMenu.hidden = true;
|
|
173
|
+
mentionMenu.classList.remove("chat-mentions-menu--open");
|
|
174
|
+
mentionMenu.innerHTML = "";
|
|
175
|
+
mentionMatches = [];
|
|
176
|
+
mentionActiveIndex = 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function setMentionActiveIndex(index) {
|
|
180
|
+
if (!mentionMatches.length) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
mentionActiveIndex = ((index % mentionMatches.length) + mentionMatches.length) % mentionMatches.length;
|
|
185
|
+
if (!mentionMenu) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
mentionMenu.querySelectorAll(".chat-mentions-item").forEach(function (item, itemIndex) {
|
|
190
|
+
item.classList.toggle("chat-mentions-item--active", itemIndex === mentionActiveIndex);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function mentionContext() {
|
|
195
|
+
var caret = input.selectionStart;
|
|
196
|
+
if (typeof caret !== "number") {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
var beforeCaret = input.value.slice(0, caret);
|
|
201
|
+
var atIndex = beforeCaret.lastIndexOf("@");
|
|
202
|
+
if (atIndex < 0) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
var previousCharacter = atIndex > 0 ? beforeCaret.charAt(atIndex - 1) : "";
|
|
207
|
+
if (/[a-zA-Z0-9_]/.test(previousCharacter)) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
var query = beforeCaret.slice(atIndex + 1);
|
|
212
|
+
if (!/^[a-zA-Z0-9_]*$/.test(query)) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
start: atIndex,
|
|
218
|
+
end: caret,
|
|
219
|
+
query: query
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function matchingMentionOptions(query) {
|
|
224
|
+
var normalizedQuery = String(query || "").toLowerCase();
|
|
225
|
+
return availableMentionOptions()
|
|
226
|
+
.filter(function (option) {
|
|
227
|
+
return option.token.slice(1).toLowerCase().indexOf(normalizedQuery) === 0;
|
|
228
|
+
})
|
|
229
|
+
.slice(0, constants.MENTION_MAX_RESULTS);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function insertMentionOption(option) {
|
|
233
|
+
var context = mentionContext();
|
|
234
|
+
if (!context) {
|
|
235
|
+
hideMentionMenu();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
var before = input.value.slice(0, context.start);
|
|
240
|
+
var after = input.value.slice(context.end);
|
|
241
|
+
var needsTrailingSpace = !after.match(/^[\s,.!?;:)]/);
|
|
242
|
+
var insertion = option.token + (needsTrailingSpace ? " " : "");
|
|
243
|
+
input.value = before + insertion + after;
|
|
244
|
+
|
|
245
|
+
var caretPosition = before.length + insertion.length;
|
|
246
|
+
input.setSelectionRange(caretPosition, caretPosition);
|
|
247
|
+
input.focus();
|
|
248
|
+
|
|
249
|
+
hideMentionMenu();
|
|
250
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renderMentionMenu() {
|
|
254
|
+
if (!mentionMatches.length) {
|
|
255
|
+
hideMentionMenu();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var menu = ensureMentionMenu();
|
|
260
|
+
menu.innerHTML = "";
|
|
261
|
+
mentionMatches.forEach(function (option, index) {
|
|
262
|
+
var item = document.createElement("button");
|
|
263
|
+
item.type = "button";
|
|
264
|
+
item.className = "chat-mentions-item";
|
|
265
|
+
item.setAttribute("data-chat-mention-index", String(index));
|
|
266
|
+
if (index === mentionActiveIndex) {
|
|
267
|
+
item.classList.add("chat-mentions-item--active");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
var token = document.createElement("span");
|
|
271
|
+
token.className = "chat-mentions-item__token";
|
|
272
|
+
token.textContent = option.token;
|
|
273
|
+
|
|
274
|
+
var label = document.createElement("span");
|
|
275
|
+
label.className = "chat-mentions-item__label";
|
|
276
|
+
label.textContent = option.label;
|
|
277
|
+
|
|
278
|
+
item.appendChild(token);
|
|
279
|
+
item.appendChild(label);
|
|
280
|
+
item.addEventListener("mousedown", function (event) {
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
});
|
|
283
|
+
item.addEventListener("click", function () {
|
|
284
|
+
insertMentionOption(option);
|
|
285
|
+
});
|
|
286
|
+
menu.appendChild(item);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
menu.hidden = false;
|
|
290
|
+
menu.classList.add("chat-mentions-menu--open");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function selectActiveMention() {
|
|
294
|
+
if (!mentionMatches.length) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
insertMentionOption(mentionMatches[mentionActiveIndex]);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function updateMentionMenu() {
|
|
303
|
+
var context = mentionContext();
|
|
304
|
+
if (!context) {
|
|
305
|
+
hideMentionMenu();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
mentionMatches = matchingMentionOptions(context.query);
|
|
310
|
+
if (!mentionMatches.length) {
|
|
311
|
+
hideMentionMenu();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
mentionActiveIndex = 0;
|
|
316
|
+
renderMentionMenu();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function handleMentionKeydown(event) {
|
|
320
|
+
if (!mentionMenu || mentionMenu.hidden || !mentionMatches.length) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (event.key === "ArrowDown") {
|
|
325
|
+
event.preventDefault();
|
|
326
|
+
setMentionActiveIndex(mentionActiveIndex + 1);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (event.key === "ArrowUp") {
|
|
331
|
+
event.preventDefault();
|
|
332
|
+
setMentionActiveIndex(mentionActiveIndex - 1);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
337
|
+
event.preventDefault();
|
|
338
|
+
return selectActiveMention();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (event.key === "Escape") {
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
hideMentionMenu();
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
hideMenu: hideMentionMenu,
|
|
352
|
+
updateMenu: updateMentionMenu,
|
|
353
|
+
handleKeydown: handleMentionKeydown,
|
|
354
|
+
setOptions: function (nextMentionOptions) {
|
|
355
|
+
mentionOptions = Array.isArray(nextMentionOptions) ? nextMentionOptions : [];
|
|
356
|
+
updateMentionMenu();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
namespace.parseMentionOptions = parseMentionOptions;
|
|
362
|
+
namespace.parseMentionTokens = parseMentionTokens;
|
|
363
|
+
namespace.mentionKind = mentionKind;
|
|
364
|
+
namespace.emptyMentionAutocomplete = emptyMentionAutocomplete;
|
|
365
|
+
namespace.setupMentionAutocomplete = setupMentionAutocomplete;
|
|
366
|
+
})();
|