PageSourceSearch

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

js pupwife.com collected 2026-09-30 15:07:28 UTC 27,252 bytes, 674 lines download raw bytes

1/**
2 * CitrusOS compact chatroom — toolbar, side aux windows, auth gate, expand.
3 */
4(function (global) {
5    'use strict';
6
7    var toolbarEl = null;
8    var vcBarEl = null;
9    var authGateEl = null;
10    var bound = false;
11    var vcBarOpen = false;
12    var vcControlsSlot = null;
13    var vcDisconnectSlot = null;
14    var resizeObserver = null;
15    var voiceChangeHandler = null;
16    /** @type {Record<string, { placeholder: Comment, el: HTMLElement }>} */
17    var auxSlots = {};
18
19    var PANELS = {
20        channels: {
21            label: 'Channels',
22            icon: 'fa-solid fa-hashtag',
23            selector: '.channels-column',
24            w: 268,
25            h: 480
26        },
27        dms: {
28            label: 'Direct messages',
29            icon: 'fa-solid fa-envelope',
30            selector: '.navigation-column',
31            w: 248,
32            h: 420
33        },
34        members: {
35            label: 'Members',
36            icon: 'fa-solid fa-users',
37            selector: '.members-column',
38            w: 268,
39            h: 480
40        },
41        settings: {
42            label: 'Settings',
43            icon: 'fa-solid fa-gear',
44            selector: '#settings-modal',
45            w: 600,
46            h: 450,
47            resizable: false,
48            isSettings: true
49        }
50    };
51
52    /** Distinct from sitewide Settings (`settings`) so session restore cannot swap them. */
53    var CHAT_SETTINGS_AUX_ID = 'chat-settings';
54
55    function getAuxWindowId(type) {
56        if (type === 'settings') return CHAT_SETTINGS_AUX_ID;
57        return type;
58    }
59
60    function isCitrusHost() {
61        return !!(global.document.body && global.document.body.classList.contains('page-citrusos'));
62    }
63
64    function recoverChatSettingsDom() {
65        var modal = global.document.getElementById('settings-modal');
66        if (modal) {
67            modal.classList.remove('chatroom-citrus-aux-panel');
68            modal.style.removeProperty('display');
69            modal.style.removeProperty('width');
70            modal.style.removeProperty('height');
71            modal.style.display = 'none';
72            modal.hidden = true;
73            if (modal.parentNode !== global.document.body) {
74                global.document.body.appendChild(modal);
75            }
76        }
77        auxSlots = {};
78        if (global.CitrusDesktop && typeof global.CitrusDesktop.closeAuxWindow === 'function') {
79            global.CitrusDesktop.closeAuxWindow(CHAT_SETTINGS_AUX_ID);
80        }
81    }
82
83    function isFullscreenChat() {
84        var root = global.document.querySelector('[data-citrus-app-root]');
85        return !!(root && root.closest('.citrus-fullscreen__content'));
86    }
87
88    function getLayout() {
89        if (!isCitrusHost()) return 'full';
90        var root = global.document.querySelector('[data-citrus-app-root]');
91        if (root && root.closest('.citrus-fullscreen__content')) return 'full';
92        if (root && root.closest('.citrus-window__content')) return 'compact';
93        return global.__chatroomCitrusLayout === 'full' ? 'full' : 'compact';
94    }
95
96    function getWrap() {
97        if (global.ChatroomSpa && typeof global.ChatroomSpa.getPageWrap === 'function') {
98            return global.ChatroomSpa.getPageWrap();
99        }
100        var root = global.document.querySelector('[data-citrus-app-root]');
101        if (root && root.id === 'page-wrap') return root;
102        if (root && root.querySelector) {
103            var nested = root.querySelector('#page-wrap');
104            if (nested) return nested;
105        }
106        return global.document.getElementById('page-wrap');
107    }
108
109    function getCitrusHosts() {
110        var root = global.document.querySelector('[data-citrus-app-root]');
111        if (!root) {
112            return { content: null, body: null };
113        }
114        var content = root.closest('.citrus-window__content, .citrus-fullscreen__content');
115        var body = content && content.closest('.citrus-window__body');
116        return { content: content, body: body };
117    }
118
119    function syncWindowChrome() {
120        if (!isCitrusHost()) return;
121        var hosts = getCitrusHosts();
122        var compact = getLayout() === 'compact';
123        if (hosts.content) {
124            hosts.content.classList.toggle('citrus-window__content--chat-compact', compact);
125            hosts.content.classList.toggle('citrus-window__content--chat-full', !compact);
126            hosts.content.classList.toggle('citrus-fullscreen__content--chat-full',
127                !compact && hosts.content.classList.contains('citrus-fullscreen__content'));
128        }
129        if (hosts.body) {
130            hosts.body.classList.toggle('citrus-window__body--chat-compact', compact);
131        }
132    }
133
134    function clearWindowChrome() {
135        global.document.querySelectorAll('.citrus-window__content--chat-compact').forEach(function (el) {
136            el.classList.remove('citrus-window__content--chat-compact');
137        });
138        global.document.querySelectorAll('.citrus-window__content--chat-full').forEach(function (el) {
139            el.classList.remove('citrus-window__content--chat-full');
140        });
141        global.document.querySelectorAll('.citrus-fullscreen__content--chat-full').forEach(function (el) {
142            el.classList.remove('citrus-fullscreen__content--chat-full');
143        });
144        global.document.querySelectorAll('.citrus-window__body--chat-compact').forEach(function (el) {
145            el.classList.remove('citrus-window__body--chat-compact');
146        });
147    }
148
149    function applyLayoutMode() {
150        var wrap = getWrap();
151        if (!wrap || !isCitrusHost()) return;
152        wrap.setAttribute('data-chatroom-layout', getLayout());
153        global.document.body.classList.toggle('chatroom-citrus-compact', getLayout() === 'compact');
154        global.document.body.classList.toggle('chatroom-citrus-full', getLayout() === 'full');
155        if (getLayout() === 'full') {
156            setVcBarOpen(false);
157        }
158        syncWindowChrome();
159        if (global.ChatroomSpa && typeof global.ChatroomSpa.scheduleChatroomThemeApply === 'function') {
160            global.ChatroomSpa.scheduleChatroomThemeApply();
161        } else if (global.ChatroomSpa && typeof global.ChatroomSpa.applyChatroomTheme === 'function') {
162            global.ChatroomSpa.applyChatroomTheme();
163        }
164    }
165
166    function isVoiceConnected() {
167        if (!global.ChatroomVoiceChat || typeof global.ChatroomVoiceChat.getStatus !== 'function') {
168            return false;
169        }
170        var status = global.ChatroomVoiceChat.getStatus();
171        return !!(status && status.connected);
172    }
173
174    function getVoiceChannelLabel() {
175        var el = global.document.getElementById('voice-status-channel');
176        if (el && el.textContent) return el.textContent.trim();
177        return 'Whimsy Zone';
178    }
179
180    function restoreVcControlSlot(slot) {
181        if (!slot || !slot.el || !slot.placeholder.parentNode) return;
182        slot.el.style.removeProperty('display');
183        slot.placeholder.parentNode.insertBefore(slot.el, slot.placeholder);
184        slot.placeholder.remove();
185    }
186
187    function restoreVcControls() {
188        restoreVcControlSlot(vcControlsSlot);
189        restoreVcControlSlot(vcDisconnectSlot);
190        vcControlsSlot = null;
191        vcDisconnectSlot = null;
192    }
193
194    function mountVcControls(host) {
195        if (!host) return;
196        if (vcControlsSlot && host.contains(vcControlsSlot.el)) {
197            return;
198        }
199
200        var controls = global.document.querySelector('#voice-chat-category .voice-chat-controls-sidebar');
201        if (controls && controls.parentNode && !vcControlsSlot) {
202            var controlsPlaceholder = global.document.createComment('citrus-vc-controls-slot');
203            controls.parentNode.insertBefore(controlsPlaceholder, controls);
204            vcControlsSlot = { placeholder: controlsPlaceholder, el: controls };
205            host.appendChild(controls);
206        }
207
208        var disconnectBtn = global.document.getElementById('voice-disconnect-btn');
209        if (disconnectBtn && disconnectBtn.parentNode && !vcDisconnectSlot) {
210            var disconnectPlaceholder = global.document.createComment('citrus-vc-disconnect-slot');
211            disconnectBtn.parentNode.insertBefore(disconnectPlaceholder, disconnectBtn);
212            vcDisconnectSlot = { placeholder: disconnectPlaceholder, el: disconnectBtn };
213            host.appendChild(disconnectBtn);
214        }
215    }
216
217    function syncVcBarContents() {
218        if (!vcBarEl) return;
219        var joinEl = vcBarEl.querySelector('.chatroom-citrus-vc-bar__join');
220        var controlsHost = vcBarEl.querySelector('.chatroom-citrus-vc-bar__controls-host');
221        var channelName = vcBarEl.querySelector('.chatroom-citrus-vc-bar__channel-name');
222        var connected = isVoiceConnected();
223
224        if (channelName) channelName.textContent = getVoiceChannelLabel();
225        if (joinEl) joinEl.hidden = connected;
226        if (controlsHost) controlsHost.hidden = !connected;
227
228        if (connected && vcBarOpen) {
229            mountVcControls(controlsHost);
230        } else {
231            restoreVcControls();
232        }
233    }
234
235    function setVcBarOpen(open) {
236        vcBarOpen = !!open;
237        if (!vcBarEl) return;
238        vcBarEl.classList.toggle('chatroom-citrus-vc-bar--open', vcBarOpen);
239        var voiceBtn = toolbarEl && toolbarEl.querySelector('[data-citrus-chat="voice"]');
240        if (voiceBtn) voiceBtn.setAttribute('aria-pressed', vcBarOpen ? 'true' : 'false');
241        if (vcBarOpen) {
242            syncVcBarContents();
243        } else {
244            restoreVcControls();
245        }
246    }
247
248    function toggleVcBar() {
249        if (getLayout() !== 'compact') return;
250        setVcBarOpen(!vcBarOpen);
251    }
252
253    function joinVoiceFromBar() {
254        if (!global.ChatroomVoiceChat) return Promise.resolve();
255        var connectFn = global.ChatroomVoiceChat.connectWithConfirmation || global.ChatroomVoiceChat.connect;
256        if (typeof connectFn !== 'function') return Promise.resolve();
257        return Promise.resolve(connectFn()).then(function () {
258            syncVcBarContents();
259        }).catch(function (err) {
260            console.warn('[ChatroomCitrusMenus] voice join failed', err);
261        });
262    }
263
264    function buildVcBar() {
265        var container = global.document.querySelector('.chatroom-container');
266        if (!container || vcBarEl) return;
267        vcBarEl = global.document.createElement('div');
268        vcBarEl.className = 'chatroom-citrus-vc-bar';
269        vcBarEl.setAttribute('role', 'region');
270        vcBarEl.setAttribute('aria-label', 'Voice controls');
271        vcBarEl.innerHTML =
272            '<div class="chatroom-citrus-vc-bar__join">' +
273            '<span class="chatroom-citrus-vc-bar__channel-name">Whimsy Zone</span>' +
274            '<button type="button" class="chatroom-citrus-vc-bar__join-btn chatroom-citrus-chrome-btn citrus-tool-btn">Join voice</button>' +
275            '</div>' +
276            '<div class="chatroom-citrus-vc-bar__controls-host" hidden></div>';
277        var joinBtn = vcBarEl.querySelector('.chatroom-citrus-vc-bar__join-btn');
278        if (joinBtn) {
279            joinBtn.addEventListener('click', function () {
280                joinVoiceFromBar();
281            });
282        }
283        if (toolbarEl && toolbarEl.nextSibling) {
284            container.insertBefore(vcBarEl, toolbarEl.nextSibling);
285        } else {
286            container.insertBefore(vcBarEl, container.firstChild);
287        }
288    }
289
290    function bindVoiceConnectionListener() {
291        if (voiceChangeHandler) return;
292        voiceChangeHandler = function () {
293            if (!vcBarOpen) return;
294            syncVcBarContents();
295        };
296        global.addEventListener('pupwife:voice-connection-change', voiceChangeHandler);
297    }
298
299    function unbindVoiceConnectionListener() {
300        if (!voiceChangeHandler) return;
301        global.removeEventListener('pupwife:voice-connection-change', voiceChangeHandler);
302        voiceChangeHandler = null;
303    }
304
305    function bindCompactResizeObserver() {
306        if (resizeObserver || typeof global.ResizeObserver !== 'function') return;
307        var hosts = getCitrusHosts();
308        var target = hosts.body || hosts.content;
309        if (!target) return;
310        resizeObserver = new global.ResizeObserver(function () {
311            if (getLayout() !== 'compact') return;
312            if (global.ChatroomSpa && typeof global.ChatroomSpa.refreshChatroomScrollLayout === 'function') {
313                global.ChatroomSpa.refreshChatroomScrollLayout();
314            }
315        });
316        resizeObserver.observe(target);
317    }
318
319    function unbindCompactResizeObserver() {
320        if (resizeObserver) {
321            resizeObserver.disconnect();
322            resizeObserver = null;
323        }
324    }
325
326    function resolvePanelElement(type) {
327        var config = PANELS[type];
328        if (!config) return null;
329        if (config.isSettings) {
330            if (global.ChatroomSettings && typeof global.ChatroomSettings.showSettings === 'function') {
331                global.ChatroomSettings.showSettings();
332            }
333            return global.document.getElementById('settings-modal');
334        }
335        return global.document.querySelector(config.selector);
336    }
337
338    function restoreAuxSlot(type) {
339        var state = auxSlots[type];
340        if (!state || !state.el || !state.placeholder.parentNode) {
341            delete auxSlots[type];
342            return;
343        }
344        state.el.classList.remove('chatroom-citrus-aux-panel');
345        state.el.style.removeProperty('display');
346        state.el.style.removeProperty('width');
347        state.el.style.removeProperty('height');
348        state.placeholder.parentNode.insertBefore(state.el, state.placeholder);
349        state.placeholder.remove();
350        delete auxSlots[type];
351
352        if (PANELS[type] && PANELS[type].isSettings) {
353            var settingsModal = global.document.getElementById('settings-modal');
354            if (settingsModal) {
355                settingsModal.style.display = 'none';
356                settingsModal.hidden = true;
357            }
358            global.document.body.classList.remove('settings-open');
359        }
360    }
361
362    function closeAuxPanel(type) {
363        if (global.CitrusDesktop && typeof global.CitrusDesktop.closeAuxWindow === 'function') {
364            global.CitrusDesktop.closeAuxWindow(getAuxWindowId(type));
365        } else {
366            restoreAuxSlot(type);
367        }
368    }
369
370    function closeAllAuxWindows() {
371        Object.keys(auxSlots).slice().forEach(function (type) {
372            closeAuxPanel(type);
373        });
374    }
375
376    function syncThemeToAuxHost(hostEl) {
377        if (!hostEl) return;
378        var wrap = getWrap();
379        if (!wrap) return;
380        if (global.ChatroomSpa && typeof global.ChatroomSpa.applyThemeToElement === 'function') {
381            global.ChatroomSpa.applyThemeToElement(hostEl);
382            return;
383        }
384        var attrs = [
385            'data-chatroom-theme',
386            'data-chatroom-color-mode',
387            'data-chatroom-font-size',
388            'data-chatroom-emoji-size'
389        ];
390        var i;
391        for (i = 0; i < attrs.length; i++) {
392            var name = attrs[i];
393            if (wrap.hasAttribute(name)) {
394                hostEl.setAttribute(name, wrap.getAttribute(name));
395            } else {
396                hostEl.removeAttribute(name);
397            }
398        }
399        hostEl.classList.toggle('animations', wrap.classList.contains('animations'));
400    }
401
402    function openAuxPanel(type) {
403        if (!global.CitrusDesktop || typeof global.CitrusDesktop.openAuxWindow !== 'function') return;
404        if (getLayout() !== 'compact') return;
405
406        if (auxSlots[type]) {
407            closeAuxPanel(type);
408            return;
409        }
410
411        var config = PANELS[type];
412        if (!config) return;
413
414        var el = resolvePanelElement(type);
415        if (!el || !el.parentNode) return;
416
417        var placeholder = global.document.createComment('citrus-chat-slot-' + type);
418        el.parentNode.insertBefore(placeholder, el);
419        auxSlots[type] = { placeholder: placeholder, el: el };
420
421        global.CitrusDesktop.openAuxWindow({
422            id: getAuxWindowId(type),
423            label: config.label,
424            icon: config.icon,
425            w: config.w,
426            h: config.h,
427            nearChat: true,
428            resizable: config.resizable === false ? false : undefined,
429            auxClass: type === 'settings' ? 'citrus-window--settings' : '',
430            onMount: function (contentEl) {
431                contentEl.classList.add('chatroom-citrus-aux-host');
432                if (type !== 'settings') {
433                    syncThemeToAuxHost(contentEl);
434                }
435                contentEl.appendChild(el);
436                el.classList.add('chatroom-citrus-aux-panel');
437                el.style.display = 'flex';
438            },
439            onClose: function () {
440                restoreAuxSlot(type);
441            }
442        });
443    }
444
445    function wireToolbarButtons() {
446        if (!toolbarEl) return;
447        var types = ['channels', 'dms', 'members', 'settings'];
448        types.forEach(function (type) {
449            var btn = toolbarEl.querySelector('[data-citrus-chat="' + type + '"]');
450            if (!btn) return;
451            btn.addEventListener('click', function () {
452                openAuxPanel(type);
453            });
454        });
455
456        var voiceBtn = toolbarEl.querySelector('[data-citrus-chat="voice"]');
457        if (voiceBtn) {
458            voiceBtn.setAttribute('aria-pressed', 'false');
459            voiceBtn.addEventListener('click', function () {
460                toggleVcBar();
461            });
462        }
463
464        var expandBtn = toolbarEl.querySelector('[data-citrus-chat="expand"]');
465        if (expandBtn) {
466            expandBtn.addEventListener('click', function () {
467                if (!global.CitrusDesktop || !global.CitrusAppRegistry) return;
468                closeAllAuxWindows();
469                setVcBarOpen(false);
470                var app = global.CitrusAppRegistry.getApp('chat');
471                if (app && typeof global.CitrusDesktop.openFullscreen === 'function') {
472                    global.CitrusDesktop.openFullscreen(app);
473                }
474            });
475        }
476    }
477
478    function buildToolbar() {
479        var container = global.document.querySelector('.chatroom-container');
480        if (!container || toolbarEl) return;
481        toolbarEl = global.document.createElement('div');
482        toolbarEl.className = 'chatroom-citrus-toolbar';
483        toolbarEl.setAttribute('role', 'toolbar');
484        toolbarEl.setAttribute('aria-label', 'Chat controls');
485        toolbarEl.innerHTML =
486            '<button type="button" class="chatroom-citrus-toolbar__btn chatroom-citrus-chrome-btn citrus-tool-btn" data-citrus-chat="channels" title="Channels" aria-label="Channels"><i class="fa-solid fa-hashtag" aria-hidden="true"></i><span>Channels</span></button>' +
487            '<button type="button" class="chatroom-citrus-toolbar__btn chatroom-citrus-chrome-btn citrus-tool-btn" data-citrus-chat="dm
487s" title="Direct messages" aria-label="Direct messages"><i class="fa-solid fa-envelope" aria-hidden="true"></i><span>DMs</span></button>' +
488            '<button type="button" class="chatroom-citrus-toolbar__btn chatroom-citrus-chrome-btn citrus-tool-btn" data-citrus-chat="members" title="Members" aria-label="Members"><i class="fa-solid fa-users" aria-hidden="true"></i><span>Members</span></button>' +
489            '<button type="button" class="chatroom-citrus-toolbar__btn chatroom-citrus-chrome-btn citrus-tool-btn" data-citrus-chat="voice" title="Voice" aria-label="Voice"><i class="fa-solid fa-microphone" aria-hidden="true"></i><span>Voice</span></button>' +
490            '<button type="button" class="chatroom-citrus-toolbar__btn chatroom-citrus-chrome-btn citrus-tool-btn" data-citrus-chat="settings" title="Settings" aria-label="Settings"><i class="fa-solid fa-gear" aria-hidden="true"></i><span>Settings</span></button>' +
491            '<button type="button" class="chatroom-citrus-toolbar__btn chatroom-citrus-chrome-btn citrus-tool-btn chatroom-citrus-toolbar__btn--expand" data-citrus-chat="expand" title="Expand to full chatroom" aria-label="Expand to full chatroom"><i class="fa-solid fa-up-right-and-down-left-from-center" aria-hidden="true"></i><span>Expand</span></button>';
492        container.insertBefore(toolbarEl, container.firstChild);
493        buildVcBar();
494        wireToolbarButtons();
495    }
496
497    function getAuthGateHost() {
498        if (!isCitrusHost()) {
499            return getWrap();
500        }
501
502        if (global.CitrusRouteScope && typeof global.CitrusRouteScope.getScopeEl === 'function') {
503            var scoped = global.CitrusRouteScope.getScopeEl();
504            if (scoped && scoped.classList &&
505                (scoped.classList.contains('citrus-window__content') ||
506                    scoped.classList.contains('citrus-fullscreen__content')) &&
507                scoped.classList.contains('page-chatroom')) {
508                return scoped;
509            }
510        }
511
512        if (global.CitrusWindowLoader && typeof global.CitrusWindowLoader.getFocusedContainer === 'function') {
513            var focused = global.CitrusWindowLoader.getFocusedContainer();
514            if (focused && focused.classList && focused.classList.contains('page-chatroom')) {
515                return focused;
516            }
517        }
518
519        var chatWindow = global.document.querySelector(
520            '.citrus-window[data-app-id="chat"] .citrus-window__content.page-chatroom'
521        );
522        if (chatWindow) return chatWindow;
523
524        return global.document.querySelector(
525            '.citrus-window__content.page-chatroom, .citrus-fullscreen__content.page-chatroom'
526        );
527    }
528
529    function getChatroomContainer(host) {
530        if (host && host.querySelector) {
531            var inHost = host.querySelector('.chatroom-container');
532            if (inHost) return inHost;
533        }
534        return null;
535    }
536
537    function mountAuthGate(host) {
538        if (!host) return null;
539        if (!authGateEl) {
540            authGateEl = global.document.createElement('div');
541            authGateEl.className = 'chatroom-citrus-auth';
542            authGateEl.innerHTML =
543                '<div class="chatroom-citrus-auth__panel">' +
544                '<i class="fa-solid fa-lock chatroom-citrus-auth__icon" aria-hidden="true"></i>' +
545                '<h2 class="chatroom-citrus-auth__title">Sign in to chat</h2>' +
546                '<p class="chatroom-citrus-auth__text">You need an account to use the chatroom.</p>' +
547                '<button type="button" class="chatroom-citrus-auth__btn citrus-tool-btn" data-citrus-chat-auth="signin">Open account</button>' +
548                '</div>';
549            authGateEl.querySelector('[data-citrus-chat-auth="signin"]').addEventListener('click', function () {
550                if (global.CitrusAccountWindow && typeof global.CitrusAccountWindow.open === 'function') {
551                    global.CitrusAccountWindow.open();
552                    return;
553                }
554                if (global.CitrusDesktop && typeof global.CitrusDesktop.openHref === 'function') {
555                    global.CitrusDesktop.openHref('/account.html');
556                } else {
557                    global.location.assign('/account.html');
558                }
559            });
560        }
561        if (authGateEl.parentNode !== host) {
562            host.appendChild(authGateEl);
563        }
564        return authGateEl;
565    }
566
567    function hideAuthGate() {
568        var host = getAuthGateHost() || (authGateEl ? authGateEl.parentNode : null);
569        if (authGateEl) {
570            authGateEl.hidden = true;
571        }
572        var container = getChatroomContainer(host);
573        if (container) container.style.display = '';
574    }
575
576    function showAuthGate() {
577        applyLayoutMode();
578        var host = getAuthGateHost();
579        if (!host) {
580            hideAuthGate();
581            return;
582        }
583        mountAuthGate(host);
584        if (!authGateEl) return;
585        authGateEl.hidden = false;
586        var container = getChatroomContainer(host);
587        if (container) container.style.display = 'none';
588    }
589
590    function init() {
591        if (!isCitrusHost()) return;
592        recoverChatSettingsDom();
593        applyLayoutMode();
594        if (getLayout() === 'compact') {
595            buildToolbar();
596            bindVoiceConnectionListener();
597            bindCompactResizeObserver();
598        } else {
599            setVcBarOpen(false);
600            if (toolbarEl) {
601                toolbarEl.style.display = 'none';
602            }
603        }
604        if (global.ChatroomCitrusUnread && typeof global.ChatroomCitrusUnread.init === 'function') {
605            global.ChatroomCitrusUnread.init();
606        }
607        if (global.ChatroomSpa && typeof global.ChatroomSpa.scheduleChatroomThemeApply === 'function') {
608            global.ChatroomSpa.scheduleChatroomThemeApply();
609        } else if (global.ChatroomSpa && typeof global.ChatroomSpa.applyChatroomTheme === 'function') {
610            global.ChatroomSpa.applyChatroomTheme();
611        }
612    }
613
614    function destroy() {
615        closeAllAuxWindows();
616        setVcBarOpen(false);
617        unbindVoiceConnectionListener();
618        unbindCompactResizeObserver();
619        if (vcBarEl) {
620            vcBarEl.remove();
621            vcBarEl = null;
622        }
623        if (toolbarEl) {
624            toolbarEl.remove();
625            toolbarEl = null;
626        }
627        if (authGateEl) {
628            authGateEl.remove();
629            authGateEl = null;
630        }
631        if (global.ChatroomCitrusUnread && typeof global.ChatroomCitrusUnread.destroy === 'function') {
632            global.ChatroomCitrusUnread.destroy();
633        }
634        global.document.body.classList.remove('chatroom-citrus-compact', 'chatroom-citrus-full');
635        clearWindowChrome();
636    }
637
638    function onFullscreenClosed() {
639        global.__chatroomCitrusLayout = 'compact';
640        setVcBarOpen(false);
641        applyLayoutMode();
642        if (toolbarEl) {
643            toolbarEl.style.display = '';
644            toolbarEl.style.removeProperty('flex');
645            toolbarEl.style.removeProperty('width');
646            toolbarEl.style.removeProperty('height');
647        }
648        if (global.ChatroomSpa && typeof global.ChatroomSpa.refreshChatroomScrollLayout === 'function') {
649            global.ChatroomSpa.refreshChatroomScrollLayout();
650        }
651        if (global.ChatroomCitrusUnread && typeof global.ChatroomCitrusUnread.refresh === 'function') {
652            global.ChatroomCitrusUnread.refresh();
653        }
654        bindVoiceConnectionListener();
655        bindCompactResizeObserver();
656        global.requestAnimationFrame(function () {
657            applyLayoutMode();
658        });
659    }
660
661    global.ChatroomCitrusMenus = {
662        init: init,
663        destroy: destroy,
664        showAuthGate: showAuthGate,
665        hideAuthGate: hideAuthGate,
666        applyLayoutMode: applyLayoutMode,
667        onFullscreenClosed: onFullscreenClosed,
668        getLayout: getLayout,
669        closeAllAuxWindows: closeAllAuxWindows,
670        openAuxPanel: openAuxPanel,
671        toggleVcBar: toggleVcBar,
672        syncVcBarContents: syncVcBarContents
673    };
674})(typeof window !== 'undefined' ? window : this);

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.