PageSourceSearch

https://pupwife.com/projects/chatroom/scripts/chatroomMessageMeta.js

js pupwife.com collected 2026-09-30 15:07:20 UTC 23,161 bytes, 593 lines download raw bytes

1/**
2 * Message edit, pin, and live update helpers.
3 */
4window.ChatroomMessageMeta = (function () {
5    'use strict';
6
7    let pinnedListener = null;
8    let pinnedRef = null;
9    let changedListener = null;
10    let changedRef = null;
11    let currentChannel = null;
12    let cachedPinned = null;
13    let popupOpen = false;
14    let outsideClickHandler = null;
15    let escapeHandler = null;
16
17    function escapeHtml(text) {
18        const div = document.createElement('div');
19        div.textContent = text == null ? '' : String(text);
20        return div.innerHTML;
21    }
22
23    function getMessageUsername(messageEl) {
24        const el = messageEl.querySelector('.message-username');
25        if (!el) return '';
26        return el.textContent.replace(/👑/g, '').trim();
27    }
28
29    function getMessagesRef() {
30        const currentDM = window.ChatroomDMs && window.ChatroomDMs.getCurrentDM
31            ? window.ChatroomDMs.getCurrentDM()
32            : null;
33        const currentUser = window.__sharedUsername ? window.__sharedUsername.get() : '';
34        if (currentDM && currentUser) {
35            const conversationId = [currentUser, currentDM].sort().join('_');
36            return firebase.database().ref('chatroom/dms/' + conversationId + '/messages');
37        }
38        const channel = window.Chatroom && window.Chatroom.getCurrentChannel
39            ? window.Chatroom.getCurrentChannel()
40            : 'general';
41        return firebase.database().ref('chatroom/channels/' + channel + '/messages');
42    }
43
44    function getPinnedRef(channelId) {
45        return firebase.database().ref('chatroom/channels/' + channelId + '/pinned');
46    }
47
48    function findMessageKey(snapshot, messageId) {
49        if (!snapshot.exists()) return null;
50        const messages = snapshot.val();
51        if (messages[messageId]) return messageId;
52        for (const key in messages) {
53            if (messages[key] && messages[key].id === messageId) return key;
54        }
55        return null;
56    }
57
58    function editedLabelHtml(message) {
59        if (!message || (!message.edited && !message.editedAt)) return '';
60        return '<span class="message-edited" title="Edited">(edited)</span>';
61    }
62
63    function applyEditedLabel(messageEl, message) {
64        const header = messageEl.querySelector('.message-header');
65        if (!header) return;
66        let label = header.querySelector('.message-edited');
67        if (message && (message.edited || message.editedAt)) {
68            if (!label) {
69                label = document.createElement('span');
70                label.className = 'message-edited';
71                label.title = 'Edited';
72                label.textContent = '(edited)';
73                const time = header.querySelector('.message-time');
74                if (time) header.insertBefore(label, time);
75                else header.appendChild(label);
76            }
77        } else if (label) {
78            label.remove();
79        }
80    }
81
82    function updateMessageTextDom(messageId, text, messageData) {
83        const el = document.querySelector('.message[data-message-id="' + messageId + '"]');
84        if (!el) return;
85        const textEl = el.querySelector('.message-text');
86        if (textEl) {
87            textEl.innerHTML = window.ChatroomMessageFormatter
88                ? window.ChatroomMessageFormatter.formatMessage(String(text || ''))
89                : escapeHtml(String(text || ''));
90        }
91        applyEditedLabel(el, messageData || { edited: true });
92    }
93
94    function saveMessageEdit(messageEl, newText) {
95        const messageId = messageEl.dataset.messageId;
96        if (!messageId || !newText) return Promise.resolve(false);
97
98        const ref = getMessagesRef();
99        return ref.once('value').then(function (snapshot) {
100            const key = findMessageKey(snapshot, messageId);
101            if (!key) {
102                showToast('Message not found.');
103                return false;
104            }
105            const msg = snapshot.val()[key];
106            const owner = msg && msg.username;
107            const me = window.__sharedUsername ? window.__sharedUsername.get() : '';
108            const isMod = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator();
109            if (owner !== me && !isMod) {
110                showToast('You can only edit your own messages.');
111                return false;
112            }
113            return ref.child(key).update({
114                text: newText,
115                edited: true,
116                editedAt: Date.now()
117            }).then(function () {
118                updateMessageTextDom(messageId, newText, { edited: true, editedAt: Date.now() });
119                showToast('Message updated.');
120                return true;
121            });
122        }).catch(function (err) {
123            console.error('[ChatroomMessageMeta] edit:', err);
124            showToast('Could not save edit.');
125            return false;
126        });
127    }
128
129    function startEdit(messageEl) {
130        const messageText = messageEl.querySelector('.message-text');
131        if (!messageText) {
132            showToast('This message has no text to edit.');
133            return;
134        }
135        const originalText = messageText.textContent;
136
137        const input = document.createElement('textarea');
138        input.value = originalText;
139        input.className = 'message-edit-input';
140        input.rows = 3;
141
142        messageText.replaceWith(input);
143        input.focus();
144        input.select();
145
146        const scrollEditedMessageIntoView = function () {
147            messageEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
148        };
149        requestAnimationFrame(scrollEditedMessageIntoView);
150        if (typeof ResizeObserver !== 'undefined') {
151            const editResizeObserver = new ResizeObserver(scrollEditedMessageIntoView);
152            editResizeObserver.observe(input);
153            input.addEventListener('blur', function () {
154                editResizeObserver.disconnect();
155            }, { once: true });
156        }
157
158        let saving = false;
159
160        const restore = function () {
161            if (input.parentNode) input.replaceWith(messageText);
162        };
163
164        const saveEdit = function () {
165            if (saving) return;
166            const newText = input.value.trim();
167            if (!newText) {
168                restore();
169                return;
170            }
171            if (newText === originalText) {
172                restore();
173                return;
174            }
175            saving = true;
176            saveMessageEdit(messageEl, newText).then(function (ok) {
177                if (ok) {
178                    if (window.ChatroomMessageFormatter) {
179                        messageText.innerHTML = window.ChatroomMessageFormatter.formatMessage(newText);
180                    } else {
181                        messageText.textContent = newText;
182                    }
183                    applyEditedLabel(messageEl, { edited: true, editedAt: Date.now() });
184                }
185            }).finally(function () {
186                saving = false;
187                restore();
188            });
189        };
190
191        input.addEventListener('blur', function () {
192            setTimeout(saveEdit, 0);
193        });
194        input.addEventListener('keydown', function (e) {
195            if (e.key === 'Enter' && !e.shiftKey) {
196                e.preventDefault();
197                input.blur();
198            }
199            if (e.key === 'Escape') {
200                saving = true;
201                restore();
202            }
203        });
204    }
205
206    function pinMessage(messageEl) {
207        const isMod = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator();
208        if (!isMod) {
209            showToast('Only moderators can pin messages.');
210            return;
211        }
212        const currentDM = window.ChatroomDMs && window.ChatroomDMs.getCurrentDM
213            ? window.ChatroomDMs.getCurrentDM()
214            : null;
215        if (currentDM) {
216            showToast('Pinning is only available in server channels.');
217            return;
218        }
219
220        const channel = window.Chatroom && window.Chatroom.getCurrentChannel
221            ? window.Chatroom.getCurrentChannel()
222            : 'general';
223        const messageId = messageEl.dataset.messageId;
224        const me = window.__sharedUsername ? window.__sharedUsername.get() : '';
225
226        const ref = getMessagesRef();
227        ref.once('value').then(function (snapshot) {
228            const key = findMessageKey(snapshot, messageId);
229            if (!key) {
230                showToast('Message not found.');
231                return;
232            }
233            const msg = snapshot.val()[key];
234            const username = (msg && msg.username) || getMessageUsername(messageEl) || 'Unknown';
235            const rawText = msg && msg.text != null
236                ? String(msg.text)
237                : ((messageEl.querySelector('.message-text') || {}).textContent || '');
238            return getPinnedRef(channel).child(key).set({
239                messageKey: key,
240                messageId: messageId,
241                username: username,
242                text: rawText.substring(0, 500),
243                messageTimestamp: (msg && msg.timestamp) || Date.now(),
244                pinnedBy: me,
245                pinnedAt: Date.now()
246            }).then(function () {
247                showToast('Message pinned.');
248            });
249        }).catch(function (err) {
250            console.error('[ChatroomMessageMeta] pin:', err);
251            showToast('Could not pin message.');
252        });
253    }
254
255    function unpinMessage(channelId, messageKey) {
256        const isMod = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator();
257        if (!isMod) return Promise.resolve(false);
258        return getPinnedRef(channelId).child(messageKey).remove()
259            .then(function () {
260                showToast('Message unpinned.');
261                return true;
262            });
263    }
264
265    function formatPinnedText(text) {
266        const raw = (text || '').trim();
267        if (!raw) return 'Attachment';
268        if (window.ChatroomMessageFormatter && window.ChatroomMessageFormatter.formatMessage) {
269            return window.ChatroomMessageFormatter.formatMessage(raw);
270        }
271        return escapeHtml(raw).replace(/\n/g, '<br>');
272    }
273
274    function formatPinnedTime(ts) {
275        const date = new Date(ts);
276        if (Number.isNaN(date.getTime())) return '';
277        return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {
278            hour: 'numeric',
279            minute: '2-digit'
280        });
281    }
282
283    function getPinnedAvatarHtml(username) {
284        const profilePicture = window.ChatroomProfilePicture && window.ChatroomProfilePicture.getProfilePicture
285            ? window.ChatroomProfilePicture.getProfilePicture(username)
286            : null;
287        if (profilePicture) {
288            return '<img src="' + escapeHtml(profilePicture) + '" alt="" class="pinned-messages-popup__avatar-img">';
289        }
290        const avatars = ['🐾', '🐱', '🐶', '🦊', '🐰', '🐻'];
291        let hash = 0;
292        for (let i = 0; i < (username || '').length; i++) {
293            hash = username.charCodeAt(i) + ((hash << 5) - hash);
294        }
295        return '<span class="pinned-messages-popup__avatar-emoji" aria-hidden="true">' +
296            avatars[Math.abs(hash) % avatars.length] + '</span>';
297    }
298
299    function scrollToPinnedMessage(messageId) {
300        if (!messageId) return;
301        if (window.ChatroomReplies && window.ChatroomReplies.scrollToMessage) {
302            window.ChatroomReplies.scrollToMessage(messageId);
303        } else if (window.Chatroom && window.Chatroom.scrollToMessage) {
304            window.Chatroom.scrollToMessage(messageId);
305        }
306    }
307
308    function isPopupOpen() {
309        const popup = document.getElementById('pinned-messages-popup');
310        return !!(popup && popup.classList.contains('is-open'));
311    }
312
313    function unbindPopupDismiss() {
314        if (outsideClickHandler) {
315            document.removeEventListener('mousedown', outsideClickHandler, true);
316            outsideClickHandler = null;
317        }
318        if (escapeHandler) {
319            document.removeEventListener('keydown', escapeHandler);
320            escapeHandler = null;
321        }
322    }
323
324    function closePinnedPopup() {
325        const popup = document.getElementById('pinned-messages-popup');
326        const btn = document.getElementById('pinned-messages-btn');
327        if (popup) {
328            popup.classList.remove('is-open');
329            popup.hidden = true;
330        }
331        if (btn) btn.setAttribute('aria-expanded', 'false');
332        popupOpen = false;
333        unbindPopupDismiss();
334    }
335
336    function openPinnedPopup() {
337        const popup = document.getElementById('pinned-messages-popup');
338        const btn = document.getElementById('pinned-messages-btn');
339        if (!popup) return;
340        popup.classList.add('is-open');
341        popup.hidden = false;
342        popupOpen = true;
343        if (btn) btn.setAttribute('aria-expanded', 'true');
344        if (currentChannel && cachedPinned) {
345            renderPinnedPopup(currentChannel, cachedPinned);
346        }
347        bindPopupDismiss();
348    }
349
350    function bindPopupDismiss() {
351        unbindPopupDismiss();
352        outsideClickHandler = function (e) {
353            const popup = document.getElementById('pinned-messages-popup');
354            const btn = document.getElementById('pinned-messages-btn');
355            if (!popup || !popup.classList.contains('is-open')) return;
356            if (popup.contains(e.target) || (btn && btn.contains(e.target))) return;
357            closePinnedPopup();
358        };
359        escapeHandler = function (e) {
360            if (e.key === 'Escape') closePinnedPopup();
361        };
362        setTimeout(function () {
363            document.addEventListener('mousedown', outsideClickHandler, true);
364        }, 0);
365        document.addEventListener('keydown', escapeHandler);
366    }
367
368    function ensurePinnedUi() {
369        let btn = document.getElementById('pinned-messages-btn');
370        if (!btn) {
371            const headerRight = document.querySelector('.header-right');
372            const settingsBtn = document.getElementById('settings-btn');
373            if (!headerRight) return;
374            btn = document.createElement('button');
375            btn.type = 'button';
376            btn.id = 'pinned-messages-btn';
377            btn.className = 'header-btn cr-icon-btn';
378            btn.title = 'Pinned Messages';
379            btn.setAttribute('aria-label', 'Pinned Messages');
380            btn.setAttribute('aria-expanded', 'false');
381            btn.hidden = true;
382            if (window.ChatroomIcons) window.ChatroomIcons.set(btn, 'thumbtack');
383            if (settingsBtn) headerRight.insertBefore(btn, settingsBtn);
384            else headerRight.appendChild(btn);
385            btn.addEventListener('click', function (e) {
386                e.stopPropagation();
387                togglePinnedPopup();
388            });
389        }
390
391        let popup = document.getElementById('pinned-messages-popup');
392        if (!popup) {
393            const chatArea = document.querySelector('.chat-area');
394            if (!chatArea) return;
395            popup = document.createElement('div');
396            popup.id = 'pinned-messages-popup';
397            popup.className = 'pinned-messages-popup';
398            popup.hidden = true;
399            popup.setAttribute('role', 'dialog');
400            popup.setAttribute('aria-label', 'Pinned Messages');
401            chatArea.appendChild(popup);
402        }
403    }
404
405    function togglePinnedPopup() {
406        if (isPopupOpen()) {
407            closePinnedPopup();
408            return;
409        }
410        openPinnedPopup();
411    }
412
413    function updatePinButton(count) {
414        const btn = document.getElementById('pinned-messages-btn');
415        if (!btn) return;
416        const show = count > 0 && !!currentChannel;
417        btn.hidden = !show;
418        btn.title = show ? ('Pinned Messages (' + count + ')') : 'Pinned Messages';
419        btn.setAttribute('aria-label', show ? ('Pinned Messages, ' + count) : 'Pinned Messages');
420    }
421
422    function renderPinnedPopup(channelId, pinnedData) {
423        const popup = document.getElementById('pinned-messages-popup');
424        if (!popup) return;
425
426        const entries = pinnedData ? Object.entries(pinnedData) : [];
427        entries.sort(function (a, b) {
428            return (b[1].pinnedAt || 0) - (a[1].pinnedAt || 0);
429        });
430
431        const isMod = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator();
432        let html = '<div class="pinned-messages-popup__header">' +
433            '<div class="pinned-messages-popup__header-title">' +
434            '<i class="fa-solid fa-thumbtack fa-width-auto" aria-hidden="true"></i>' +
435            '<span>Pinned Messages</span></div>' +
436            '<button type="button" class="pinned-messages-popup__close" aria-label="Close pinned messages">' +
437            '<i class="fa-solid fa-xmark fa-width-auto" aria-hidden="true"></i></button></div>';
438
439        if (!entries.length) {
440            html += '<p class="pinned-messages-popup__empty">No pinned messages in this channel.</p>';
441        } else {
442            html += '<div class="pinned-messages-popup__list">';
443            entries.forEach(function (pair) {
444                const key = pair[0];
445                const pin = pair[1];
446                const author = escapeHtml(pin.username || 'Unknown');
447                const text = formatPinnedText(pin.text);
448                const messageId = pin.messageId || key;
449                const time = formatPinnedTime(pin.messageTimestamp || pin.pinnedAt || Date.now());
450                html += '<article class="pinned-messages-popup__item" data-pin-key="' + escapeHtml(key) + '">';
451                html += '<div class="pinned-messages-popup__avatar">' + getPinnedAvatarHtml(pin.username) + '</div>';
452                html += '<div class="pinned-messages-popup__body">';
453                html += '<div class="pinned-messages-popup__meta">';
454                html += '<span class="pinned-messages-popup__author">' + author + '</span>';
455                if (time) html += '<span class="pinned-messages-popup__time">' + escapeHtml(time) + '</span>';
456                html += '</div>';
457                html += '<div class="pinned-messages-popup__text message-text">' + text + '</div>';
458                html += '</div>';
459                html += '<div class="pinned-messages-popup__actions">';
460                html += '<button type="button" class="pinned-messages-popup__jump" data-message-id="' + escapeHtml(messageId) + '">Jump</button>';
461                if (isMod) {
462                    html += '<button type="button" class="pinned-messages-popup__unpin" data-unpin-key="' + escapeHtml(key) + '" title="Unpin" aria-label="Unpin"><i class="fa-solid fa-xmark fa-width-auto" aria-hidden="true"></i></button>';
463                }
464                html += '</div></article>';
465            });
466            html += '</div>';
467        }
468
469        popup.innerHTML = html;
470
471        const closeBtn = popup.querySelector('.pinned-messages-popup__close');
472        if (closeBtn) {
473            closeBtn.addEventListener('click', function (e) {
474                e.stopPropagation();
475                closePinnedPopup();
476            });
477        }
478
479        popup.querySelectorAll('.pinned-messages-popup__jump').forEach(function (btn) {
480            btn.addEventListener('click', function (e) {
481                e.stopPropagation();
482                scrollToPinnedMessage(btn.dataset.messageId);
483                closePinnedPopup();
484            });
485        });
486
487        popup.querySelectorAll('.pinned-messages-popup__unpin').forEach(function (btn) {
488            btn.addEventListener('click', function (e) {
489                e.stopPropagation();
490                const pinKey = btn.dataset.unpinKey;
491                if (pinKey) unpinMessage(channelId, pinKey);
492            });
493        });
494    }
495
496    function renderPinnedState(channelId, pinnedData) {
497        ensurePinnedUi();
498        cachedPinned = pinnedData;
499        const count = pinnedData ? Object.keys(pinnedData).length : 0;
500        updatePinButton(count);
501
502        if (!count) {
503            closePinnedPopup();
504            const popup = document.getElementById('pinned-messages-popup');
505            if (popup) popup.innerHTML = '';
506            return;
507        }
508
509        if (popupOpen) {
510            renderPinnedPopup(channelId, pinnedData);
511        }
512    }
513
514    function removeLegacyPinnedBar() {
515        const bar = document.getElementById('pinned-messages-bar');
516        if (bar) bar.remove();
517    }
518
519    function teardownChannelMeta() {
520        if (pinnedRef && pinnedListener) {
521            pinnedRef.off('value', pinnedListener);
522        }
523        if (changedRef && changedListener) {
524            changedRef.off('child_changed', changedListener);
525        }
526        pinnedRef = null;
527        pinnedListener = null;
528        changedRef = null;
529        changedListener = null;
530        currentChannel = null;
531        cachedPinned = null;
532        closePinnedPopup();
533        updatePinButton(0);
534        removeLegacyPinnedBar();
535    }
536
537    function bindMessageChangeListener(ref) {
538        changedRef = ref;
539        changedListener = changedRef.on('child_changed', function (snap) {
540            const data = snap.val();
541            if (!data) return;
542            const id = snap.key;
543            updateMessageTextDom(id, data.text, data);
544        });
545    }
546
547    function bindChannel(channelId) {
548        if (typeof firebase === 'undefined' || !firebase.database) return;
549        teardownChannelMeta();
550        currentChannel = channelId;
551
552        pinnedRef = getPinnedRef(channelId);
553        ensurePinnedUi();
554        pinnedListener = pinnedRef.on('value', function (snap) {
555            renderPinnedState(channelId, snap.exists() ? snap.val() : null);
556        });
557
558        bindMessageChangeListener(firebase.database().ref('chatroom/channels/' + channelId + '/messages'));
559    }
560
561    function bindDM(otherUsername) {
562        if (typeof firebase === 'undefined' || !firebase.database) return;
563        teardownChannelMeta();
564        currentChannel = null;
565        const me = window.__sharedUsername ? window.__sharedUsername.get() : '';
566        if (!me || !otherUsername) return;
567        const conversationId = [me, otherUsername].sort().join('_');
568        bindMessageChangeListener(firebase.database().ref('chatroom/dms/' + conversationId + '/messages'));
569    }
570
571    function showToast(message) {
572        const toast = document.createElement('div');
573        toast.className = 'toast';
574        toast.textContent = message;
575        document.body.appendChild(toast);
576        setTimeout(function () { toast.classList.add('show'); }, 10);
577        setTimeout(function () {
578            toast.classList.remove('show');
579            setTimeout(function () { toast.remove(); }, 300);
580        }, 2000);
581    }
582
583    return {
584        startEdit: startEdit,
585        pinMessage: pinMessage,
586        unpinMessage: unpinMessage,
587        bindChannel: bindChannel,
588        bindDM: bindDM,
589        teardownChannelMeta: teardownChannelMeta,
590        editedLabelHtml: editedLabelHtml,
591        applyEditedLabel: applyEditedLabel
592    };
593})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.