// static/js/00-config.js "use strict"; window.Glow = window.Glow || {}; Glow.els = { chatBox: document.getElementById("chatBox"), dmBox: document.getElementById("dmBox"), feedBox: document.getElementById("feedBox"), feedButton: document.getElementById("feedButton"), dmsButton: document.getElementById("dmsButton"), composerWrap: document.getElementById("composerWrap"), chatForm: document.getElementById("chatForm"), messageInput: document.getElementById("messageInput"), sendButton: document.getElementById("sendButton"), messageCount: document.getElementById("messageCount"), attachmentInput: document.getElementById("attachmentInput"), attachmentButton: document.getElementById("attachmentButton"), attachmentName: document.getElementById("attachmentName"), authOverlay: document.getElementById("authOverlay"), authNotice: document.getElementById("authNotice"), accessKeyInput: document.getElementById("accessKeyInput"), usernameInput: document.getElementById("usernameInput"), passwordInput: document.getElementById("passwordInput"), loginButton: document.getElementById("loginButton"), registerButton: document.getElementById("registerButton"), roomSelect: document.getElementById("roomSelect"), headerTitle: document.getElementById("headerTitle"), headerSubtitle: document.getElementById("headerSubtitle"), headerAccount: document.getElementById("headerAccount"), presenceDot: document.getElementById("presenceDot"), presenceLabel: document.getElementById("presenceLabel"), logoutButton: document.getElementById("logoutButton"), adminFab: document.getElementById("adminFab"), adminPanel: document.getElementById("adminPanel"), closeAdminButton: document.getElementById("closeAdminButton"), clearMessagesButton: document.getElementById("clearMessagesButton"), createRoomButton: document.getElementById("createRoomButton"), typingToggleButton: document.getElementById("typingToggleButton"), notificationsToggleButton: document.getElementById("notificationsToggleButton"), themeButton: document.getElementById("themeButton"), adminUsers: document.getElementById("adminUsers"), adminRooms: document.getElementById("adminRooms"), adminTeamsChannels: document.getElementById("adminTeamsChannels"), statusToast: document.getElementById("statusToast"), blockedOverlay: document.getElementById("blockedOverlay"), blockedMessage: document.getElementById("blockedMessage"), newMessagesBar: document.getElementById("newMessagesBar"), jumpToLatestButton: document.getElementById("jumpToLatestButton"), typingBar: document.getElementById("typingBar"), dialogOverlay: document.getElementById("dialogOverlay"), dialogTitle: document.getElementById("dialogTitle"), dialogBody: document.getElementById("dialogBody"), dialogActions: document.getElementById("dialogActions"), teamSelect: document.getElementById("teamSelect"), pagesButton: document.getElementById("pagesButton"), backButton: document.getElementById("backButton"), threadBox: document.getElementById("threadBox"), threadComposerWrap: document.getElementById("threadComposerWrap"), threadForm: document.getElementById("threadForm"), threadInput: document.getElementById("threadInput"), threadSendButton: document.getElementById("threadSendButton"), threadMessageCount: document.getElementById("threadMessageCount"), threadAttachmentInput: document.getElementById("threadAttachmentInput"), threadAttachmentButton: document.getElementById("threadAttachmentButton"), threadAttachmentName: document.getElementById("threadAttachmentName"), pagesBox: document.getElementById("pagesBox"), pageDetailBox: document.getElementById("pageDetailBox"), settingsButton: document.getElementById("settingsButton"), profileBox: document.getElementById("profileBox"), headerMenuOverlay: document.getElementById("headerMenuOverlay"), headerMenuList: document.getElementById("headerMenuList"), reactionPopupOverlay: document.getElementById("reactionPopupOverlay"), reactionPopupGrid: document.getElementById("reactionPopupGrid"), reactionPopupList: document.getElementById("reactionPopupList"), reactionCustomInput: document.getElementById("reactionCustomInput"), reactionCustomButton: document.getElementById("reactionCustomButton"), closeReactionPopupButton: document.getElementById("closeReactionPopupButton"), mediaOverlay: document.getElementById("mediaOverlay"), mediaTitle: document.getElementById("mediaTitle"), mediaImage: document.getElementById("mediaImage"), mediaAudio: document.getElementById("mediaAudio"), mediaVideo: document.getElementById("mediaVideo"), mediaFileFallback: document.getElementById("mediaFileFallback"), mediaFallbackLink: document.getElementById("mediaFallbackLink"), mediaDownloadLink: document.getElementById("mediaDownloadLink"), closeMediaButton: document.getElementById("closeMediaButton"), callOverlay: document.getElementById("callOverlay"), callStatus: document.getElementById("callStatus"), callPeerLabel: document.getElementById("callPeerLabel"), callParticipants: document.getElementById("callParticipants"), callPendingInvites: document.getElementById("callPendingInvites"), remoteAudioContainer: document.getElementById("remoteAudioContainer"), callStatusChip: document.getElementById("callStatusChip"), startCallButton: document.getElementById("startCallButton"), retryCallButton: document.getElementById("retryCallButton"), addToCallButton: document.getElementById("addToCallButton"), muteCallButton: document.getElementById("muteCallButton"), hangupCallButton: document.getElementById("hangupCallButton"), endCallButton: document.getElementById("endCallButton"), closeCallPanelButton: document.getElementById("closeCallPanelButton"), dmLayoutToggle: document.getElementById("dmLayoutToggle"), emojiAutocomplete: document.getElementById("emojiAutocomplete"), threadEmojiAutocomplete: document.getElementById("threadEmojiAutocomplete"), settingsSavePanel: document.getElementById("settingsSavePanel"), settingsSaveButton: document.getElementById("settingsSaveButton"), settingsCloseButton: document.getElementById("settingsCloseButton"), settingsUnsavedIndicator: document.getElementById("settingsUnsavedIndicator"), avatarUploadInput: document.getElementById("avatarUploadInput"), bannerUploadInput: document.getElementById("bannerUploadInput"), imageCropOverlay: document.getElementById("imageCropOverlay"), imageCropPreview: document.getElementById("imageCropPreview"), imageCropZoom: document.getElementById("imageCropZoom"), imageCropAuto: document.getElementById("imageCropAuto"), imageCropApply: document.getElementById("imageCropApply"), imageCropCancel: document.getElementById("imageCropCancel"), adminTabRooms: document.getElementById("adminTabRooms"), adminTabUsers: document.getElementById("adminTabUsers"), adminTabTeams: document.getElementById("adminTabTeams"), adminRoomsPanel: document.getElementById("adminRoomsPanel"), adminUsersPanel: document.getElementById("adminUsersPanel"), adminTeamsPanel: document.getElementById("adminTeamsPanel"), callDragHandle: document.getElementById("callDragHandle"), callSummaryStats: document.getElementById("callSummaryStats"), callLatencyStat: document.getElementById("callLatencyStat"), callPacketLossStat: document.getElementById("callPacketLossStat"), callUptimeStat: document.getElementById("callUptimeStat"), callUserCountStat: document.getElementById("callUserCountStat"), callElapsedStat: document.getElementById("callElapsedStat") }; Glow.state = { currentUser: null, csrfToken: "", rooms: [], activeRoom: null, teams: [], activeTeam: null, channels: [], activeChannel: null, dms: [], activeDm: null, activeView: "feed", lastSeenId: 0, knownMessageIds: new Set(), renderedMessages: [], messageRequestId: 0, channelRequestId: 0, dmRequestId: 0, pollInFlight: false, channelPollInFlight: false, dmPollInFlight: false, feedLastSeenId: 0, feedKnownMessageIds: new Set(), feedEntries: [], feedRequestId: 0, feedPollInFlight: false, activeThread: null, threadReplies: [], threadRequestId: 0, threadPollInFlight: false, pages: [], activePage: null, pageTypeFilter: "", activeProfile: null, settings: null, settingsSaved: null, settingsDraft: null, settingsDirty: false, pollTimer: null, initialized: false, toastTimer: null, selectedAttachment: [], selectedThreadAttachment: [], typingTimer: null, typingLastSent: 0, typingUsers: [], dmTypingUsers: [], typingIndicatorsEnabled: localStorage.getItem("glow_typing_enabled") !== "false", notificationsEnabled: localStorage.getItem("glow_notifications_enabled") === "true", accentColor: localStorage.getItem("glow_accent_color") || "#f7dce8", unreadMessages: 0, originalTitle: document.title, dialogResolver: null, lastFocusedElement: null, dmLayoutMode: localStorage.getItem("glow_dm_layout_mode") === "compact" ? "compact" : "cards", profilePresenceBadgeVisible: localStorage.getItem("glow_profile_presence_badge_visible") !== "false", profileMediaDraft: { avatarFile: null, bannerFile: null }, imageCrop: { open: false, kind: "", file: null, previewUrl: "", zoom: 1, autoCrop: false }, emojiAutocomplete: { open: false, owner: "main", query: "", suggestions: [], selectedIndex: 0, triggerIndex: -1 }, adminTab: "rooms", presence: { roomOnlineCount: 0, channelOnlineCount: 0, dmOnlineCount: 0 }, calls: { activeCall: null, ws: null, session: null, localStream: null, muted: false, selfPeerId: null, peerConnections: new Map(), remoteStreams: new Map(), participantAudioEls: new Map(), peers: [], reconnectTimer: null, invitePollTimer: null, participantPollTimer: null, heartbeatTimer: null, invites: [], reconnecting: false, joining: false, leaving: false, permissions: { can_manage: false, can_end: false, can_invite: false }, backendParticipants: [], scope: null, statusText: "Idle", metrics: { packetLoss: null, uptime: null, latency: null, elapsedSeconds: 0, userCount: 0 }, drag: { active: false, x: null, y: null, offsetX: 0, offsetY: 0 } } }; Glow.MAX_MESSAGE_LENGTH = 1000; Glow.MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024; Glow.DEFAULT_ACCENT_COLOR = "#f7dce8"; Glow.normalizeAccentColor = value => { if (typeof value !== "string") return null; const color = value.trim(); if (!/^#[0-9a-fA-F]{6}$/.test(color)) return null; return color.toLowerCase(); }; Glow.normalizeNameColor = value => { if (value == null || value === "") return null; if (typeof value !== "string") return null; const color = value.trim(); if (!/^#[0-9a-fA-F]{6}$/.test(color)) return null; return color.toLowerCase(); }; Glow.hexToRgb = hex => { const normalized = Glow.normalizeAccentColor(hex); if (!normalized) return null; return { r: parseInt(normalized.slice(1, 3), 16), g: parseInt(normalized.slice(3, 5), 16), b: parseInt(normalized.slice(5, 7), 16) }; }; Glow.relativeLuminance = hex => { const rgb = Glow.hexToRgb(hex); if (!rgb) return 0; const channel = value => { const normalized = value / 255; return normalized <= 0.03928 ? normalized / 12.92 : Math.pow((normalized + 0.055) / 1.055, 2.4); }; return ( 0.2126 * channel(rgb.r) + 0.7152 * channel(rgb.g) + 0.0722 * channel(rgb.b) ); }; Glow.accentTextColor = hex => { return Glow.relativeLuminance(hex) >= 0.18 ? hex : "#f2f2f4"; }; Glow.applyAccentColor = value => { const accent = Glow.normalizeAccentColor(value) || Glow.DEFAULT_ACCENT_COLOR; const rgb = Glow.hexToRgb(accent); const root = document.documentElement; Glow.state.accentColor = accent; root.style.setProperty("--accent", accent); root.style.setProperty("--accent-active", Glow.accentTextColor(accent)); root.style.setProperty( "--accent-soft", `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.16)` ); root.style.setProperty( "--accent-hover", `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.28)` ); localStorage.setItem("glow_accent_color", accent); Glow.updateThemeButton?.(); }; Glow.resetAccentColor = () => { localStorage.removeItem("glow_accent_color"); Glow.applyAccentColor(Glow.DEFAULT_ACCENT_COLOR); }; Glow.loadDmLayoutPreference = () => { const stored = localStorage.getItem("glow_dm_layout_mode"); return stored === "compact" ? "compact" : "cards"; }; Glow.saveDmLayoutPreference = mode => { const normalized = mode === "compact" ? "compact" : "cards"; Glow.state.dmLayoutMode = normalized; localStorage.setItem("glow_dm_layout_mode", normalized); }; Glow.isSettingsDirty = () => Boolean(Glow.state.settingsDirty); Glow.markSettingsDirty = () => { Glow.state.settingsDirty = true; Glow.syncSettingsSavePanel?.(); }; Glow.clearSettingsDirty = () => { Glow.state.settingsDirty = false; Glow.syncSettingsSavePanel?.(); }; Glow.getTimestampVisibilityMode = () => { return Glow.state.activeDm ? "hover" : "always"; }; Glow.shouldShowMessageTimestamp = (_message = null, context = null) => { const view = context || Glow.getTimestampVisibilityMode(); return view !== "hover"; }; Glow.roleDefaultNameColor = role => { switch (String(role || "").toLowerCase()) { case "admin": return "#ff6b6b"; case "moderator": return "#7dd3fc"; default: return "#f5f5f7"; } }; Glow.resolveUserNameColor = userish => { const explicit = Glow.normalizeNameColor(userish?.name_color) || Glow.normalizeNameColor(userish?.user_name_color); if (explicit) return explicit; return Glow.roleDefaultNameColor(userish?.role || userish?.user_role); }; Glow.applyUserColor = (element, userish) => { if (!(element instanceof HTMLElement)) return; element.style.color = Glow.resolveUserNameColor(userish); }; Glow.canModerate = () => { const user = Glow.state.currentUser; return Boolean(user && ["moderator", "admin"].includes(user.role)); }; Glow.isAdmin = () => { const user = Glow.state.currentUser; return Boolean(user && user.role === "admin"); }; Glow.showToast = (message, type = "") => { clearTimeout(Glow.state.toastTimer); Glow.els.statusToast.textContent = String(message); Glow.els.statusToast.className = `status-toast show ${type}`.trim(); Glow.state.toastTimer = setTimeout(() => { Glow.els.statusToast.className = "status-toast"; }, 3500); }; Glow.showAuthNotice = (message, type = "error") => { Glow.els.authNotice.textContent = String(message); Glow.els.authNotice.className = `notice show ${type}`; }; Glow.clearAuthNotice = () => { Glow.els.authNotice.textContent = ""; Glow.els.authNotice.className = "notice"; }; Glow.showBlocked = message => { Glow.els.blockedMessage.textContent = message || "Access has been blocked."; Glow.els.blockedOverlay.classList.remove("hidden"); Glow.els.authOverlay.classList.add("hidden"); Glow.els.messageInput.disabled = true; Glow.els.sendButton.disabled = true; Glow.els.roomSelect.disabled = true; Glow.els.attachmentInput.disabled = true; Glow.els.attachmentButton.classList.add("disabled"); clearTimeout(Glow.state.pollTimer); }; Glow.relativeTime = iso => { const seconds = Math.max( 0, Math.floor((Date.now() - new Date(iso).getTime()) / 1000) ); if (seconds < 5) return "now"; if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h`; return `${Math.floor(hours / 24)}d`; }; Glow.formatDateLabel = iso => { const date = new Date(iso); const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const target = new Date(date.getFullYear(), date.getMonth(), date.getDate()); const diff = Math.round((today - target) / 86400000); if (diff === 0) return "Today"; if (diff === 1) return "Yesterday"; return date.toLocaleDateString(undefined, { weekday: "long", year: "numeric", month: "long", day: "numeric" }); }; Glow.dateKey = iso => { const date = new Date(iso); return [ date.getFullYear(), String(date.getMonth() + 1).padStart(2, "0"), String(date.getDate()).padStart(2, "0") ].join("-"); }; Glow.isNearBottom = () => { const activeBox = Glow.state.activeView === "feed" ? Glow.els.feedBox : Glow.state.activeView === "thread" ? Glow.els.threadBox : Glow.state.activeView === "dms" ? Glow.els.dmBox : Glow.els.chatBox; if (!activeBox) return true; return activeBox.scrollHeight - activeBox.scrollTop - activeBox.clientHeight < 100; }; Glow.autoResizeComposer = () => { const input = Glow.els.messageInput; input.style.height = "auto"; input.style.height = `${Math.min(input.scrollHeight, 150)}px`; }; Glow.updateMessageCount = () => { const count = Glow.els.messageInput.value.length; Glow.els.messageCount.textContent = `${count} / ${Glow.MAX_MESSAGE_LENGTH}`; }; Glow.updateDocumentTitle = () => { if (!Glow.state.unreadMessages) { document.title = Glow.state.originalTitle; return; } document.title = `(${Glow.state.unreadMessages})` + Glow.state.originalTitle; }; Glow.resetUnreadMessages = () => { Glow.state.unreadMessages = 0; Glow.updateDocumentTitle(); Glow.els.newMessagesBar.classList.add("hidden"); }; Glow.showNewMessages = () => { Glow.els.newMessagesBar.classList.remove("hidden"); }; Glow.normalizeSelectedFiles = fileList => { if (!fileList) return []; return [...fileList].filter(Boolean); }; Glow.websiteBrandMeta = value => { if (typeof value !== "string" || !value.trim()) { return { host: "", brand: "web", label: "website", iconText: "â—‹" }; } try { const url = new URL(value); const host = url.hostname.toLowerCase().replace(/^www\./, ""); const mappings = [ { match: /github\.com$/, brand: "github", label: "GitHub", iconText: "gh" }, { match: /youtube\.com$|youtu\.be$/, brand: "youtube", label: "YouTube", iconText: "yt" }, { match: /spotify\.com$/, brand: "spotify", label: "Spotify", iconText: "sp" }, { match: /soundcloud\.com$/, brand: "soundcloud", label: "SoundCloud", iconText: "sc" }, { match: /twitter\.com$|x\.com$/, brand: "x", label: "X", iconText: "x" } ]; for (const item of mappings) { if (item.match.test(host)) { return { host, brand: item.brand, label: item.label, iconText: item.iconText }; } } return { host, brand: "web", label: "website", iconText: "â—‹" }; } catch (_) { return { host: "", brand: "web", label: "website", iconText: "â—‹" }; } }; Glow.openDialog = ({ title, body, confirmText = "Confirm", cancelText = "Cancel", danger = false, fields = [] }) => { if (Glow.state.dialogResolver) { Glow.closeDialog(null); } Glow.state.lastFocusedElement = document.activeElement; Glow.els.dialogTitle.textContent = title; Glow.els.dialogBody.replaceChildren(); Glow.els.dialogActions.replaceChildren(); if (typeof body === "string") { const paragraph = document.createElement("p"); paragraph.textContent = body; Glow.els.dialogBody.append(paragraph); } else if (body instanceof Node) { Glow.els.dialogBody.append(body); } const fieldElements = {}; for (const field of fields) { const label = document.createElement("label"); label.className = "dialog-label"; label.textContent = field.label; const isMultiline = Boolean(field.multiline); const input = document.createElement(isMultiline ? "textarea" : "input"); input.className = "field"; input.name = field.name; input.value = field.value || ""; input.placeholder = field.placeholder || ""; if (isMultiline) { input.rows = field.rows || 3; input.maxLength = field.maxLength || 300; } else { input.type = field.type || "text"; input.maxLength = field.maxLength || 300; if (field.type === "number") { input.min = field.min || "1"; input.max = field.max || "43200"; input.step = field.step || "1"; } } label.append(input); Glow.els.dialogBody.append(label); fieldElements[field.name] = input; } const cancel = document.createElement("button"); cancel.className = "button secondary"; cancel.type = "button"; cancel.textContent = cancelText; const confirmButton = document.createElement("button"); confirmButton.className = danger ? "danger-button" : "button"; confirmButton.type = "button"; confirmButton.textContent = confirmText; Glow.els.dialogActions.append(cancel, confirmButton); Glow.els.dialogOverlay.classList.remove("hidden"); return new Promise(resolve => { Glow.state.dialogResolver = resolve; cancel.addEventListener("click", () => Glow.closeDialog(null), { once: true }); confirmButton.addEventListener( "click", () => { const values = {}; for (const [name, input] of Object.entries(fieldElements)) { values[name] = input.value.trim(); } Glow.closeDialog(values); }, { once: true } ); setTimeout(() => { const first = Object.values(fieldElements)[0] || confirmButton; first.focus(); }, 0); }); }; Glow.closeDialog = (result = null) => { Glow.els.dialogOverlay.classList.add("hidden"); const resolve = Glow.state.dialogResolver; Glow.state.dialogResolver = null; if (resolve) resolve(result); if (Glow.state.lastFocusedElement instanceof HTMLElement) { Glow.state.lastFocusedElement.focus(); } };