glo/static/js/04-main.js
2026-11-02 06:10:25 +00:00

1100 lines
31 KiB
JavaScript

// static/js/04-main.js
"use strict";
window.Glow = window.Glow || {};
Glow.openCallPanel = async () => {
Glow.calls.ensureState();
Glow.state.calls.panelOpen = true;
const overlay = document.getElementById("callOverlay");
const participants = document.getElementById("callParticipants");
overlay?.classList.remove("hidden");
participants?.classList.remove("hidden");
Glow.restoreCallPanelPosition?.();
if (Glow.state.calls.activeCall?.groupSession?.id) {
await Glow.calls.refreshBackendParticipants();
}
Glow.renderCallUi();
};
Glow.calls.renderHeaderCallChip = () => {
if (!Glow.els.callStatusChip) return;
Glow.calls.ensureState();
const state = Glow.calls.state();
Glow.els.callStatusChip.classList.remove("hidden");
if (state.reconnecting) {
Glow.els.callStatusChip.textContent = "Call reconnecting";
} else if (state.joining) {
Glow.els.callStatusChip.textContent = "Call joining";
} else if (state.activeCall) {
Glow.els.callStatusChip.textContent = "Call active";
} else {
Glow.els.callStatusChip.textContent = "Call";
}
};
Glow.loadRooms = async () => {
const previousSlug = Glow.state.activeRoom?.slug;
const data = await Glow.apiRequest("/api/rooms");
Glow.state.rooms = data.rooms || [];
Glow.els.roomSelect.replaceChildren();
for (const room of Glow.state.rooms) {
const option = document.createElement("option");
option.value = room.slug;
option.textContent =
`${room.name}${room.visibility === "private" ? " (private)" : ""}`;
Glow.els.roomSelect.append(option);
}
Glow.els.roomSelect.disabled = !Glow.state.rooms.length;
const selected =
Glow.state.rooms.find(room => room.slug === previousSlug) ||
Glow.state.rooms.find(room => room.slug === "general") ||
Glow.state.rooms[0] ||
null;
Glow.state.activeRoom = selected;
Glow.updateViewUI();
Glow.updateChatControls();
};
Glow.restoreCallPanelPosition = () => {
const overlay = Glow.els.callOverlay;
if (!overlay) return;
const savedX = localStorage.getItem("glow_call_panel_x");
const savedY = localStorage.getItem("glow_call_panel_y");
if (savedX == null || savedY == null) return;
const x = Number(savedX);
const y = Number(savedY);
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
Glow.state.calls.drag.x = x;
Glow.state.calls.drag.y = y;
overlay.style.left = `${x}px`;
overlay.style.top = `${y}px`;
overlay.style.right = "auto";
overlay.style.bottom = "auto";
overlay.style.alignItems = "flex-start";
overlay.style.justifyContent = "flex-start";
};
Glow.persistCallPanelPosition = () => {
const drag = Glow.state.calls?.drag;
if (!drag) return;
if (!Number.isFinite(drag.x) || !Number.isFinite(drag.y)) return;
localStorage.setItem("glow_call_panel_x", String(drag.x));
localStorage.setItem("glow_call_panel_y", String(drag.y));
};
Glow.confirmDirtySettingsBeforeNavigate = async action => {
if (Glow.state.activeView !== "settings" || !Glow.isSettingsDirty()) {
await action();
return true;
}
Glow.syncSettingsSavePanel?.();
const values = await Glow.openDialog({
title: "Unsaved settings changes",
body: "You have unsaved changes. Would you like to save before leaving this page?",
confirmText: "Save and leave",
cancelText: "Stay"
});
if (values === null) {
const discard = await Glow.openDialog({
title: "Discard changes?",
body: "If you leave now, your unsaved settings changes will be lost.",
confirmText: "Discard changes",
cancelText: "Stay",
danger: true
});
if (!discard) {
return false;
}
Glow.discardSettingsDraft?.();
await action();
return true;
}
try {
await Glow.saveSettingsDraft?.();
await action();
return true;
} catch (error) {
Glow.showToast(error.message || "Could not save settings.", "error");
return false;
}
};
Glow.navigateToView = async (view, options = {}) => {
return Glow.confirmDirtySettingsBeforeNavigate(async () => {
if (typeof options.before === "function") {
await options.before();
}
Glow.state.activeView = view;
Glow.updateViewUI();
if (typeof options.after === "function") {
await options.after();
}
if (options.schedulePoll !== false) {
Glow.schedulePoll?.();
}
});
};
Glow.updateViewUI = () => {
const view = Glow.state.activeView || "feed";
const room = Glow.state.activeRoom;
const dm = Glow.state.activeDm;
const feedActive = view === "feed";
const dmListActive = view === "dms";
const chatActive = view === "room" || view === "channel" || view === "dm";
const threadActive = view === "thread";
const pagesActive = view === "pages";
const pageActive = view === "page";
const profileActive = view === "profile";
const settingsActive = view === "settings";
Glow.els.feedBox.classList.toggle("hidden", !feedActive);
Glow.els.dmBox?.classList.toggle("hidden", !dmListActive);
Glow.els.chatBox.classList.toggle("hidden", !chatActive);
Glow.els.threadBox?.classList.toggle("hidden", !threadActive);
Glow.els.pagesBox?.classList.toggle("hidden", !pagesActive);
Glow.els.pageDetailBox?.classList.toggle("hidden", !pageActive);
Glow.els.profileBox?.classList.toggle("hidden", !(profileActive || settingsActive));
Glow.els.composerWrap.classList.toggle("hidden", !chatActive);
Glow.els.threadComposerWrap?.classList.toggle("hidden", !threadActive);
Glow.syncSettingsSavePanel?.();
if (!chatActive) {
Glow.state.typingUsers = [];
Glow.state.dmTypingUsers = [];
if (Glow.els.typingBar) {
Glow.els.typingBar.textContent = "";
Glow.els.typingBar.classList.add("hidden");
}
}
Glow.els.feedButton.classList.toggle("active", feedActive);
Glow.els.feedButton.setAttribute("aria-pressed", String(feedActive));
Glow.els.pagesButton?.classList.toggle("active", pagesActive || pageActive);
Glow.els.pagesButton?.setAttribute("aria-pressed", String(pagesActive || pageActive));
Glow.els.dmsButton?.classList.toggle("active", dmListActive || view === "dm");
Glow.els.dmsButton?.setAttribute("aria-pressed", String(dmListActive || view === "dm"));
Glow.els.backButton?.classList.toggle(
"hidden",
!(threadActive || pageActive || profileActive || settingsActive || view === "dm")
);
Glow.els.roomSelect.classList.toggle("active", chatActive && !dm);
Glow.els.teamSelect?.classList.toggle("hidden", dmListActive || view === "dm");
Glow.els.roomSelect?.classList.toggle("hidden", dmListActive || view === "dm");
if (feedActive) {
Glow.els.headerTitle.textContent = "Feed";
Glow.els.headerSubtitle.textContent = Glow.state.currentUser
? "Recent activity across your accessible rooms"
: "Sign in or create an account to personalize your feed";
Glow.els.roomSelect.disabled = !Glow.state.rooms.length;
Glow.els.presenceLabel?.classList.add("hidden");
Glow.calls?.renderHeaderCallChip?.();
return;
}
if (dmListActive) {
Glow.els.headerTitle.textContent = "Direct messages";
Glow.els.headerSubtitle.textContent = Glow.state.currentUser
? "Private conversations"
: "Sign in to open private conversations";
Glow.els.presenceLabel?.classList.add("hidden");
Glow.calls?.renderHeaderCallChip?.();
return;
}
if (threadActive) {
Glow.els.headerTitle.textContent = "Thread";
Glow.els.headerSubtitle.textContent =
dm ? "Direct message replies" : room?.name || "Conversation replies";
Glow.updateThreadControls?.();
Glow.calls?.renderHeaderCallChip?.();
return;
}
if (pagesActive) {
Glow.els.headerTitle.textContent = "Pages";
Glow.els.headerSubtitle.textContent = "Shared knowledge and resources";
Glow.els.presenceLabel?.classList.add("hidden");
Glow.calls?.renderHeaderCallChip?.();
return;
}
if (pageActive) {
Glow.els.headerTitle.textContent = Glow.state.activePage?.title || "Page";
Glow.els.headerSubtitle.textContent = "Shared knowledge and resources";
Glow.els.presenceLabel?.classList.add("hidden");
Glow.calls?.renderHeaderCallChip?.();
return;
}
if (profileActive) {
Glow.els.headerTitle.textContent =
Glow.state.activeProfile?.display_name ||
Glow.state.activeProfile?.username ||
"Profile";
Glow.els.headerSubtitle.textContent = "User profile";
Glow.els.presenceLabel?.classList.add("hidden");
Glow.calls?.renderHeaderCallChip?.();
return;
}
if (settingsActive) {
Glow.els.headerTitle.textContent = "Settings";
Glow.els.headerSubtitle.textContent = "Appearance, profile, and preferences";
Glow.els.presenceLabel?.classList.add("hidden");
Glow.calls?.renderHeaderCallChip?.();
return;
}
if (view === "dm" && dm) {
const peer =
(dm.members || []).find(
member => Number(member.id) !== Number(Glow.state.currentUser?.id)
) || dm.members?.[0];
Glow.els.headerTitle.textContent =
peer?.display_name || peer?.username || "Direct message";
Glow.els.headerSubtitle.textContent =
peer?.username ? `@${peer.username}` : "Private conversation";
Glow.refreshPresence?.();
Glow.updateChatControls();
Glow.calls?.renderHeaderCallChip?.();
return;
}
if (room) {
Glow.els.headerTitle.textContent = room.name;
Glow.els.headerSubtitle.textContent =
room.description ||
(room.visibility === "private" ? "private room" : "public room");
Glow.els.roomSelect.value = room.slug;
} else {
Glow.els.headerTitle.textContent = "No rooms";
Glow.els.headerSubtitle.textContent = "No accessible chat rooms";
}
Glow.updateChatControls();
Glow.calls?.renderHeaderCallChip?.();
};
Glow.updateAuthUI = () => {
const user = Glow.state.currentUser;
const authenticated = Boolean(user);
Glow.els.authOverlay.classList.toggle("hidden", authenticated);
Glow.els.logoutButton.classList.toggle("hidden", !authenticated);
Glow.els.headerAccount.textContent =
user ? `${user.username} · ${user.role}` : "not signed in";
document.body.classList.toggle("is-moderator", Glow.canModerate());
document.body.classList.toggle("is-admin", Glow.isAdmin());
Glow.els.adminFab.classList.toggle("visible", Glow.canModerate());
Glow.els.presenceDot.classList.toggle("online", authenticated);
Glow.els.presenceDot.classList.toggle("new", Boolean(user?.status === "restricted"));
if (user?.status === "restricted") {
Glow.showToast("Your account is temporarily restricted.", "error");
}
Glow.updateViewUI();
Glow.updateChatControls();
Glow.updateThreadControls?.();
Glow.renderHeaderMenu?.();
};
Glow.checkInvite = async () => {
const params = new URLSearchParams(window.location.search);
const roomToken = params.get("invite");
const teamToken = params.get("team_invite");
if (roomToken) {
try {
const data = await Glow.apiRequest(
`/api/invites/${encodeURIComponent(roomToken)}/accept`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({})
}
);
Glow.showToast(`Joined private room: ${data.room.name}`);
window.history.replaceState({}, "", window.location.pathname);
} catch (error) {
if (error.status !== 401) {
Glow.showToast(error.message, "error");
window.history.replaceState({}, "", window.location.pathname);
}
}
}
if (teamToken) {
try {
const data = await Glow.apiAcceptTeamInvite(teamToken);
Glow.showToast(`Joined team: ${data.team.name}`);
window.history.replaceState({}, "", window.location.pathname);
} catch (error) {
if (error.status !== 401) {
Glow.showToast(error.message, "error");
window.history.replaceState({}, "", window.location.pathname);
}
}
}
};
Glow.checkAuth = async () => {
try {
const data = await Glow.apiRequest("/api/me");
Glow.state.currentUser = data.authenticated ? data.user : null;
Glow.state.csrfToken = data.csrf_token || "";
Glow.updateAuthUI();
await Glow.checkInvite();
await Glow.loadRooms();
await Glow.loadDms?.().catch(() => {});
Glow.state.activeView = "feed";
Glow.updateViewUI();
await Glow.loadFeed();
await Glow.loadMySettings?.().catch(() => {});
Glow.calls?.scheduleInvitePolling?.();
} catch (error) {
Glow.showToast(error.message || "Could not connect.", "error");
} finally {
Glow.schedulePoll();
}
};
Glow.authenticate = async endpoint => {
Glow.clearAuthNotice();
const accessKey = Glow.els.accessKeyInput.value;
const username = Glow.els.usernameInput.value.trim().toLowerCase();
const password = Glow.els.passwordInput.value;
if (!accessKey || !username || !password) {
Glow.showAuthNotice("Enter the private access key, username, and password.");
return;
}
Glow.els.loginButton.disabled = true;
Glow.els.registerButton.disabled = true;
try {
const data = await Glow.apiRequest(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ access_key: accessKey, username, password })
});
Glow.state.currentUser = data.user;
Glow.state.csrfToken = data.csrf_token || "";
Glow.els.passwordInput.value = "";
Glow.updateAuthUI();
await Glow.checkInvite();
await Glow.loadRooms();
await Glow.loadDms?.().catch(() => {});
Glow.state.activeView = "feed";
Glow.updateViewUI();
await Glow.loadFeed();
await Glow.loadMySettings?.().catch(() => {});
Glow.calls?.scheduleInvitePolling?.();
Glow.showToast(
endpoint.endsWith("register") ? "Account created." : "Signed in."
);
} catch (error) {
Glow.showAuthNotice(
error.status === 429 && error.retryAfter
? `Please wait ${error.retryAfter} seconds before trying again.`
: error.message
);
} finally {
Glow.els.loginButton.disabled = false;
Glow.els.registerButton.disabled = false;
Glow.schedulePoll();
}
};
Glow.logout = async () => {
if (Glow.state.calls.activeCall?.groupSession?.id) {
const values = await Glow.openDialog({
title: "Leave active call and sign out?",
body: "You are currently in a voice call. Signing out will disconnect you from the call.",
confirmText: "Leave and sign out",
cancelText: "Stay signed in",
danger: true
});
if (!values) return;
}
try {
await Glow.apiRequest("/api/logout", { method: "POST" });
} catch (_) {}
if (Glow.state.calls.activeCall?.groupSession?.id) {
try {
await Glow.apiLeaveGroupCall(Glow.state.calls.activeCall.groupSession.id);
} catch (_) {}
}
Glow.state.currentUser = null;
Glow.state.csrfToken = "";
Glow.state.rooms = [];
Glow.state.activeRoom = null;
Glow.state.teams = [];
Glow.state.activeTeam = null;
Glow.state.channels = [];
Glow.state.activeChannel = null;
Glow.state.dms = [];
Glow.state.activeDm = null;
Glow.state.activeView = "feed";
Glow.state.activeThread = null;
Glow.state.threadReplies = [];
Glow.state.activePage = null;
Glow.state.activeProfile = null;
Glow.state.settings = null;
Glow.state.settingsSaved = null;
Glow.state.settingsDraft = null;
Glow.state.settingsDirty = false;
Glow.clearMessages();
Glow.clearFeed();
Glow.clearDms();
Glow.closeHeaderMenu?.();
Glow.closeReactionPopup?.();
Glow.closeMediaModal?.();
Glow.endLocalCallUi?.();
clearTimeout(Glow.state.calls?.invitePollTimer);
clearTimeout(Glow.state.calls?.participantPollTimer);
clearTimeout(Glow.state.calls?.heartbeatTimer);
Glow.updateAuthUI();
Glow.els.authOverlay.classList.remove("hidden");
Glow.els.accessKeyInput.focus();
await Glow.refreshCsrfToken().catch(() => {});
await Glow.loadFeed().catch(() => {});
Glow.showToast("Signed out.");
};
Glow.toggleNotifications = async () => {
if (!("Notification" in window)) {
Glow.showToast("Browser notifications are not supported.", "error");
return;
}
if (!Glow.state.notificationsEnabled) {
const permission = await Notification.requestPermission();
if (permission !== "granted") {
Glow.showToast("Notification permission was not granted.", "error");
return;
}
}
Glow.state.notificationsEnabled = !Glow.state.notificationsEnabled;
localStorage.setItem(
"glow_notifications_enabled",
String(Glow.state.notificationsEnabled)
);
Glow.updateAdminPreferenceButtons?.();
Glow.showToast(
Glow.state.notificationsEnabled
? "Browser notifications enabled."
: "Browser notifications disabled."
);
};
Glow.toggleTypingIndicators = () => {
Glow.state.typingIndicatorsEnabled = !Glow.state.typingIndicatorsEnabled;
localStorage.setItem(
"glow_typing_enabled",
String(Glow.state.typingIndicatorsEnabled)
);
if (!Glow.state.typingIndicatorsEnabled) {
Glow.state.typingUsers = [];
Glow.state.dmTypingUsers = [];
Glow.renderTyping();
}
Glow.updateAdminPreferenceButtons?.();
Glow.showToast(
Glow.state.typingIndicatorsEnabled
? "Typing indicators enabled."
: "Typing indicators disabled."
);
};
Glow.pingPresence = async () => {
if (!Glow.state.currentUser) return;
const payload = {
view: Glow.state.activeView
};
if (Glow.state.activeDm?.id) {
payload.dm_id = Glow.state.activeDm.id;
} else if (Glow.state.activeChannel?.id) {
payload.channel_id = Glow.state.activeChannel.id;
} else if (Glow.state.activeRoom?.id) {
payload.room_id = Glow.state.activeRoom.id;
}
try {
await Glow.apiPingPresence(payload);
} catch (_) {}
};
Glow.refreshPresence = async () => {
try {
if (Glow.state.activeDm?.id) {
const data = await Glow.apiGetDmPresence(Glow.state.activeDm.id);
Glow.state.presence.dmOnlineCount = Number(data.online_count) || 0;
Glow.els.presenceLabel.textContent = `${Glow.state.presence.dmOnlineCount} online`;
Glow.els.presenceLabel.classList.remove("hidden");
return;
}
if (Glow.state.activeTeam?.slug && Glow.state.activeChannel?.slug) {
const data = await Glow.apiGetChannelPresence(
Glow.state.activeTeam.slug,
Glow.state.activeChannel.slug
);
Glow.state.presence.channelOnlineCount = Number(data.online_count) || 0;
Glow.els.presenceLabel.textContent = `${Glow.state.presence.channelOnlineCount} online`;
Glow.els.presenceLabel.classList.remove("hidden");
return;
}
if (Glow.state.activeRoom?.slug) {
const data = await Glow.apiGetRoomPresence(Glow.state.activeRoom.slug);
Glow.state.presence.roomOnlineCount = Number(data.online_count) || 0;
Glow.els.presenceLabel.textContent = `${Glow.state.presence.roomOnlineCount} online`;
Glow.els.presenceLabel.classList.remove("hidden");
return;
}
Glow.els.presenceLabel.classList.add("hidden");
} catch (_) {
Glow.els.presenceLabel?.classList.add("hidden");
}
};
Glow.schedulePoll = () => {
clearTimeout(Glow.state.pollTimer);
if (Glow.els.blockedOverlay.classList.contains("hidden") === false) return;
Glow.state.pollTimer = setTimeout(async () => {
if (Glow.state.activeView === "feed") {
await Glow.pollFeed();
} else if (
Glow.state.activeView === "room" ||
Glow.state.activeView === "channel" ||
Glow.state.activeView === "dm"
) {
await Glow.pollMessages();
} else if (Glow.state.activeView === "thread") {
await Glow.pollThread?.();
} else if (Glow.state.activeView === "dms") {
await Glow.loadDms?.().catch(() => {});
}
await Glow.pingPresence?.();
await Glow.refreshPresence?.();
Glow.schedulePoll();
}, document.hidden ? 12000 : 5000);
};
Glow.els.chatForm.addEventListener("submit", Glow.sendMessage);
Glow.els.threadForm?.addEventListener("submit", event => Glow.sendThreadReply?.(event));
Glow.els.messageInput.addEventListener("input", () => {
Glow.autoResizeComposer();
Glow.updateMessageCount();
clearTimeout(Glow.state.typingTimer);
if (Glow.els.messageInput.value.trim()) {
Glow.sendTyping(true);
Glow.state.typingTimer = setTimeout(() => {
Glow.sendTyping(false);
}, 2500);
} else {
Glow.sendTyping(false);
}
});
Glow.els.threadInput?.addEventListener("input", () => {
Glow.autoResizeThreadComposer?.();
Glow.updateThreadMessageCount?.();
});
Glow.els.messageInput.addEventListener("keydown", event => {
if (Glow.state.emojiAutocomplete?.open) {
if (event.key === "Enter" || event.key === "Tab") {
if (typeof Glow.applyEmojiSuggestion === "function") {
event.preventDefault();
Glow.applyEmojiSuggestion();
return;
}
}
if (event.key === "Escape") {
if (typeof Glow.closeEmojiAutocomplete === "function") {
event.preventDefault();
Glow.closeEmojiAutocomplete();
return;
}
}
}
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
Glow.sendMessage(event);
}
});
Glow.els.threadInput?.addEventListener("keydown", event => {
if (Glow.state.emojiAutocomplete?.open && Glow.state.emojiAutocomplete.owner === "thread") {
if (event.key === "Enter" || event.key === "Tab") {
if (typeof Glow.applyEmojiSuggestion === "function") {
event.preventDefault();
Glow.applyEmojiSuggestion();
return;
}
}
if (event.key === "Escape") {
if (typeof Glow.closeEmojiAutocomplete === "function") {
event.preventDefault();
Glow.closeEmojiAutocomplete();
return;
}
}
}
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
Glow.sendThreadReply?.(event);
}
});
Glow.els.attachmentInput.addEventListener("change", () => {
const files = Glow.normalizeSelectedFiles(Glow.els.attachmentInput.files);
if (!files.length) {
Glow.setSelectedAttachment([]);
return;
}
const invalid = files.find(file => file.size > Glow.MAX_ATTACHMENT_BYTES);
if (invalid) {
Glow.showToast("Attachments must be 5 MB or smaller.", "error");
Glow.els.attachmentInput.value = "";
Glow.setSelectedAttachment([]);
return;
}
Glow.setSelectedAttachment(files);
});
Glow.els.threadAttachmentInput?.addEventListener("change", () => {
const files = Glow.normalizeSelectedFiles(Glow.els.threadAttachmentInput.files);
if (!files.length) {
Glow.setSelectedThreadAttachment?.([]);
return;
}
const invalid = files.find(file => file.size > Glow.MAX_ATTACHMENT_BYTES);
if (invalid) {
Glow.showToast("Attachments must be 5 MB or smaller.", "error");
Glow.els.threadAttachmentInput.value = "";
Glow.setSelectedThreadAttachment?.([]);
return;
}
Glow.setSelectedThreadAttachment?.(files);
});
Glow.els.feedButton.addEventListener("click", async () => {
await Glow.navigateToView("feed", {
before: async () => {
Glow.state.activeDm = null;
},
after: async () => {
await Glow.loadFeed();
}
});
});
Glow.els.dmsButton?.addEventListener("click", async () => {
await Glow.navigateToView("dms", {
after: async () => {
await Glow.loadDms?.();
}
});
});
Glow.els.pagesButton?.addEventListener("click", async () => {
await Glow.confirmDirtySettingsBeforeNavigate(async () => {
Glow.openPages?.();
});
});
Glow.els.backButton?.addEventListener("click", async () => {
await Glow.confirmDirtySettingsBeforeNavigate(async () => {
if (Glow.state.activeView === "thread") {
await Glow.returnToChannel?.();
} else if (Glow.state.activeView === "page") {
await Glow.openPages?.();
} else if (Glow.state.activeView === "profile" || Glow.state.activeView === "settings") {
await Glow.openFeed?.();
} else if (Glow.state.activeView === "dm") {
Glow.state.activeView = "dms";
Glow.updateViewUI();
await Glow.loadDms?.();
}
});
});
Glow.els.loginButton.addEventListener("click", () => {
Glow.authenticate("/api/login");
});
Glow.els.registerButton.addEventListener("click", () => {
Glow.authenticate("/api/register");
});
Glow.els.passwordInput.addEventListener("keydown", event => {
if (event.key === "Enter") {
event.preventDefault();
Glow.authenticate("/api/login");
}
});
Glow.els.logoutButton.addEventListener("click", Glow.logout);
Glow.els.roomSelect.addEventListener("change", async () => {
const selected = Glow.state.rooms.find(
room => room.slug === Glow.els.roomSelect.value
);
if (!selected) return;
await Glow.confirmDirtySettingsBeforeNavigate(async () => {
Glow.state.activeDm = null;
Glow.state.activeRoom = selected;
Glow.state.activeView = "room";
Glow.state.typingUsers = [];
Glow.state.dmTypingUsers = [];
Glow.renderTyping();
Glow.updateViewUI();
await Glow.loadMessages();
Glow.pollTyping();
Glow.pingPresence?.();
Glow.refreshPresence?.();
Glow.schedulePoll();
});
});
Glow.els.teamSelect?.addEventListener("change", () => {
const teams = Glow.state.teams || [];
Glow.state.activeTeam =
teams.find(team => team.slug === Glow.els.teamSelect.value) || null;
});
Glow.els.adminFab.addEventListener("click", Glow.openAdminPanel);
Glow.els.closeAdminButton.addEventListener("click", Glow.closeAdminPanel);
Glow.els.createRoomButton.addEventListener("click", Glow.createRoom);
Glow.els.clearMessagesButton.addEventListener("click", Glow.clearActiveRoom);
Glow.els.typingToggleButton?.addEventListener("click", Glow.toggleTypingIndicators);
Glow.els.notificationsToggleButton?.addEventListener("click", Glow.toggleNotifications);
Glow.els.themeButton?.addEventListener("click", Glow.openAccentColorPicker);
Glow.els.settingsButton?.addEventListener("click", () => {
Glow.toggleHeaderMenu?.();
});
document.getElementById("callStatusChip")?.addEventListener("click", async () => {
await Glow.openCallPanel?.();
});
Glow.els.jumpToLatestButton.addEventListener("click", () => {
Glow.els.chatBox.scrollTop = Glow.els.chatBox.scrollHeight;
Glow.resetUnreadMessages();
});
Glow.els.chatBox.addEventListener("scroll", () => {
if (Glow.isNearBottom()) {
Glow.resetUnreadMessages();
}
});
Glow.els.adminPanel.addEventListener("click", event => {
if (event.target === Glow.els.adminPanel) {
Glow.closeAdminPanel();
}
});
Glow.els.dialogOverlay.addEventListener("click", event => {
if (event.target === Glow.els.dialogOverlay) {
Glow.closeDialog(null);
}
});
Glow.els.headerMenuOverlay?.addEventListener("click", event => {
if (event.target === Glow.els.headerMenuOverlay) {
Glow.closeHeaderMenu?.();
}
});
Glow.els.closeReactionPopupButton?.addEventListener("click", () => {
Glow.closeReactionPopup?.();
});
Glow.els.reactionPopupOverlay?.addEventListener("click", event => {
if (event.target === Glow.els.reactionPopupOverlay) {
Glow.closeReactionPopup?.();
}
});
Glow.els.closeMediaButton?.addEventListener("click", () => {
Glow.closeMediaModal?.();
});
Glow.els.mediaOverlay?.addEventListener("click", event => {
if (event.target === Glow.els.mediaOverlay) {
Glow.closeMediaModal?.();
}
});
document.getElementById("closeCallPanelButton")?.addEventListener("click", () => {
Glow.calls.ensureState();
Glow.state.calls.panelOpen = false;
document.getElementById("callOverlay")?.classList.add("hidden");
Glow.persistCallPanelPosition?.();
});
document.addEventListener("keydown", async event => {
if (event.key !== "Escape") return;
if (!Glow.els.dialogOverlay.classList.contains("hidden")) {
Glow.closeDialog(null);
return;
}
if (!Glow.els.imageCropOverlay?.classList.contains("hidden")) {
Glow.profileResetCropState?.();
return;
}
if (!Glow.els.mediaOverlay?.classList.contains("hidden")) {
Glow.closeMediaModal?.();
return;
}
if (!Glow.els.reactionPopupOverlay?.classList.contains("hidden")) {
Glow.closeReactionPopup?.();
return;
}
if (!Glow.els.headerMenuOverlay?.classList.contains("hidden")) {
Glow.closeHeaderMenu?.();
return;
}
if (!Glow.els.callOverlay?.classList.contains("hidden")) {
Glow.calls.ensureState();
Glow.state.calls.panelOpen = false;
Glow.els.callOverlay.classList.add("hidden");
Glow.persistCallPanelPosition?.();
return;
}
if (Glow.state.activeView === "thread") {
await Glow.confirmDirtySettingsBeforeNavigate(async () => {
Glow.returnToChannel?.();
});
return;
}
if (Glow.state.activeView === "page") {
await Glow.confirmDirtySettingsBeforeNavigate(async () => {
Glow.openPages?.();
});
return;
}
if (Glow.state.activeView === "dm") {
await Glow.confirmDirtySettingsBeforeNavigate(async () => {
Glow.state.activeView = "dms";
Glow.updateViewUI();
Glow.loadDms?.();
});
return;
}
if (Glow.els.adminPanel.classList.contains("open")) {
Glow.closeAdminPanel();
}
});
document.addEventListener("visibilitychange", () => {
Glow.schedulePoll();
Glow.calls?.scheduleInvitePolling?.();
if (!document.hidden) {
Glow.resetUnreadMessages();
if (Glow.state.activeView === "feed") {
Glow.pollFeed();
} else if (
Glow.state.activeView === "room" ||
Glow.state.activeView === "channel" ||
Glow.state.activeView === "dm"
) {
Glow.pollMessages();
Glow.pollTyping();
} else if (Glow.state.activeView === "thread") {
Glow.pollThread?.();
} else if (Glow.state.activeView === "dms") {
Glow.loadDms?.();
}
Glow.pingPresence?.();
Glow.refreshPresence?.();
}
});
window.addEventListener("online", () => {
Glow.showToast("Back online.");
if (Glow.state.activeView === "feed") {
Glow.pollFeed();
} else if (
Glow.state.activeView === "room" ||
Glow.state.activeView === "channel" ||
Glow.state.activeView === "dm"
) {
Glow.pollMessages();
Glow.pollTyping();
} else if (Glow.state.activeView === "thread") {
Glow.pollThread?.();
} else if (Glow.state.activeView === "dms") {
Glow.loadDms?.();
}
Glow.pingPresence?.();
Glow.refreshPresence?.();
Glow.calls?.scheduleInvitePolling?.();
});
window.addEventListener("offline", () => {
Glow.showToast("You are offline.", "error");
});
setInterval(() => {
if (
Glow.state.activeView === "room" ||
Glow.state.activeView === "channel" ||
Glow.state.activeView === "dm"
) {
Glow.pollTyping();
}
}, 2500);
setInterval(() => {
if (
Glow.state.activeView === "room" ||
Glow.state.activeView === "channel" ||
Glow.state.activeView === "dm"
) {
Glow.pingPresence?.();
Glow.refreshPresence?.();
}
}, 10000);
window.addEventListener("beforeunload", event => {
if (Glow.state.activeView === "settings" && Glow.isSettingsDirty()) {
event.preventDefault();
event.returnValue = "";
return "";
}
const sessionId = Glow.state.calls.activeCall?.groupSession?.id;
if (!sessionId || !navigator.sendBeacon) return;
const csrf = Glow.state.csrfToken || "";
const blob = new Blob([JSON.stringify({})], {
type: "application/json"
});
navigator.sendBeacon(
`/api/group-calls/${encodeURIComponent(sessionId)}/leave`,
blob
);
fetch(`/api/group-calls/${encodeURIComponent(sessionId)}/leave`, {
method: "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrf,
Accept: "application/json"
},
body: JSON.stringify({}),
keepalive: true
}).catch(() => {});
});
Glow.applyAccentColor(Glow.state.accentColor);
Glow.autoResizeComposer();
Glow.updateMessageCount();
Glow.autoResizeThreadComposer?.();
Glow.updateThreadMessageCount?.();
Glow.state.dmLayoutMode = Glow.loadDmLayoutPreference();
Glow.checkAuth();