diff --git a/static/js/00-config.js b/static/js/00-config.js index 83dfd7d..f3c7219 100644 --- a/static/js/00-config.js +++ b/static/js/00-config.js @@ -1,6 +1,8 @@ // static/js/00-config.js "use strict"; + window.Glow = window.Glow || {}; + Glow.els = { chatBox: document.getElementById("chatBox"), dmBox: document.getElementById("dmBox"), @@ -85,15 +87,19 @@ Glow.els = { 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"), - answerCallButton: document.getElementById("answerCallButton"), + retryCallButton: document.getElementById("retryCallButton"), addToCallButton: document.getElementById("addToCallButton"), muteCallButton: document.getElementById("muteCallButton"), hangupCallButton: document.getElementById("hangupCallButton"), - closeCallPanelButton: document.getElementById("closeCallPanelButton"), - remoteAudio: document.getElementById("remoteAudio"), - emojiAutocomplete: document.getElementById("emojiAutocomplete") + endCallButton: document.getElementById("endCallButton"), + closeCallPanelButton: document.getElementById("closeCallPanelButton") }; + Glow.state = { currentUser: null, csrfToken: "", @@ -148,8 +154,6 @@ Glow.state = { originalTitle: document.title, dialogResolver: null, lastFocusedElement: null, - emojiSuggestions: [], - emojiActiveInput: null, presence: { roomOnlineCount: 0, channelOnlineCount: 0, @@ -164,40 +168,38 @@ Glow.state = { selfPeerId: null, peerConnections: new Map(), remoteStreams: new Map(), + participantAudioEls: new Map(), peers: [], reconnectTimer: null, + invitePollTimer: null, + participantPollTimer: null, + heartbeatTimer: null, invites: [], - invitePollTimer: null + reconnecting: false, + joining: false, + leaving: false, + permissions: { + can_manage: false, + can_end: false, + can_invite: false + }, + backendParticipants: [], + scope: null, + statusText: "Idle" } }; + Glow.MAX_MESSAGE_LENGTH = 1000; Glow.MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024; Glow.DEFAULT_ACCENT_COLOR = "#f7dce8"; -Glow.EMOJI_ALIASES = { - smile: "😄", - grin: "😁", - joy: "😂", - heart: "❤️", - thumbs_up: "👍", - fire: "🔥", - tada: "🎉", - clap: "👏", - eyes: "👀", - rocket: "🚀", - wave: "👋", - sparkles: "✨", - thinking: "🤔", - cry: "😢", - check: "✅", - bulb: "💡", - shrug: "¯\\_(ツ)_/¯" -}; + 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; @@ -205,6 +207,7 @@ Glow.normalizeNameColor = value => { 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; @@ -214,6 +217,7 @@ Glow.hexToRgb = hex => { b: parseInt(normalized.slice(5, 7), 16) }; }; + Glow.relativeLuminance = hex => { const rgb = Glow.hexToRgb(hex); if (!rgb) return 0; @@ -229,9 +233,11 @@ Glow.relativeLuminance = hex => { 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; @@ -251,10 +257,12 @@ Glow.applyAccentColor = value => { localStorage.setItem("glow_accent_color", accent); Glow.updateThemeButton?.(); }; + Glow.resetAccentColor = () => { localStorage.removeItem("glow_accent_color"); Glow.applyAccentColor(Glow.DEFAULT_ACCENT_COLOR); }; + Glow.roleDefaultNameColor = role => { switch (String(role || "").toLowerCase()) { case "admin": @@ -265,6 +273,7 @@ Glow.roleDefaultNameColor = role => { return "#f5f5f7"; } }; + Glow.resolveUserNameColor = userish => { const explicit = Glow.normalizeNameColor(userish?.name_color) || @@ -272,18 +281,22 @@ Glow.resolveUserNameColor = userish => { 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); @@ -292,14 +305,17 @@ Glow.showToast = (message, type = "") => { 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"); @@ -311,6 +327,7 @@ Glow.showBlocked = message => { Glow.els.attachmentButton.classList.add("disabled"); clearTimeout(Glow.state.pollTimer); }; + Glow.relativeTime = iso => { const seconds = Math.max( 0, @@ -324,6 +341,7 @@ Glow.relativeTime = iso => { if (hours < 24) return `${hours}h`; return `${Math.floor(hours / 24)}d`; }; + Glow.formatDateLabel = iso => { const date = new Date(iso); const now = new Date(); @@ -339,6 +357,7 @@ Glow.formatDateLabel = iso => { day: "numeric" }); }; + Glow.dateKey = iso => { const date = new Date(iso); return [ @@ -347,6 +366,7 @@ Glow.dateKey = iso => { String(date.getDate()).padStart(2, "0") ].join("-"); }; + Glow.isNearBottom = () => { const activeBox = Glow.state.activeView === "feed" @@ -359,34 +379,41 @@ Glow.isNearBottom = () => { 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}`; + 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.openDialog = ({ title, body, @@ -398,10 +425,12 @@ Glow.openDialog = ({ 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; @@ -409,17 +438,20 @@ Glow.openDialog = ({ } 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; @@ -432,20 +464,26 @@ Glow.openDialog = ({ 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 }); @@ -466,6 +504,7 @@ Glow.openDialog = ({ }, 0); }); }; + Glow.closeDialog = (result = null) => { Glow.els.dialogOverlay.classList.add("hidden"); const resolve = Glow.state.dialogResolver;