// static/js/07-team-channel-adapter.js "use strict"; window.Glow = window.Glow || {}; // note: // this file keeps team/channel helper methods available, // but the old production monkey-patching adapter is disabled. // rooms remain the stable fallback runtime path. Glow.teamAdapter = Glow.teamAdapter || { initialized: false, enabled: false }; Glow.loadTeamsForAdapter = async () => { if (!Glow.els.teamSelect) return; try { const data = await Glow.apiRequest("/api/teams"); const teams = data.teams || []; const selectedSlug = Glow.state.activeTeam?.slug || Glow.els.teamSelect.value; Glow.state.teams = teams; Glow.els.teamSelect.replaceChildren(); for (const team of teams) { const option = document.createElement("option"); option.value = team.slug; option.textContent = team.name; Glow.els.teamSelect.append(option); } Glow.state.activeTeam = teams.find(team => team.slug === selectedSlug) || teams.find(team => team.slug === "general") || teams[0] || null; Glow.els.teamSelect.disabled = !Glow.state.activeTeam; if (Glow.state.activeTeam) { Glow.els.teamSelect.value = Glow.state.activeTeam.slug; } } catch (error) { console.warn("could not load teams:", error); Glow.state.teams = []; Glow.state.activeTeam = null; Glow.els.teamSelect.disabled = true; } }; Glow.loadChannelsForTeam = async (teamSlug, preferredChannelSlug = null) => { if (!Glow.els.roomSelect) return; if (!teamSlug) { Glow.state.channels = []; Glow.state.activeChannel = null; Glow.els.roomSelect.replaceChildren(); Glow.els.roomSelect.disabled = true; return; } const data = await Glow.apiRequest( `/api/teams/${encodeURIComponent(teamSlug)}/channels` ); const channels = data.channels || []; Glow.state.channels = channels; const matchingTeam = Glow.state.teams.find(team => team.slug === teamSlug); if (matchingTeam || data.team) { Glow.state.activeTeam = { ...(matchingTeam || {}), ...(data.team || {}) }; } Glow.els.roomSelect.replaceChildren(); for (const channel of channels) { const option = document.createElement("option"); option.value = channel.slug; option.textContent = `# ${channel.name}`; Glow.els.roomSelect.append(option); } const selected = channels.find(channel => channel.slug === preferredChannelSlug) || channels.find(channel => channel.slug === Glow.state.activeChannel?.slug) || channels.find(channel => channel.slug === "general") || channels[0] || null; Glow.state.activeChannel = selected; if (selected) { Glow.state.activeRoom = selected; Glow.els.roomSelect.value = selected.slug; } Glow.els.roomSelect.disabled = !selected; }; Glow.decorateChannelSelector = () => { if (!Glow.els.roomSelect) return; Glow.els.roomSelect.classList.add("channel-select"); Glow.els.roomSelect.setAttribute("aria-label", "Select channel"); }; Glow.updateTeamChannelHeader = () => { const team = Glow.state.activeTeam; const channel = Glow.state.activeChannel || Glow.state.activeRoom; const view = Glow.state.activeView; if ( view === "dm" || view === "dms" || view === "feed" || view === "pages" || view === "page" || view === "profile" || view === "settings" || view === "thread" ) { return; } if (!(view === "channel" || view === "room")) return; if (!team || !channel || !Glow.els.headerTitle) return; Glow.els.headerTitle.textContent = `# ${channel.name}`; Glow.els.headerSubtitle.textContent = `${team.name} · ${channel.description || "Channel conversation"}`; }; Glow.applyComposerVisibility = () => { const view = Glow.state.activeView || "feed"; const showChannelComposer = view === "room" || view === "channel" || view === "dm"; const showThreadComposer = view === "thread"; Glow.els.composerWrap?.classList.toggle("hidden", !showChannelComposer); Glow.els.threadComposerWrap?.classList.toggle("hidden", !showThreadComposer); if (!showChannelComposer) { Glow.state.typingUsers = []; Glow.state.dmTypingUsers = []; Glow.renderTyping?.(); } }; Glow.openCreateTeamDialog = async () => { const values = await Glow.openDialog({ title: "Create team", body: "Create a team. Each user can own up to 3 teams.", confirmText: "Create team", fields: [ { name: "name", label: "Team name", placeholder: "Design", maxLength: 60 }, { name: "description", label: "Description", placeholder: "Optional team description", multiline: true, rows: 3, maxLength: 300 }, { name: "slug", label: "Slug (optional)", placeholder: "design", maxLength: 48 } ] }); if (!values) return; try { const data = await Glow.apiCreateTeam({ name: values.name, description: values.description, slug: values.slug }); Glow.showToast("Team created."); await Glow.refreshTeamAndChannelData(data.team?.slug || null); } catch (error) { Glow.showToast(error.message || "Could not create team.", "error"); } }; Glow.openEditTeamDialog = async team => { if (!team?.can_manage) { Glow.showToast("You cannot edit this team.", "error"); return; } const values = await Glow.openDialog({ title: `Edit ${team.name}`, confirmText: "Save", fields: [ { name: "name", label: "Team name", value: team.name || "", maxLength: 60 }, { name: "description", label: "Description", value: team.description || "", multiline: true, rows: 3, maxLength: 300 } ] }); if (!values) return; try { await Glow.apiUpdateTeam(team.slug, { name: values.name, description: values.description }); Glow.showToast("Team updated."); await Glow.refreshTeamAndChannelData(team.slug); } catch (error) { Glow.showToast(error.message || "Could not update team.", "error"); } }; Glow.openCreateChannelDialog = async team => { if (!team?.can_manage) { Glow.showToast("You cannot add channels to this team.", "error"); return; } const values = await Glow.openDialog({ title: `Add channel to ${team.name}`, confirmText: "Create channel", fields: [ { name: "name", label: "Channel name", placeholder: "announcements", maxLength: 60 }, { name: "description", label: "Description", placeholder: "Optional channel description", multiline: true, rows: 3, maxLength: 300 }, { name: "slug", label: "Slug (optional)", placeholder: "announcements", maxLength: 48 } ] }); if (!values) return; try { const data = await Glow.apiCreateChannel(team.slug, { name: values.name, description: values.description, slug: values.slug }); Glow.showToast("Channel created."); await Glow.refreshTeamAndChannelData(team.slug, data.channel?.slug || null); } catch (error) { Glow.showToast(error.message || "Could not create channel.", "error"); } }; Glow.openEditChannelDialog = async (team, channel) => { if (!team?.can_manage || !channel) { Glow.showToast("You cannot edit this channel.", "error"); return; } const values = await Glow.openDialog({ title: `Edit # ${channel.name}`, confirmText: "Save", fields: [ { name: "name", label: "Channel name", value: channel.name || "", maxLength: 60 }, { name: "description", label: "Description", value: channel.description || "", multiline: true, rows: 3, maxLength: 300 } ] }); if (!values) return; try { await Glow.apiUpdateChannel(team.slug, channel.slug, { name: values.name, description: values.description }); Glow.showToast("Channel updated."); await Glow.refreshTeamAndChannelData(team.slug, channel.slug); } catch (error) { Glow.showToast(error.message || "Could not update channel.", "error"); } }; Glow.manageTeamMembers = async team => { if (!team?.can_manage) { Glow.showToast("You cannot manage this team.", "error"); return; } try { const data = await Glow.apiListTeamMembers(team.slug); const container = document.createElement("div"); container.className = "member-list"; for (const member of data.members || []) { const row = document.createElement("div"); row.className = "member-row"; const name = document.createElement("span"); name.textContent = `${member.username} · ${member.membership_role}`; const actions = document.createElement("div"); actions.className = "settings-actions"; const promote = document.createElement("button"); promote.className = "small-button"; promote.type = "button"; promote.textContent = member.membership_role === "member" ? "Make admin" : "Make member"; promote.disabled = member.membership_role === "owner"; promote.addEventListener("click", async () => { try { await Glow.apiUpdateTeamMember(team.slug, member.id, { membership_role: member.membership_role === "member" ? "admin" : "member" }); Glow.closeDialog({}); Glow.manageTeamMembers(team); } catch (error) { Glow.showToast(error.message || "Could not update member.", "error"); } }); const remove = document.createElement("button"); remove.className = "small-button danger"; remove.type = "button"; remove.textContent = "Remove"; remove.disabled = member.membership_role === "owner"; remove.addEventListener("click", async () => { try { await Glow.apiRemoveTeamMember(team.slug, member.id); Glow.closeDialog({}); Glow.manageTeamMembers(team); } catch (error) { Glow.showToast(error.message || "Could not remove member.", "error"); } }); actions.append(promote, remove); row.append(name, actions); container.append(row); } const values = await Glow.openDialog({ title: `${team.name} members`, body: container, confirmText: "Add member", fields: [ { name: "username", label: "Username to add", placeholder: "username", maxLength: 30 }, { name: "membership_role", label: "Role: member or admin", value: "member", maxLength: 6 } ] }); if (!values?.username) return; await Glow.apiAddTeamMember(team.slug, { username: values.username, membership_role: values.membership_role || "member" }); Glow.showToast("Member added."); await Glow.manageTeamMembers(team); } catch (error) { Glow.showToast(error.message || "Could not manage team members.", "error"); } }; Glow.createTeamInviteLink = async team => { if (!team?.can_manage) return; try { const values = await Glow.openDialog({ title: `Create invite for ${team.name}`, confirmText: "Create link", fields: [ { name: "membership_role", label: "Invite role: member or admin", value: "member", maxLength: 6 } ] }); if (!values) return; const data = await Glow.apiCreateTeamInvite(team.slug, { membership_role: values.membership_role || "member" }); const url = new URL(data.invite.url, window.location.origin).toString(); const result = await Glow.openDialog({ title: "Team invite link", body: "Copy and share this link. It can be used once before it expires.", confirmText: "Copy link", fields: [ { name: "invite", label: "Invite link", value: url, maxLength: 500 } ] }); if (result) { await navigator.clipboard.writeText(url); Glow.showToast("Invite link copied."); } } catch (error) { Glow.showToast(error.message || "Could not create team invite.", "error"); } }; Glow.deleteCurrentChannel = async team => { const channel = Glow.state.activeChannel; if (!team?.can_manage || !channel) return; const result = await Glow.openDialog({ title: `Delete # ${channel.name}?`, body: "This channel will no longer be available.", confirmText: "Delete channel", danger: true }); if (!result) return; try { await Glow.apiDeleteChannel(team.slug, channel.slug); Glow.showToast("Channel deleted."); await Glow.refreshTeamAndChannelData(team.slug); } catch (error) { Glow.showToast(error.message || "Could not delete channel.", "error"); } }; Glow.refreshTeamAndChannelData = async (teamSlug = null, channelSlug = null) => { await Glow.loadTeamsForAdapter(); const targetTeamSlug = teamSlug || Glow.state.activeTeam?.slug || Glow.state.teams?.[0]?.slug || null; if (!targetTeamSlug) { Glow.updateViewUI(); return; } await Glow.loadChannelsForTeam(targetTeamSlug, channelSlug); Glow.state.activeDm = null; Glow.state.activeView = "channel"; Glow.updateViewUI(); if (Glow.state.activeChannel) { await Glow.loadChannelMessages?.(); Glow.pollTyping?.(); Glow.pingPresence?.(); Glow.refreshPresence?.(); } else { Glow.clearMessages?.(); Glow.renderEmptyState?.("No channels are available in this team."); } Glow.schedulePoll?.(); }; Glow.installTeamAdapter = () => { if (Glow.teamAdapter.initialized) return; Glow.teamAdapter.initialized = true; Glow.decorateChannelSelector(); console.info("team/channel helper mode enabled; runtime monkey-patching disabled."); }; Glow.installTeamAdapter(); Glow.applyComposerVisibility();