glo/static/js/07-team-channel-adapter.js

540 lines
14 KiB
JavaScript

// 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();