Discord: update UIs to use the new config
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,6 +30,7 @@ vendor/
|
||||
|
||||
# Vendor build artifacts
|
||||
vendor/a2ui/renderers/lit/dist/
|
||||
.bundle.hash
|
||||
|
||||
# fastlane (iOS)
|
||||
apps/ios/fastlane/README.md
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1 +1 @@
|
||||
debc115fd2b264d3b3090e28b733551ca5f7532287d827c13fc579e5a7a95b9d
|
||||
549aa1fff40f95b4a899940ad2cde0adc3991423136ba02c23bcdbd621765ad7
|
||||
|
||||
@@ -210,6 +210,10 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.field.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.field span {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export type ChatState = {
|
||||
lastError: string | null;
|
||||
};
|
||||
|
||||
type ChatEventPayload = {
|
||||
export type ChatEventPayload = {
|
||||
runId: string;
|
||||
sessionKey: string;
|
||||
state: "delta" | "final" | "aborted" | "error";
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> </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
|
||||
|
||||
Reference in New Issue
Block a user