Discord: update UIs to use the new config

This commit is contained in:
Shadow
2026-01-03 01:02:18 -06:00
parent 14ee2b2c11
commit 7400c0946e
11 changed files with 682 additions and 28 deletions

1
.gitignore vendored
View File

@@ -30,6 +30,7 @@ vendor/
# Vendor build artifacts
vendor/a2ui/renderers/lit/dist/
.bundle.hash
# fastlane (iOS)
apps/ios/fastlane/README.md

View File

@@ -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)

View File

@@ -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 {

View File

@@ -1 +1 @@
debc115fd2b264d3b3090e28b733551ca5f7532287d827c13fc579e5a7a95b9d
549aa1fff40f95b4a899940ad2cde0adc3991423136ba02c23bcdbd621765ad7

View File

@@ -210,6 +210,10 @@
gap: 6px;
}
.field.full {
grid-column: 1 / -1;
}
.field span {
color: var(--muted);
font-size: 11px;

View File

@@ -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;
}

View File

@@ -14,7 +14,7 @@ export type ChatState = {
lastError: string | null;
};
type ChatEventPayload = {
export type ChatEventPayload = {
runId: string;
sessionKey: string;
state: "delta" | "final" | "aborted" | "error";

View File

@@ -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<string, unknown>;
const slash = (discord.slashCommand ?? {}) as Record<string, unknown>;
const discordActions = (discord.actions ?? {}) as Record<string, unknown>;
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<string, unknown>).map(
([key, value]): DiscordGuildForm => {
const entry =
value && typeof value === "object"
? (value as Record<string, unknown>)
: {};
const channelsRaw =
entry.channels && typeof entry.channels === "object"
? (entry.channels as Record<string, unknown>)
: {};
const channels = Object.entries(channelsRaw).map(
([channelKey, channelValue]): DiscordGuildChannelForm => {
const channel =
channelValue && typeof channelValue === "object"
? (channelValue as Record<string, unknown>)
: {};
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"),

View File

@@ -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<string, unknown>;
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<string, unknown> = {};
guildsForm.forEach((guild: DiscordGuildForm) => {
const key = String(guild.key ?? "").trim();
if (!key) return;
const entry: Record<string, unknown> = {};
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<string, unknown> = {};
const channelForms = Array.isArray(guild.channels) ? guild.channels : [];
channelForms.forEach((channel: DiscordGuildChannelForm) => {
const channelKey = String(channel.key ?? "").trim();
if (!channelKey) return;
const channelEntry: Record<string, unknown> = {};
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<DiscordActionForm> = {};
const applyAction = (key: keyof DiscordActionForm) => {
const value = form.actions[key];

View File

@@ -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;

View File

@@ -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"
/>
</label>
<label class="field">
<span>DMs enabled</span>
<select
.value=${props.discordForm.dmEnabled ? "yes" : "no"}
@change=${(e: Event) =>
props.onDiscordChange({
dmEnabled: (e.target as HTMLSelectElement).value === "yes",
})}
>
<option value="yes">Enabled</option>
<option value="no">Disabled</option>
</select>
</label>
<label class="field">
<span>Group DMs</span>
<select
@@ -546,6 +559,268 @@ function renderProvider(
placeholder="20"
/>
</label>
<label class="field">
<span>Text chunk limit</span>
<input
.value=${props.discordForm.textChunkLimit}
@input=${(e: Event) =>
props.onDiscordChange({
textChunkLimit: (e.target as HTMLInputElement).value,
})}
placeholder="2000"
/>
</label>
<label class="field">
<span>Reply to mode</span>
<select
.value=${props.discordForm.replyToMode}
@change=${(e: Event) =>
props.onDiscordChange({
replyToMode: (e.target as HTMLSelectElement).value as
| "off"
| "first"
| "all",
})}
>
<option value="off">Off</option>
<option value="first">First</option>
<option value="all">All</option>
</select>
</label>
<div class="field full">
<span>Guilds</span>
<div class="card-sub">
Add each guild (id or slug) and optional channel rules. Empty channel
entries still allow that channel.
</div>
<div class="list">
${props.discordForm.guilds.map(
(guild, guildIndex) => html`
<div class="list-item">
<div class="list-main">
<div class="form-grid">
<label class="field">
<span>Guild id / slug</span>
<input
.value=${guild.key}
@input=${(e: Event) => {
const next = [...props.discordForm.guilds];
next[guildIndex] = {
...next[guildIndex],
key: (e.target as HTMLInputElement).value,
};
props.onDiscordChange({ guilds: next });
}}
/>
</label>
<label class="field">
<span>Slug</span>
<input
.value=${guild.slug}
@input=${(e: Event) => {
const next = [...props.discordForm.guilds];
next[guildIndex] = {
...next[guildIndex],
slug: (e.target as HTMLInputElement).value,
};
props.onDiscordChange({ guilds: next });
}}
/>
</label>
<label class="field">
<span>Require mention</span>
<select
.value=${guild.requireMention ? "yes" : "no"}
@change=${(e: Event) => {
const next = [...props.discordForm.guilds];
next[guildIndex] = {
...next[guildIndex],
requireMention:
(e.target as HTMLSelectElement).value === "yes",
};
props.onDiscordChange({ guilds: next });
}}
>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</label>
<label class="field">
<span>Users allowlist</span>
<input
.value=${guild.users}
@input=${(e: Event) => {
const next = [...props.discordForm.guilds];
next[guildIndex] = {
...next[guildIndex],
users: (e.target as HTMLInputElement).value,
};
props.onDiscordChange({ guilds: next });
}}
placeholder="123456789, username#1234"
/>
</label>
</div>
${guild.channels.length
? html`
<div class="form-grid" style="margin-top: 8px;">
${guild.channels.map(
(channel, channelIndex) => html`
<label class="field">
<span>Channel id / slug</span>
<input
.value=${channel.key}
@input=${(e: Event) => {
const next = [...props.discordForm.guilds];
const channels = [
...(next[guildIndex].channels ?? []),
];
channels[channelIndex] = {
...channels[channelIndex],
key: (e.target as HTMLInputElement).value,
};
next[guildIndex] = {
...next[guildIndex],
channels,
};
props.onDiscordChange({ guilds: next });
}}
/>
</label>
<label class="field">
<span>Allow</span>
<select
.value=${channel.allow ? "yes" : "no"}
@change=${(e: Event) => {
const next = [...props.discordForm.guilds];
const channels = [
...(next[guildIndex].channels ?? []),
];
channels[channelIndex] = {
...channels[channelIndex],
allow:
(e.target as HTMLSelectElement).value ===
"yes",
};
next[guildIndex] = {
...next[guildIndex],
channels,
};
props.onDiscordChange({ guilds: next });
}}
>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</label>
<label class="field">
<span>Require mention</span>
<select
.value=${channel.requireMention ? "yes" : "no"}
@change=${(e: Event) => {
const next = [...props.discordForm.guilds];
const channels = [
...(next[guildIndex].channels ?? []),
];
channels[channelIndex] = {
...channels[channelIndex],
requireMention:
(e.target as HTMLSelectElement).value ===
"yes",
};
next[guildIndex] = {
...next[guildIndex],
channels,
};
props.onDiscordChange({ guilds: next });
}}
>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</label>
<label class="field">
<span>&nbsp;</span>
<button
class="btn"
@click=${() => {
const next = [
...props.discordForm.guilds,
];
const channels = [
...(next[guildIndex].channels ?? []),
];
channels.splice(channelIndex, 1);
next[guildIndex] = {
...next[guildIndex],
channels,
};
props.onDiscordChange({ guilds: next });
}}
>
Remove
</button>
</label>
`,
)}
</div>
`
: nothing}
</div>
<div class="list-meta">
<span>Channels</span>
<button
class="btn"
@click=${() => {
const next = [...props.discordForm.guilds];
const channels = [
...(next[guildIndex].channels ?? []),
{ key: "", allow: true, requireMention: false },
];
next[guildIndex] = {
...next[guildIndex],
channels,
};
props.onDiscordChange({ guilds: next });
}}
>
Add channel
</button>
<button
class="btn danger"
@click=${() => {
const next = [...props.discordForm.guilds];
next.splice(guildIndex, 1);
props.onDiscordChange({ guilds: next });
}}
>
Remove guild
</button>
</div>
</div>
`,
)}
</div>
<button
class="btn"
style="margin-top: 8px;"
@click=${() =>
props.onDiscordChange({
guilds: [
...props.discordForm.guilds,
{
key: "",
slug: "",
requireMention: false,
users: "",
channels: [],
},
],
})}
>
Add guild
</button>
</div>
<label class="field">
<span>Slash command</span>
<select