540 lines
14 KiB
JavaScript
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(); |