851 lines
22 KiB
JavaScript
851 lines
22 KiB
JavaScript
// static/js/03-admin.js
|
|
"use strict";
|
|
|
|
window.Glow = window.Glow || {};
|
|
|
|
Glow.closeAdminPanel = () => {
|
|
Glow.els.adminPanel.classList.remove("open");
|
|
Glow.els.adminPanel.setAttribute("aria-hidden", "true");
|
|
Glow.els.adminFab.focus();
|
|
};
|
|
|
|
Glow.setAdminTab = tab => {
|
|
Glow.state.adminTab = ["rooms", "users", "teams"].includes(tab) ? tab : "rooms";
|
|
|
|
const isRooms = Glow.state.adminTab === "rooms";
|
|
const isUsers = Glow.state.adminTab === "users";
|
|
const isTeams = Glow.state.adminTab === "teams";
|
|
|
|
Glow.els.adminTabRooms?.classList.toggle("active", isRooms);
|
|
Glow.els.adminTabUsers?.classList.toggle("active", isUsers);
|
|
Glow.els.adminTabTeams?.classList.toggle("active", isTeams);
|
|
|
|
Glow.els.adminTabRooms?.setAttribute("aria-selected", String(isRooms));
|
|
Glow.els.adminTabUsers?.setAttribute("aria-selected", String(isUsers));
|
|
Glow.els.adminTabTeams?.setAttribute("aria-selected", String(isTeams));
|
|
|
|
Glow.els.adminRoomsPanel?.classList.toggle("hidden", !isRooms);
|
|
Glow.els.adminUsersPanel?.classList.toggle("hidden", !isUsers);
|
|
Glow.els.adminTeamsPanel?.classList.toggle("hidden", !isTeams);
|
|
};
|
|
|
|
Glow.buildAdminRow = ({
|
|
title,
|
|
subtitle,
|
|
actions = [],
|
|
className = ""
|
|
}) => {
|
|
const row = document.createElement("div");
|
|
row.className = className || "admin-room";
|
|
|
|
const details = document.createElement("div");
|
|
const name = document.createElement("div");
|
|
name.className =
|
|
className.includes("user")
|
|
? "admin-user-name"
|
|
: className.includes("team")
|
|
? "admin-team-name"
|
|
: "admin-room-name";
|
|
name.textContent = title;
|
|
|
|
const info = document.createElement("div");
|
|
info.className =
|
|
className.includes("user")
|
|
? "admin-user-info"
|
|
: className.includes("team")
|
|
? "admin-team-info"
|
|
: "admin-room-info";
|
|
info.textContent = subtitle || "";
|
|
|
|
details.append(name, info);
|
|
|
|
const actionsWrap = document.createElement("div");
|
|
actionsWrap.className =
|
|
className.includes("user")
|
|
? "admin-user-actions"
|
|
: className.includes("team")
|
|
? "admin-team-actions"
|
|
: "admin-room-actions";
|
|
|
|
for (const action of actions) {
|
|
actionsWrap.append(action);
|
|
}
|
|
|
|
row.append(details, actionsWrap);
|
|
return row;
|
|
};
|
|
|
|
Glow.renderAdminUsers = users => {
|
|
Glow.els.adminUsers.replaceChildren();
|
|
|
|
const heading = document.createElement("h3");
|
|
heading.textContent = "Users";
|
|
Glow.els.adminUsers.append(heading);
|
|
|
|
if (!users.length) {
|
|
const empty = document.createElement("p");
|
|
empty.className = "admin-empty";
|
|
empty.textContent = "No users found.";
|
|
Glow.els.adminUsers.append(empty);
|
|
return;
|
|
}
|
|
|
|
for (const user of users) {
|
|
const current =
|
|
Glow.state.currentUser &&
|
|
Number(Glow.state.currentUser.id) === Number(user.id);
|
|
|
|
const actions = [];
|
|
|
|
if (!current && Glow.isAdmin()) {
|
|
const role = document.createElement("button");
|
|
role.className = "small-button";
|
|
role.type = "button";
|
|
role.textContent =
|
|
user.role === "moderator" ? "Make user" : "Make moderator";
|
|
role.addEventListener("click", () => {
|
|
Glow.updateAdminUser(user.id, {
|
|
role: user.role === "moderator" ? "user" : "moderator"
|
|
});
|
|
});
|
|
|
|
const restrict = document.createElement("button");
|
|
restrict.className = "small-button";
|
|
restrict.type = "button";
|
|
restrict.textContent =
|
|
user.status === "restricted" ? "Unrestrict" : "Restrict";
|
|
restrict.addEventListener("click", () => {
|
|
Glow.toggleRestriction(user);
|
|
});
|
|
|
|
const blacklist = document.createElement("button");
|
|
blacklist.className = "small-button danger";
|
|
blacklist.type = "button";
|
|
blacklist.textContent =
|
|
user.status === "blacklisted" ? "Unblacklist" : "Blacklist";
|
|
blacklist.addEventListener("click", () => {
|
|
Glow.toggleBlacklist(user);
|
|
});
|
|
|
|
actions.push(role, restrict, blacklist);
|
|
}
|
|
|
|
Glow.els.adminUsers.append(
|
|
Glow.buildAdminRow({
|
|
title: `${user.username} · ${user.role}`,
|
|
subtitle:
|
|
`${user.status} · ${user.message_count} message(s)` +
|
|
(user.restricted_until
|
|
? ` · until ${new Date(user.restricted_until).toLocaleString()}`
|
|
: ""),
|
|
actions,
|
|
className: "admin-user"
|
|
})
|
|
);
|
|
}
|
|
};
|
|
|
|
Glow.renderAdminRooms = rooms => {
|
|
Glow.els.adminRooms.replaceChildren();
|
|
|
|
const heading = document.createElement("h3");
|
|
heading.textContent = "Rooms";
|
|
Glow.els.adminRooms.append(heading);
|
|
|
|
if (!rooms.length) {
|
|
const empty = document.createElement("p");
|
|
empty.className = "admin-empty";
|
|
empty.textContent = "No rooms found.";
|
|
Glow.els.adminRooms.append(empty);
|
|
return;
|
|
}
|
|
|
|
for (const room of rooms) {
|
|
const actions = [];
|
|
|
|
if (Glow.isAdmin()) {
|
|
const edit = document.createElement("button");
|
|
edit.className = "small-button";
|
|
edit.type = "button";
|
|
edit.textContent = "Edit";
|
|
edit.addEventListener("click", () => Glow.editRoom(room));
|
|
actions.push(edit);
|
|
|
|
if (room.visibility === "private") {
|
|
const members = document.createElement("button");
|
|
members.className = "small-button";
|
|
members.type = "button";
|
|
members.textContent = "Members";
|
|
members.addEventListener("click", () => {
|
|
Glow.manageRoomMembers(room);
|
|
});
|
|
|
|
const invite = document.createElement("button");
|
|
invite.className = "small-button";
|
|
invite.type = "button";
|
|
invite.textContent = "Invite link";
|
|
invite.addEventListener("click", () => {
|
|
Glow.createInviteLink(room);
|
|
});
|
|
|
|
actions.push(members, invite);
|
|
}
|
|
|
|
if (room.slug !== "general") {
|
|
const remove = document.createElement("button");
|
|
remove.className = "small-button danger";
|
|
remove.type = "button";
|
|
remove.textContent = "Delete";
|
|
remove.addEventListener("click", () => Glow.deleteRoom(room));
|
|
actions.push(remove);
|
|
}
|
|
}
|
|
|
|
Glow.els.adminRooms.append(
|
|
Glow.buildAdminRow({
|
|
title: `${room.name} · ${room.visibility}`,
|
|
subtitle:
|
|
`/${room.slug}` +
|
|
(room.description ? ` · ${room.description}` : ""),
|
|
actions,
|
|
className: "admin-room"
|
|
})
|
|
);
|
|
}
|
|
};
|
|
|
|
Glow.renderAdminTeamsChannels = () => {
|
|
const box = Glow.els.adminTeamsChannels;
|
|
if (!box) return;
|
|
box.replaceChildren();
|
|
|
|
const heading = document.createElement("h3");
|
|
heading.textContent = "Teams / channels";
|
|
box.append(heading);
|
|
|
|
const team = Glow.state.activeTeam;
|
|
const channel = Glow.state.activeChannel || Glow.state.activeRoom;
|
|
|
|
if (!team) {
|
|
const empty = document.createElement("p");
|
|
empty.className = "admin-empty";
|
|
empty.textContent = "Select a team to manage team/channel actions.";
|
|
box.append(empty);
|
|
return;
|
|
}
|
|
|
|
const teamActions = [];
|
|
if (team.can_manage) {
|
|
const editTeam = document.createElement("button");
|
|
editTeam.className = "small-button";
|
|
editTeam.type = "button";
|
|
editTeam.textContent = "Edit team";
|
|
editTeam.addEventListener("click", () => Glow.openEditTeamDialog?.(team));
|
|
|
|
const members = document.createElement("button");
|
|
members.className = "small-button";
|
|
members.type = "button";
|
|
members.textContent = "Members";
|
|
members.addEventListener("click", () => Glow.manageTeamMembers?.(team));
|
|
|
|
const invite = document.createElement("button");
|
|
invite.className = "small-button";
|
|
invite.type = "button";
|
|
invite.textContent = "Invite";
|
|
invite.addEventListener("click", () => Glow.createTeamInviteLink?.(team));
|
|
|
|
const addChannel = document.createElement("button");
|
|
addChannel.className = "small-button";
|
|
addChannel.type = "button";
|
|
addChannel.textContent = "Add channel";
|
|
addChannel.addEventListener("click", () => Glow.openCreateChannelDialog?.(team));
|
|
|
|
teamActions.push(editTeam, members, invite, addChannel);
|
|
}
|
|
|
|
box.append(
|
|
Glow.buildAdminRow({
|
|
title: team.name,
|
|
subtitle: team.description || `/${team.slug}`,
|
|
actions: teamActions,
|
|
className: "admin-team-row"
|
|
})
|
|
);
|
|
|
|
const channels = Array.isArray(Glow.state.channels) ? Glow.state.channels : [];
|
|
|
|
if (!channels.length) {
|
|
const empty = document.createElement("p");
|
|
empty.className = "admin-empty";
|
|
empty.textContent = "No channels available for this team.";
|
|
box.append(empty);
|
|
return;
|
|
}
|
|
|
|
for (const item of channels) {
|
|
const actions = [];
|
|
const isActive = channel && item.slug === channel.slug;
|
|
|
|
if (team.can_manage) {
|
|
const edit = document.createElement("button");
|
|
edit.className = "small-button";
|
|
edit.type = "button";
|
|
edit.textContent = "Edit";
|
|
edit.addEventListener("click", () => {
|
|
Glow.openEditChannelDialog?.(team, item);
|
|
});
|
|
actions.push(edit);
|
|
|
|
if (!item.legacy_room_id) {
|
|
const remove = document.createElement("button");
|
|
remove.className = "small-button danger";
|
|
remove.type = "button";
|
|
remove.textContent = "Delete";
|
|
remove.addEventListener("click", async () => {
|
|
const previous = Glow.state.activeChannel;
|
|
Glow.state.activeChannel = item;
|
|
await Glow.deleteCurrentChannel?.(team);
|
|
Glow.state.activeChannel = previous;
|
|
});
|
|
actions.push(remove);
|
|
}
|
|
}
|
|
|
|
box.append(
|
|
Glow.buildAdminRow({
|
|
title: `${isActive ? "• " : ""}# ${item.name}`,
|
|
subtitle: item.description || `/${item.slug}`,
|
|
actions,
|
|
className: "admin-team-row"
|
|
})
|
|
);
|
|
}
|
|
};
|
|
|
|
Glow.openAdminPanel = async () => {
|
|
if (!Glow.canModerate()) return;
|
|
|
|
Glow.els.adminPanel.classList.add("open");
|
|
Glow.els.adminPanel.setAttribute("aria-hidden", "false");
|
|
|
|
if (Glow.els.adminUsers) {
|
|
Glow.els.adminUsers.textContent = "Loading users…";
|
|
}
|
|
if (Glow.els.adminRooms) {
|
|
Glow.els.adminRooms.textContent = "Loading rooms…";
|
|
}
|
|
if (Glow.els.adminTeamsChannels) {
|
|
Glow.els.adminTeamsChannels.textContent = "Loading teams/channels…";
|
|
}
|
|
|
|
try {
|
|
const [usersData, roomsData] = await Promise.all([
|
|
Glow.apiRequest("/api/admin/users"),
|
|
Glow.apiRequest("/api/admin/rooms")
|
|
]);
|
|
|
|
Glow.renderAdminUsers(usersData.users || []);
|
|
Glow.renderAdminRooms(roomsData.rooms || []);
|
|
Glow.renderAdminTeamsChannels();
|
|
|
|
Glow.setAdminTab(Glow.state.adminTab || "rooms");
|
|
|
|
setTimeout(() => Glow.els.closeAdminButton.focus(), 0);
|
|
} catch (error) {
|
|
Glow.showToast(error.message, "error");
|
|
}
|
|
};
|
|
|
|
Glow.updateThemeButton = () => {
|
|
if (!Glow.els.themeButton) return;
|
|
Glow.els.themeButton.textContent = "Accent color";
|
|
};
|
|
|
|
Glow.updateAdminPreferenceButtons = () => {};
|
|
|
|
Glow.openAccentColorPicker = async () => {
|
|
const container = document.createElement("div");
|
|
container.className = "accent-picker";
|
|
|
|
const description = document.createElement("p");
|
|
description.className = "accent-picker-description";
|
|
description.textContent =
|
|
"Choose an accent color for focus rings, buttons, selected controls, and highlighted mentions. It is stored only in this browser unless saved in account settings.";
|
|
|
|
const label = document.createElement("label");
|
|
label.className = "accent-picker-label";
|
|
label.textContent = "Accent color";
|
|
|
|
const row = document.createElement("div");
|
|
row.className = "accent-picker-row";
|
|
|
|
const colorInput = document.createElement("input");
|
|
colorInput.className = "accent-color-input";
|
|
colorInput.type = "color";
|
|
colorInput.value =
|
|
Glow.normalizeAccentColor(Glow.state.accentColor) ||
|
|
Glow.DEFAULT_ACCENT_COLOR;
|
|
colorInput.setAttribute("aria-label", "Choose accent color");
|
|
|
|
const hexInput = document.createElement("input");
|
|
hexInput.className = "field accent-hex-input";
|
|
hexInput.type = "text";
|
|
hexInput.value = colorInput.value;
|
|
hexInput.maxLength = 7;
|
|
hexInput.autocomplete = "off";
|
|
hexInput.autocapitalize = "off";
|
|
hexInput.spellcheck = false;
|
|
hexInput.placeholder = "#f7dce8";
|
|
hexInput.setAttribute(
|
|
"aria-label",
|
|
"Accent color hexadecimal value"
|
|
);
|
|
|
|
const preview = document.createElement("span");
|
|
preview.className = "accent-preview";
|
|
preview.style.background = colorInput.value;
|
|
preview.setAttribute("aria-hidden", "true");
|
|
|
|
const error = document.createElement("div");
|
|
error.className = "accent-picker-error";
|
|
error.setAttribute("role", "status");
|
|
|
|
const updatePreview = value => {
|
|
const normalized = Glow.normalizeAccentColor(value);
|
|
|
|
if (!normalized) {
|
|
preview.style.background = "transparent";
|
|
error.textContent =
|
|
"Use a six-digit hex color, such as #7dd3fc.";
|
|
return false;
|
|
}
|
|
|
|
preview.style.background = normalized;
|
|
error.textContent = "";
|
|
|
|
if (colorInput.value !== normalized) {
|
|
colorInput.value = normalized;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
colorInput.addEventListener("input", () => {
|
|
const color = Glow.normalizeAccentColor(colorInput.value);
|
|
if (!color) return;
|
|
hexInput.value = color;
|
|
updatePreview(color);
|
|
});
|
|
|
|
hexInput.addEventListener("input", () => {
|
|
updatePreview(hexInput.value.trim());
|
|
});
|
|
|
|
row.append(colorInput, hexInput, preview);
|
|
label.append(row);
|
|
container.append(description, label, error);
|
|
|
|
const result = await Glow.openDialog({
|
|
title: "Accent color",
|
|
body: container,
|
|
confirmText: "Save color",
|
|
cancelText: "Cancel"
|
|
});
|
|
|
|
if (!result) return;
|
|
|
|
const accent = Glow.normalizeAccentColor(hexInput.value);
|
|
|
|
if (!accent) {
|
|
Glow.showToast(
|
|
"Choose a valid six-digit hexadecimal color.",
|
|
"error"
|
|
);
|
|
return;
|
|
}
|
|
|
|
Glow.applyAccentColor(accent);
|
|
Glow.showToast("Accent color updated.");
|
|
};
|
|
|
|
Glow.toggleRestriction = async user => {
|
|
if (user.status === "restricted") {
|
|
return Glow.updateAdminUser(user.id, {
|
|
status: "active",
|
|
restricted_until: null
|
|
});
|
|
}
|
|
|
|
const values = await Glow.openDialog({
|
|
title: `Restrict ${user.username}`,
|
|
body: "Enter the restriction duration in minutes.",
|
|
confirmText: "Restrict",
|
|
danger: true,
|
|
fields: [
|
|
{
|
|
name: "minutes",
|
|
label: "Duration in minutes",
|
|
type: "number",
|
|
value: "60",
|
|
maxLength: 5
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!values) return;
|
|
|
|
const minutes = Number(values.minutes);
|
|
|
|
if (!Number.isFinite(minutes) || minutes < 1 || minutes > 43200) {
|
|
Glow.showToast(
|
|
"Enter a duration between 1 minute and 30 days.",
|
|
"error"
|
|
);
|
|
return;
|
|
}
|
|
|
|
await Glow.updateAdminUser(user.id, {
|
|
status: "restricted",
|
|
restricted_until: new Date(
|
|
Date.now() + minutes * 60 * 1000
|
|
).toISOString()
|
|
});
|
|
};
|
|
|
|
Glow.toggleBlacklist = async user => {
|
|
const blacklisting = user.status !== "blacklisted";
|
|
|
|
const result = await Glow.openDialog({
|
|
title: blacklisting
|
|
? `Blacklist ${user.username}?`
|
|
: `Unblacklist ${user.username}?`,
|
|
body: blacklisting
|
|
? "The account will be denied access immediately."
|
|
: "The account will be restored to active status.",
|
|
confirmText: blacklisting ? "Blacklist" : "Unblacklist",
|
|
danger: blacklisting
|
|
});
|
|
|
|
if (!result) return;
|
|
|
|
await Glow.updateAdminUser(user.id, {
|
|
status: blacklisting ? "blacklisted" : "active"
|
|
});
|
|
};
|
|
|
|
Glow.updateAdminUser = async (userId, changes) => {
|
|
try {
|
|
await Glow.apiRequest(`/api/admin/users/${encodeURIComponent(userId)}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(changes)
|
|
});
|
|
|
|
Glow.showToast("User updated.");
|
|
await Glow.openAdminPanel();
|
|
} catch (error) {
|
|
Glow.showToast(error.message, "error");
|
|
}
|
|
};
|
|
|
|
Glow.createRoom = async () => {
|
|
const values = await Glow.openDialog({
|
|
title: "Create room",
|
|
body: "Choose a room name, description, and visibility.",
|
|
confirmText: "Create",
|
|
fields: [
|
|
{
|
|
name: "name",
|
|
label: "Room name",
|
|
placeholder: "Project chat",
|
|
maxLength: 60
|
|
},
|
|
{
|
|
name: "description",
|
|
label: "Description",
|
|
placeholder: "Optional room description",
|
|
multiline: true,
|
|
rows: 3,
|
|
maxLength: 300
|
|
},
|
|
{
|
|
name: "visibility",
|
|
label: "Visibility: public or private",
|
|
value: "public",
|
|
maxLength: 7
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!values) return;
|
|
|
|
const visibility = values.visibility.toLowerCase();
|
|
|
|
if (!["public", "private"].includes(visibility)) {
|
|
Glow.showToast("Visibility must be public or private.", "error");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await Glow.apiRequest("/api/rooms", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
name: values.name,
|
|
description: values.description,
|
|
visibility
|
|
})
|
|
});
|
|
|
|
Glow.showToast("Room created.");
|
|
await Glow.loadRooms();
|
|
await Glow.openAdminPanel();
|
|
} catch (error) {
|
|
Glow.showToast(error.message, "error");
|
|
}
|
|
};
|
|
|
|
Glow.editRoom = async room => {
|
|
const values = await Glow.openDialog({
|
|
title: `Edit ${room.name}`,
|
|
confirmText: "Save",
|
|
fields: [
|
|
{
|
|
name: "name",
|
|
label: "Room name",
|
|
value: room.name,
|
|
maxLength: 60
|
|
},
|
|
{
|
|
name: "description",
|
|
label: "Description",
|
|
value: room.description || "",
|
|
multiline: true,
|
|
rows: 3,
|
|
maxLength: 300
|
|
},
|
|
{
|
|
name: "visibility",
|
|
label: "Visibility: public or private",
|
|
value: room.visibility,
|
|
maxLength: 7
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!values) return;
|
|
|
|
const visibility = values.visibility.toLowerCase();
|
|
|
|
if (!["public", "private"].includes(visibility)) {
|
|
Glow.showToast("Visibility must be public or private.", "error");
|
|
return;
|
|
}
|
|
|
|
await Glow.updateRoom(room.slug, {
|
|
name: values.name,
|
|
description: values.description,
|
|
visibility
|
|
});
|
|
};
|
|
|
|
Glow.updateRoom = async (slug, changes) => {
|
|
try {
|
|
await Glow.apiRequest(`/api/rooms/${encodeURIComponent(slug)}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(changes)
|
|
});
|
|
|
|
Glow.showToast("Room updated.");
|
|
await Glow.loadRooms();
|
|
|
|
if (!Glow.state.activeDm) {
|
|
await Glow.loadMessages();
|
|
}
|
|
|
|
await Glow.openAdminPanel();
|
|
} catch (error) {
|
|
Glow.showToast(error.message, "error");
|
|
}
|
|
};
|
|
|
|
Glow.deleteRoom = async room => {
|
|
const result = await Glow.openDialog({
|
|
title: `Delete ${room.name}?`,
|
|
body: "This permanently deletes the room and every message in it.",
|
|
confirmText: "Delete room",
|
|
danger: true
|
|
});
|
|
|
|
if (!result) return;
|
|
|
|
try {
|
|
await Glow.apiRequest(`/api/rooms/${encodeURIComponent(room.slug)}`, {
|
|
method: "DELETE"
|
|
});
|
|
|
|
Glow.showToast("Room deleted.");
|
|
await Glow.loadRooms();
|
|
|
|
if (!Glow.state.activeDm) {
|
|
await Glow.loadMessages();
|
|
}
|
|
|
|
await Glow.openAdminPanel();
|
|
} catch (error) {
|
|
Glow.showToast(error.message, "error");
|
|
}
|
|
};
|
|
|
|
Glow.clearActiveRoom = async () => {
|
|
const room = Glow.state.activeRoom;
|
|
|
|
if (!room || Glow.state.activeDm) return;
|
|
|
|
const result = await Glow.openDialog({
|
|
title: `Clear ${room.name}?`,
|
|
body: "All messages in this room will be permanently deleted.",
|
|
confirmText: "Clear room",
|
|
danger: true
|
|
});
|
|
|
|
if (!result) return;
|
|
|
|
try {
|
|
await Glow.apiRequest(
|
|
`/api/rooms/${encodeURIComponent(room.slug)}/messages`,
|
|
{ method: "DELETE" }
|
|
);
|
|
|
|
Glow.clearMessages();
|
|
Glow.renderEmptyState();
|
|
Glow.showToast("Room messages cleared.");
|
|
} catch (error) {
|
|
Glow.showToast(error.message, "error");
|
|
}
|
|
};
|
|
|
|
Glow.manageRoomMembers = async room => {
|
|
try {
|
|
const data = await Glow.apiRequest(
|
|
`/api/rooms/${encodeURIComponent(room.slug)}/members`
|
|
);
|
|
|
|
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 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.apiRequest(
|
|
`/api/rooms/${encodeURIComponent(room.slug)}/members/${member.id}`,
|
|
{ method: "DELETE" }
|
|
);
|
|
|
|
Glow.showToast("Member removed.");
|
|
Glow.closeDialog({});
|
|
Glow.manageRoomMembers(room);
|
|
} catch (error) {
|
|
Glow.showToast(error.message, "error");
|
|
}
|
|
});
|
|
|
|
row.append(name, remove);
|
|
container.append(row);
|
|
}
|
|
|
|
const values = await Glow.openDialog({
|
|
title: `${room.name} members`,
|
|
body: container,
|
|
confirmText: "Add member",
|
|
fields: [
|
|
{
|
|
name: "username",
|
|
label: "Username to add",
|
|
placeholder: "username",
|
|
maxLength: 30
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!values?.username) return;
|
|
|
|
await Glow.apiRequest(
|
|
`/api/rooms/${encodeURIComponent(room.slug)}/members`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username: values.username })
|
|
}
|
|
);
|
|
|
|
Glow.showToast("Member added.");
|
|
await Glow.manageRoomMembers(room);
|
|
} catch (error) {
|
|
Glow.showToast(error.message, "error");
|
|
}
|
|
};
|
|
|
|
Glow.createInviteLink = async room => {
|
|
try {
|
|
const data = await Glow.apiRequest(
|
|
`/api/rooms/${encodeURIComponent(room.slug)}/invites`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({})
|
|
}
|
|
);
|
|
|
|
const url = new URL(
|
|
data.invite.url,
|
|
window.location.origin
|
|
).toString();
|
|
|
|
const values = await Glow.openDialog({
|
|
title: "Private-room 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 (values) {
|
|
await navigator.clipboard.writeText(url);
|
|
Glow.showToast("Invite link copied.");
|
|
}
|
|
} catch (error) {
|
|
Glow.showToast(error.message, "error");
|
|
}
|
|
};
|
|
|
|
Glow.els.adminTabRooms?.addEventListener("click", () => {
|
|
Glow.setAdminTab("rooms");
|
|
});
|
|
|
|
Glow.els.adminTabUsers?.addEventListener("click", () => {
|
|
Glow.setAdminTab("users");
|
|
});
|
|
|
|
Glow.els.adminTabTeams?.addEventListener("click", () => {
|
|
Glow.setAdminTab("teams");
|
|
}); |