From 7400c0946e460c90f92d8b64216799e3dbb5b0b9 Mon Sep 17 00:00:00 2001 From: Shadow Date: Sat, 3 Jan 2026 01:02:18 -0600 Subject: [PATCH] Discord: update UIs to use the new config --- .gitignore | 1 + .../Sources/Clawdis/ConnectionsSettings.swift | 93 ++++++ .../Sources/Clawdis/ConnectionsStore.swift | 143 ++++++++ src/canvas-host/a2ui/.bundle.hash | 2 +- ui/src/styles/components.css | 4 + ui/src/ui/app.ts | 32 +- ui/src/ui/controllers/chat.ts | 2 +- ui/src/ui/controllers/config.ts | 55 ++++ ui/src/ui/controllers/connections.ts | 49 +++ ui/src/ui/ui-types.ts | 18 + ui/src/ui/views/connections.ts | 311 +++++++++++++++++- 11 files changed, 682 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 769176832..09a54faf8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ vendor/ # Vendor build artifacts vendor/a2ui/renderers/lit/dist/ +.bundle.hash # fastlane (iOS) apps/ios/fastlane/README.md diff --git a/apps/macos/Sources/Clawdis/ConnectionsSettings.swift b/apps/macos/Sources/Clawdis/ConnectionsSettings.swift index a69379c26..da506953c 100644 --- a/apps/macos/Sources/Clawdis/ConnectionsSettings.swift +++ b/apps/macos/Sources/Clawdis/ConnectionsSettings.swift @@ -289,6 +289,12 @@ struct ConnectionsSettings: View { TextField("123456789, username#1234", text: self.$store.discordAllowFrom) .textFieldStyle(.roundedBorder) } + GridRow { + self.gridLabel("DMs enabled") + Toggle("", isOn: self.$store.discordDmEnabled) + .labelsHidden() + .toggleStyle(.checkbox) + } GridRow { self.gridLabel("Group DMs") Toggle("", isOn: self.$store.discordGroupEnabled) @@ -310,6 +316,20 @@ struct ConnectionsSettings: View { TextField("20", text: self.$store.discordHistoryLimit) .textFieldStyle(.roundedBorder) } + GridRow { + self.gridLabel("Text chunk limit") + TextField("2000", text: self.$store.discordTextChunkLimit) + .textFieldStyle(.roundedBorder) + } + GridRow { + self.gridLabel("Reply to mode") + Picker("", selection: self.$store.discordReplyToMode) { + Text("off").tag("off") + Text("first").tag("first") + Text("all").tag("all") + } + .labelsHidden() + } GridRow { self.gridLabel("Slash command") Toggle("", isOn: self.$store.discordSlashEnabled) @@ -336,6 +356,79 @@ struct ConnectionsSettings: View { Divider().padding(.vertical, 2) + Text("Guilds") + .font(.caption) + .foregroundStyle(.secondary) + + VStack(alignment: .leading, spacing: 12) { + ForEach($store.discordGuilds) { $guild in + VStack(alignment: .leading, spacing: 10) { + HStack { + TextField("guild id or slug", text: $guild.key) + .textFieldStyle(.roundedBorder) + Button("Remove") { + self.store.discordGuilds.removeAll { $0.id == guild.id } + } + .buttonStyle(.bordered) + } + + Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) { + GridRow { + self.gridLabel("Slug") + TextField("optional slug", text: $guild.slug) + .textFieldStyle(.roundedBorder) + } + GridRow { + self.gridLabel("Require mention") + Toggle("", isOn: $guild.requireMention) + .labelsHidden() + .toggleStyle(.checkbox) + } + GridRow { + self.gridLabel("Users allowlist") + TextField("123456789, username#1234", text: $guild.users) + .textFieldStyle(.roundedBorder) + } + } + + Text("Channels") + .font(.caption) + .foregroundStyle(.secondary) + + VStack(alignment: .leading, spacing: 8) { + ForEach($guild.channels) { $channel in + HStack(spacing: 10) { + TextField("channel id or slug", text: $channel.key) + .textFieldStyle(.roundedBorder) + Toggle("Allow", isOn: $channel.allow) + .toggleStyle(.checkbox) + Toggle("Require mention", isOn: $channel.requireMention) + .toggleStyle(.checkbox) + Button("Remove") { + guild.channels.removeAll { $0.id == channel.id } + } + .buttonStyle(.bordered) + } + } + Button("Add channel") { + guild.channels.append(DiscordGuildChannelForm()) + } + .buttonStyle(.bordered) + } + } + .padding(10) + .background(Color.secondary.opacity(0.08)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + + Button("Add guild") { + self.store.discordGuilds.append(DiscordGuildForm()) + } + .buttonStyle(.bordered) + } + + Divider().padding(.vertical, 2) + Text("Tool actions") .font(.caption) .foregroundStyle(.secondary) diff --git a/apps/macos/Sources/Clawdis/ConnectionsStore.swift b/apps/macos/Sources/Clawdis/ConnectionsStore.swift index fd1bafecf..27d66b82f 100644 --- a/apps/macos/Sources/Clawdis/ConnectionsStore.swift +++ b/apps/macos/Sources/Clawdis/ConnectionsStore.swift @@ -144,6 +144,42 @@ struct ConfigSnapshot: Codable { let issues: [Issue]? } +struct DiscordGuildChannelForm: Identifiable { + let id = UUID() + var key: String + var allow: Bool + var requireMention: Bool + + init(key: String = "", allow: Bool = true, requireMention: Bool = false) { + self.key = key + self.allow = allow + self.requireMention = requireMention + } +} + +struct DiscordGuildForm: Identifiable { + let id = UUID() + var key: String + var slug: String + var requireMention: Bool + var users: String + var channels: [DiscordGuildChannelForm] + + init( + key: String = "", + slug: String = "", + requireMention: Bool = false, + users: String = "", + channels: [DiscordGuildChannelForm] = [] + ) { + self.key = key + self.slug = slug + self.requireMention = requireMention + self.users = users + self.channels = channels + } +} + @MainActor @Observable final class ConnectionsStore { @@ -169,11 +205,15 @@ final class ConnectionsStore { var telegramBusy = false var discordEnabled = true var discordToken: String = "" + var discordDmEnabled = true var discordAllowFrom: String = "" var discordGroupEnabled = false var discordGroupChannels: String = "" var discordMediaMaxMb: String = "" var discordHistoryLimit: String = "" + var discordTextChunkLimit: String = "" + var discordReplyToMode: String = "off" + var discordGuilds: [DiscordGuildForm] = [] var discordActionReactions = true var discordActionStickers = true var discordActionPolls = true @@ -401,6 +441,7 @@ final class ConnectionsStore { self.discordEnabled = discord?["enabled"]?.boolValue ?? true self.discordToken = discord?["token"]?.stringValue ?? "" let discordDm = discord?["dm"]?.dictionaryValue + self.discordDmEnabled = discordDm?["enabled"]?.boolValue ?? true if let allow = discordDm?["allowFrom"]?.arrayValue { let strings = allow.compactMap { entry -> String? in if let str = entry.stringValue { return str } @@ -434,6 +475,56 @@ final class ConnectionsStore { } else { self.discordHistoryLimit = "" } + if let limit = discord?["textChunkLimit"]?.doubleValue ?? discord?["textChunkLimit"]?.intValue.map(Double.init) { + self.discordTextChunkLimit = String(Int(limit)) + } else { + self.discordTextChunkLimit = "" + } + if let mode = discord?["replyToMode"]?.stringValue, ["off", "first", "all"].contains(mode) { + self.discordReplyToMode = mode + } else { + self.discordReplyToMode = "off" + } + if let guilds = discord?["guilds"]?.dictionaryValue { + self.discordGuilds = guilds + .map { key, value in + let entry = value.dictionaryValue ?? [:] + let slug = entry["slug"]?.stringValue ?? "" + let requireMention = entry["requireMention"]?.boolValue ?? false + let users = entry["users"]?.arrayValue? + .compactMap { item -> String? in + if let str = item.stringValue { return str } + if let intVal = item.intValue { return String(intVal) } + if let doubleVal = item.doubleValue { return String(Int(doubleVal)) } + return nil + } + .joined(separator: ", ") ?? "" + let channels: [DiscordGuildChannelForm] + if let channelMap = entry["channels"]?.dictionaryValue { + channels = channelMap.map { channelKey, channelValue in + let channelEntry = channelValue.dictionaryValue ?? [:] + let allow = channelEntry["allow"]?.boolValue ?? true + let channelRequireMention = + channelEntry["requireMention"]?.boolValue ?? false + return DiscordGuildChannelForm( + key: channelKey, + allow: allow, + requireMention: channelRequireMention) + } + } else { + channels = [] + } + return DiscordGuildForm( + key: key, + slug: slug, + requireMention: requireMention, + users: users, + channels: channels) + } + .sorted { $0.key < $1.key } + } else { + self.discordGuilds = [] + } let discordActions = discord?["actions"]?.dictionaryValue self.discordActionReactions = discordActions?["reactions"]?.boolValue ?? true self.discordActionStickers = discordActions?["stickers"]?.boolValue ?? true @@ -625,6 +716,11 @@ final class ConnectionsStore { } var dm: [String: Any] = (discord["dm"] as? [String: Any]) ?? [:] + if self.discordDmEnabled { + dm.removeValue(forKey: "enabled") + } else { + dm["enabled"] = false + } let allow = self.discordAllowFrom .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } @@ -673,6 +769,53 @@ final class ConnectionsStore { discord.removeValue(forKey: "historyLimit") } + let chunkLimit = self.discordTextChunkLimit.trimmingCharacters(in: .whitespacesAndNewlines) + if chunkLimit.isEmpty { + discord.removeValue(forKey: "textChunkLimit") + } else if let value = Int(chunkLimit), value > 0 { + discord["textChunkLimit"] = value + } else { + discord.removeValue(forKey: "textChunkLimit") + } + + let replyToMode = self.discordReplyToMode.trimmingCharacters(in: .whitespacesAndNewlines) + if replyToMode.isEmpty || replyToMode == "off" { + discord.removeValue(forKey: "replyToMode") + } else if ["first", "all"].contains(replyToMode) { + discord["replyToMode"] = replyToMode + } else { + discord.removeValue(forKey: "replyToMode") + } + + let guilds: [String: Any] = self.discordGuilds.reduce(into: [:]) { result, entry in + let key = entry.key.trimmingCharacters(in: .whitespacesAndNewlines) + guard !key.isEmpty else { return } + var payload: [String: Any] = [:] + let slug = entry.slug.trimmingCharacters(in: .whitespacesAndNewlines) + if !slug.isEmpty { payload["slug"] = slug } + if entry.requireMention { payload["requireMention"] = true } + let users = entry.users + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + if !users.isEmpty { payload["users"] = users } + let channels: [String: Any] = entry.channels.reduce(into: [:]) { channelsResult, channel in + let channelKey = channel.key.trimmingCharacters(in: .whitespacesAndNewlines) + guard !channelKey.isEmpty else { return } + var channelPayload: [String: Any] = [:] + if !channel.allow { channelPayload["allow"] = false } + if channel.requireMention { channelPayload["requireMention"] = true } + channelsResult[channelKey] = channelPayload + } + if !channels.isEmpty { payload["channels"] = channels } + result[key] = payload + } + if guilds.isEmpty { + discord.removeValue(forKey: "guilds") + } else { + discord["guilds"] = guilds + } + var actions: [String: Any] = (discord["actions"] as? [String: Any]) ?? [:] func setAction(_ key: String, value: Bool, defaultValue: Bool) { if value == defaultValue { diff --git a/src/canvas-host/a2ui/.bundle.hash b/src/canvas-host/a2ui/.bundle.hash index 42970b8c1..4f59d30f5 100644 --- a/src/canvas-host/a2ui/.bundle.hash +++ b/src/canvas-host/a2ui/.bundle.hash @@ -1 +1 @@ -debc115fd2b264d3b3090e28b733551ca5f7532287d827c13fc579e5a7a95b9d +549aa1fff40f95b4a899940ad2cde0adc3991423136ba02c23bcdbd621765ad7 diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index 5a0e155d4..0b3aabdc2 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -210,6 +210,10 @@ gap: 6px; } +.field.full { + grid-column: 1 / -1; +} + .field span { color: var(--muted); font-size: 11px; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index fe88468a2..f589aa906 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -34,7 +34,12 @@ import { type SignalForm, type TelegramForm, } from "./ui-types"; -import { loadChatHistory, sendChat, handleChatEvent } from "./controllers/chat"; +import { + loadChatHistory, + sendChat, + handleChatEvent, + type ChatEventPayload, +} from "./controllers/chat"; import { loadNodes } from "./controllers/nodes"; import { loadConfig } from "./controllers/config"; import { @@ -139,11 +144,15 @@ export class ClawdisApp extends LitElement { @state() discordForm: DiscordForm = { enabled: true, token: "", + dmEnabled: true, allowFrom: "", groupEnabled: false, groupChannels: "", mediaMaxMb: "", historyLimit: "", + textChunkLimit: "", + replyToMode: "off", + guilds: [], actions: { ...defaultDiscordActions }, slashEnabled: false, slashName: "", @@ -350,7 +359,8 @@ export class ClawdisApp extends LitElement { ].slice(0, 250); if (evt.event === "chat") { - const state = handleChatEvent(this, evt.payload as unknown); + const payload = evt.payload as ChatEventPayload | undefined; + const state = handleChatEvent(this, payload); if (state === "final") void loadChatHistory(this); return; } @@ -469,20 +479,26 @@ export class ClawdisApp extends LitElement { if (this.theme !== "system") return; this.applyResolvedTheme(event.matches ? "dark" : "light"); }; - if ("addEventListener" in this.themeMedia) { + if (typeof this.themeMedia.addEventListener === "function") { this.themeMedia.addEventListener("change", this.themeMediaHandler); - } else { - this.themeMedia.addListener(this.themeMediaHandler); + return; } + const legacy = this.themeMedia as MediaQueryList & { + addListener: (cb: (event: MediaQueryListEvent) => void) => void; + }; + legacy.addListener(this.themeMediaHandler); } private detachThemeListener() { if (!this.themeMedia || !this.themeMediaHandler) return; - if ("removeEventListener" in this.themeMedia) { + if (typeof this.themeMedia.removeEventListener === "function") { this.themeMedia.removeEventListener("change", this.themeMediaHandler); - } else { - this.themeMedia.removeListener(this.themeMediaHandler); + return; } + const legacy = this.themeMedia as MediaQueryList & { + removeListener: (cb: (event: MediaQueryListEvent) => void) => void; + }; + legacy.removeListener(this.themeMediaHandler); this.themeMedia = null; this.themeMediaHandler = null; } diff --git a/ui/src/ui/controllers/chat.ts b/ui/src/ui/controllers/chat.ts index b407a3862..0957bd27b 100644 --- a/ui/src/ui/controllers/chat.ts +++ b/ui/src/ui/controllers/chat.ts @@ -14,7 +14,7 @@ export type ChatState = { lastError: string | null; }; -type ChatEventPayload = { +export type ChatEventPayload = { runId: string; sessionKey: string; state: "delta" | "final" | "aborted" | "error"; diff --git a/ui/src/ui/controllers/config.ts b/ui/src/ui/controllers/config.ts index fb2248002..e1fc9811b 100644 --- a/ui/src/ui/controllers/config.ts +++ b/ui/src/ui/controllers/config.ts @@ -4,6 +4,8 @@ import { defaultDiscordActions, type DiscordActionForm, type DiscordForm, + type DiscordGuildChannelForm, + type DiscordGuildForm, type IMessageForm, type SignalForm, type TelegramForm, @@ -96,6 +98,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot const discordDm = (discord.dm ?? {}) as Record; const slash = (discord.slashCommand ?? {}) as Record; const discordActions = (discord.actions ?? {}) as Record; + const discordGuilds = discord.guilds; const readAction = (key: keyof DiscordActionForm) => typeof discordActions[key] === "boolean" ? (discordActions[key] as boolean) @@ -103,6 +106,7 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot state.discordForm = { enabled: typeof discord.enabled === "boolean" ? discord.enabled : true, token: typeof discord.token === "string" ? discord.token : "", + dmEnabled: typeof discordDm.enabled === "boolean" ? discordDm.enabled : true, allowFrom: toList(discordDm.allowFrom), groupEnabled: typeof discordDm.groupEnabled === "boolean" ? discordDm.groupEnabled : false, @@ -111,6 +115,57 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot typeof discord.mediaMaxMb === "number" ? String(discord.mediaMaxMb) : "", historyLimit: typeof discord.historyLimit === "number" ? String(discord.historyLimit) : "", + textChunkLimit: + typeof discord.textChunkLimit === "number" + ? String(discord.textChunkLimit) + : "", + replyToMode: + discord.replyToMode === "first" || discord.replyToMode === "all" + ? discord.replyToMode + : "off", + guilds: Array.isArray(discordGuilds) + ? [] + : typeof discordGuilds === "object" && discordGuilds + ? Object.entries(discordGuilds as Record).map( + ([key, value]): DiscordGuildForm => { + const entry = + value && typeof value === "object" + ? (value as Record) + : {}; + const channelsRaw = + entry.channels && typeof entry.channels === "object" + ? (entry.channels as Record) + : {}; + const channels = Object.entries(channelsRaw).map( + ([channelKey, channelValue]): DiscordGuildChannelForm => { + const channel = + channelValue && typeof channelValue === "object" + ? (channelValue as Record) + : {}; + return { + key: channelKey, + allow: + typeof channel.allow === "boolean" ? channel.allow : true, + requireMention: + typeof channel.requireMention === "boolean" + ? channel.requireMention + : false, + }; + }, + ); + return { + key, + slug: typeof entry.slug === "string" ? entry.slug : "", + requireMention: + typeof entry.requireMention === "boolean" + ? entry.requireMention + : false, + users: toList(entry.users), + channels, + }; + }, + ) + : [], actions: { reactions: readAction("reactions"), stickers: readAction("stickers"), diff --git a/ui/src/ui/controllers/connections.ts b/ui/src/ui/controllers/connections.ts index f056db87d..8990c1b17 100644 --- a/ui/src/ui/controllers/connections.ts +++ b/ui/src/ui/controllers/connections.ts @@ -5,6 +5,8 @@ import { defaultDiscordActions, type DiscordActionForm, type DiscordForm, + type DiscordGuildChannelForm, + type DiscordGuildForm, type IMessageForm, type SignalForm, type TelegramForm, @@ -233,6 +235,8 @@ export async function saveDiscordConfig(state: ConnectionsState) { const allowFrom = parseList(form.allowFrom); const groupChannels = parseList(form.groupChannels); const dm = { ...(discord.dm ?? {}) } as Record; + if (form.dmEnabled) delete dm.enabled; + else dm.enabled = false; if (allowFrom.length > 0) dm.allowFrom = allowFrom; else delete dm.allowFrom; if (form.groupEnabled) dm.groupEnabled = true; @@ -261,6 +265,51 @@ export async function saveDiscordConfig(state: ConnectionsState) { } } + const chunkLimitRaw = form.textChunkLimit.trim(); + if (chunkLimitRaw.length === 0) { + delete discord.textChunkLimit; + } else { + const chunkLimit = Number(chunkLimitRaw); + if (Number.isFinite(chunkLimit) && chunkLimit > 0) { + discord.textChunkLimit = chunkLimit; + } else { + delete discord.textChunkLimit; + } + } + + if (form.replyToMode === "off") { + delete discord.replyToMode; + } else { + discord.replyToMode = form.replyToMode; + } + + const guildsForm = Array.isArray(form.guilds) ? form.guilds : []; + const guilds: Record = {}; + guildsForm.forEach((guild: DiscordGuildForm) => { + const key = String(guild.key ?? "").trim(); + if (!key) return; + const entry: Record = {}; + const slug = String(guild.slug ?? "").trim(); + if (slug) entry.slug = slug; + if (guild.requireMention) entry.requireMention = true; + const users = parseList(guild.users); + if (users.length > 0) entry.users = users; + const channels: Record = {}; + const channelForms = Array.isArray(guild.channels) ? guild.channels : []; + channelForms.forEach((channel: DiscordGuildChannelForm) => { + const channelKey = String(channel.key ?? "").trim(); + if (!channelKey) return; + const channelEntry: Record = {}; + if (channel.allow === false) channelEntry.allow = false; + if (channel.requireMention) channelEntry.requireMention = true; + channels[channelKey] = channelEntry; + }); + if (Object.keys(channels).length > 0) entry.channels = channels; + guilds[key] = entry; + }); + if (Object.keys(guilds).length > 0) discord.guilds = guilds; + else delete discord.guilds; + const actions: Partial = {}; const applyAction = (key: keyof DiscordActionForm) => { const value = form.actions[key]; diff --git a/ui/src/ui/ui-types.ts b/ui/src/ui/ui-types.ts index 231b932af..8850793f6 100644 --- a/ui/src/ui/ui-types.ts +++ b/ui/src/ui/ui-types.ts @@ -11,11 +11,15 @@ export type TelegramForm = { export type DiscordForm = { enabled: boolean; token: string; + dmEnabled: boolean; allowFrom: string; groupEnabled: boolean; groupChannels: string; mediaMaxMb: string; historyLimit: string; + textChunkLimit: string; + replyToMode: "off" | "first" | "all"; + guilds: DiscordGuildForm[]; actions: DiscordActionForm; slashEnabled: boolean; slashName: string; @@ -23,6 +27,20 @@ export type DiscordForm = { slashEphemeral: boolean; }; +export type DiscordGuildForm = { + key: string; + slug: string; + requireMention: boolean; + users: string; + channels: DiscordGuildChannelForm[]; +}; + +export type DiscordGuildChannelForm = { + key: string; + allow: boolean; + requireMention: boolean; +}; + export type DiscordActionForm = { reactions: boolean; stickers: boolean; diff --git a/ui/src/ui/views/connections.ts b/ui/src/ui/views/connections.ts index 3438dbaac..1f918ace0 100644 --- a/ui/src/ui/views/connections.ts +++ b/ui/src/ui/views/connections.ts @@ -10,6 +10,24 @@ import type { TelegramForm, } from "../ui-types"; +const discordActionOptions = [ + { key: "reactions", label: "Reactions" }, + { key: "stickers", label: "Stickers" }, + { key: "polls", label: "Polls" }, + { key: "permissions", label: "Permissions" }, + { key: "messages", label: "Messages" }, + { key: "threads", label: "Threads" }, + { key: "pins", label: "Pins" }, + { key: "search", label: "Search" }, + { key: "memberInfo", label: "Member info" }, + { key: "roleInfo", label: "Role info" }, + { key: "channelInfo", label: "Channel info" }, + { key: "voiceStatus", label: "Voice status" }, + { key: "events", label: "Events" }, + { key: "roles", label: "Role changes" }, + { key: "moderation", label: "Moderation" }, +] satisfies Array<{ key: keyof DiscordActionForm; label: string }>; + export type ConnectionsProps = { connected: boolean; loading: boolean; @@ -54,24 +72,6 @@ export function renderConnections(props: ConnectionsProps) { const discord = props.snapshot?.discord ?? null; const signal = props.snapshot?.signal ?? null; const imessage = props.snapshot?.imessage ?? null; - const discordActionOptions: Array<{ key: keyof DiscordActionForm; label: string }> = - [ - { key: "reactions", label: "Reactions" }, - { key: "stickers", label: "Stickers" }, - { key: "polls", label: "Polls" }, - { key: "permissions", label: "Permissions" }, - { key: "messages", label: "Messages" }, - { key: "threads", label: "Threads" }, - { key: "pins", label: "Pins" }, - { key: "search", label: "Search" }, - { key: "memberInfo", label: "Member info" }, - { key: "roleInfo", label: "Role info" }, - { key: "channelInfo", label: "Channel info" }, - { key: "voiceStatus", label: "Voice status" }, - { key: "events", label: "Events" }, - { key: "roles", label: "Role changes" }, - { key: "moderation", label: "Moderation" }, - ]; const providerOrder: ProviderKey[] = [ "whatsapp", "telegram", @@ -500,6 +500,19 @@ function renderProvider( placeholder="123456789, username#1234" /> + + +
+ Guilds +
+ Add each guild (id or slug) and optional channel rules. Empty channel + entries still allow that channel. +
+
+ ${props.discordForm.guilds.map( + (guild, guildIndex) => html` +
+
+
+ + + + +
+ ${guild.channels.length + ? html` +
+ ${guild.channels.map( + (channel, channelIndex) => html` + + + + + `, + )} +
+ ` + : nothing} +
+
+ Channels + + +
+
+ `, + )} +
+ +