add leave system, add cockroach on players that leave
This commit is contained in:
parent
347d1423fc
commit
fd1b8ed50c
7 changed files with 253 additions and 14 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -16,6 +16,7 @@ data/usermap.json
|
||||||
data/wrank.json
|
data/wrank.json
|
||||||
data/bringer.json
|
data/bringer.json
|
||||||
data/attendance.json
|
data/attendance.json
|
||||||
|
data/leaves.json
|
||||||
data/sessionPreferences.json
|
data/sessionPreferences.json
|
||||||
data/tg-history/
|
data/tg-history/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ import { handleCharSetNation } from "@subcommands/char/setNation";
|
||||||
import { handleCharSetStats } from "@subcommands/char/setStats";
|
import { handleCharSetStats } from "@subcommands/char/setStats";
|
||||||
import { handleCharActive } from "@subcommands/char/active";
|
import { handleCharActive } from "@subcommands/char/active";
|
||||||
import { Nation } from "@types";
|
import { Nation } from "@types";
|
||||||
|
import { handleMarkLeft, handleUnmarkLeft } from "@subcommands/poll/mark-left";
|
||||||
|
|
||||||
export function buildTgCommand(): SlashCommandBuilder {
|
export function buildTgCommand(): SlashCommandBuilder {
|
||||||
const cmd = new SlashCommandBuilder()
|
const cmd = new SlashCommandBuilder()
|
||||||
|
|
@ -127,6 +128,16 @@ export function buildTgCommand(): SlashCommandBuilder {
|
||||||
)
|
)
|
||||||
.addSubcommand((s) => s.setName("purge").setDescription("Delete all bot messages from the poll channel"))
|
.addSubcommand((s) => s.setName("purge").setDescription("Delete all bot messages from the poll channel"))
|
||||||
.addSubcommand((s) => s.setName("seed").setDescription("Inject all registered players as Yes votes for layout testing"))
|
.addSubcommand((s) => s.setName("seed").setDescription("Inject all registered players as Yes votes for layout testing"))
|
||||||
|
.addSubcommand((s) => s
|
||||||
|
.setName("mark-left")
|
||||||
|
.setDescription("Mark a character as having left TG")
|
||||||
|
.addStringOption((o) => o.setName("char_name").setDescription("Character name").setRequired(true).setAutocomplete(true))
|
||||||
|
)
|
||||||
|
.addSubcommand((s) => s
|
||||||
|
.setName("unmark-left")
|
||||||
|
.setDescription("Remove left mark from a character")
|
||||||
|
.addStringOption((o) => o.setName("char_name").setDescription("Character name").setRequired(true).setAutocomplete(true))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── score group ────────────────────────────────────────────────────────────
|
// ── score group ────────────────────────────────────────────────────────────
|
||||||
|
|
@ -314,6 +325,8 @@ export async function handleTgCommand(interaction: ChatInputCommandInteraction):
|
||||||
if (sub === "remove-vote") return handleRemoveVote(interaction);
|
if (sub === "remove-vote") return handleRemoveVote(interaction);
|
||||||
if (sub === "purge") return handlePurge(interaction);
|
if (sub === "purge") return handlePurge(interaction);
|
||||||
if (sub === "seed") return handleSeed(interaction);
|
if (sub === "seed") return handleSeed(interaction);
|
||||||
|
if (sub === "mark-left") return handleMarkLeft(interaction);
|
||||||
|
if (sub === "unmark-left") return handleUnmarkLeft(interaction);
|
||||||
}
|
}
|
||||||
if (group === "score") {
|
if (group === "score") {
|
||||||
if (sub === "set") return handleScoreSet(interaction);
|
if (sub === "set") return handleScoreSet(interaction);
|
||||||
|
|
|
||||||
69
src/subcommands/poll/mark-left.ts
Normal file
69
src/subcommands/poll/mark-left.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { ChatInputCommandInteraction, TextChannel } from "discord.js";
|
||||||
|
import { Config } from "@systems/config";
|
||||||
|
import { resolveUser, hasOfficerRole } from "@systems/users";
|
||||||
|
import { Leaves } from "@systems/leaves";
|
||||||
|
import { polls, updatePollMessage } from "@systems/poll";
|
||||||
|
import { CharacterRegistry } from "@registry/character-registry";
|
||||||
|
import { replyAndDelete } from "@utils";
|
||||||
|
|
||||||
|
function getCurrentHistoryKey(slot: number): string {
|
||||||
|
const date = new Date().toISOString().slice(0, 10);
|
||||||
|
return `${date}-${slot}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleMarkLeft(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
|
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) {
|
||||||
|
return void replyAndDelete(interaction, "❌ Only officers can mark characters as left.", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const charName = interaction.options.getString("char_name", true);
|
||||||
|
const officer = await resolveUser(member);
|
||||||
|
|
||||||
|
// Find character and its owner
|
||||||
|
const char = CharacterRegistry.find(charName);
|
||||||
|
if (!char) return void replyAndDelete(interaction, `❌ Character **${charName}** not found.`, true);
|
||||||
|
|
||||||
|
const slot = [...polls.keys()][0];
|
||||||
|
if (slot === undefined) return void replyAndDelete(interaction, "❌ No active poll.", true);
|
||||||
|
|
||||||
|
const historyKey = getCurrentHistoryKey(slot);
|
||||||
|
|
||||||
|
Leaves.mark({
|
||||||
|
characterName: char.name,
|
||||||
|
ownerKey: char.ownerKey,
|
||||||
|
historyKey,
|
||||||
|
markedBy: officer.userKey ?? "unknown",
|
||||||
|
});
|
||||||
|
|
||||||
|
const channel = interaction.channel as TextChannel;
|
||||||
|
await updatePollMessage(channel, slot);
|
||||||
|
|
||||||
|
const count = Leaves.countForChar({ characterName: char.name });
|
||||||
|
return void replyAndDelete(interaction,
|
||||||
|
`🪲 **${char.name}** marked as left (total leaves: ${count}).`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleUnmarkLeft(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
|
if (!hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }))) {
|
||||||
|
return void replyAndDelete(interaction, "❌ Only officers can unmark characters.", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const charName = interaction.options.getString("char_name", true);
|
||||||
|
const char = CharacterRegistry.find(charName);
|
||||||
|
if (!char) return void replyAndDelete(interaction, `❌ Character **${charName}** not found.`, true);
|
||||||
|
|
||||||
|
const slot = [...polls.keys()][0];
|
||||||
|
if (slot === undefined) return void replyAndDelete(interaction, "❌ No active poll.", true);
|
||||||
|
|
||||||
|
const historyKey = getCurrentHistoryKey(slot);
|
||||||
|
Leaves.unmark({ characterName: char.name, historyKey });
|
||||||
|
|
||||||
|
const channel = interaction.channel as TextChannel;
|
||||||
|
await updatePollMessage(channel, slot);
|
||||||
|
|
||||||
|
return void replyAndDelete(interaction, `✅ Removed left mark from **${char.name}**.`, true);
|
||||||
|
}
|
||||||
132
src/systems/leaves.ts
Normal file
132
src/systems/leaves.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
/**
|
||||||
|
* Leaves — tracks characters who left TG mid-game.
|
||||||
|
*
|
||||||
|
* Keyed by characterName — leaving is a character action, not a user action.
|
||||||
|
* User-level stats are derived by aggregating across all characters owned by a user.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { Leaves } from "@systems/leaves";
|
||||||
|
*
|
||||||
|
* Leaves.mark({ characterName, historyKey, markedBy })
|
||||||
|
* Leaves.unmark({ characterName, historyKey })
|
||||||
|
* Leaves.hasLeft({ characterName, historyKey })
|
||||||
|
* Leaves.countForChar({ characterName })
|
||||||
|
* Leaves.countForUser({ userKey })
|
||||||
|
* Leaves.formatIndicator({ characterName })
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { UserKey, CharName, HistoryKey } from "@types";
|
||||||
|
import { Store } from "@systems/store";
|
||||||
|
import { Paths } from "@paths";
|
||||||
|
import { Emoji } from "@systems/emojis";
|
||||||
|
import { Runtime } from "@systems/runtime";
|
||||||
|
|
||||||
|
Runtime.phase("load", () => Leaves.load(), { name: "Leaves.load" });
|
||||||
|
|
||||||
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface LeaveRecord {
|
||||||
|
historyKey: HistoryKey;
|
||||||
|
markedBy: UserKey;
|
||||||
|
markedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CharacterLeaves {
|
||||||
|
characterName: CharName;
|
||||||
|
ownerKey: UserKey;
|
||||||
|
count: number;
|
||||||
|
history: LeaveRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LeavesData {
|
||||||
|
[characterName: CharName]: CharacterLeaves;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── State ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let _data: LeavesData = {};
|
||||||
|
|
||||||
|
// ─── Namespace ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const Leaves = {
|
||||||
|
load(): void {
|
||||||
|
_data = Store.readOrDefault<LeavesData>(Paths.data("leaves.json"), {});
|
||||||
|
},
|
||||||
|
|
||||||
|
save(): void {
|
||||||
|
Store.write(Paths.data("leaves.json"), _data);
|
||||||
|
},
|
||||||
|
|
||||||
|
mark({ characterName, ownerKey, historyKey, markedBy }: {
|
||||||
|
characterName: CharName;
|
||||||
|
ownerKey: UserKey;
|
||||||
|
historyKey: HistoryKey;
|
||||||
|
markedBy: UserKey;
|
||||||
|
}): void {
|
||||||
|
if (!_data[characterName]) {
|
||||||
|
_data[characterName] = { characterName, ownerKey, count: 0, history: [] };
|
||||||
|
}
|
||||||
|
const char = _data[characterName];
|
||||||
|
if (char.history.some((r) => r.historyKey === historyKey)) return; // already marked
|
||||||
|
char.history.push({ historyKey, markedBy, markedAt: new Date().toISOString() });
|
||||||
|
char.count = char.history.length;
|
||||||
|
Leaves.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
unmark({ characterName, historyKey }: {
|
||||||
|
characterName: CharName;
|
||||||
|
historyKey: HistoryKey;
|
||||||
|
}): void {
|
||||||
|
if (!_data[characterName]) return;
|
||||||
|
_data[characterName].history = _data[characterName].history.filter(
|
||||||
|
(r) => r.historyKey !== historyKey
|
||||||
|
);
|
||||||
|
_data[characterName].count = _data[characterName].history.length;
|
||||||
|
Leaves.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
hasLeft({ characterName, historyKey }: {
|
||||||
|
characterName: CharName;
|
||||||
|
historyKey: HistoryKey;
|
||||||
|
}): boolean {
|
||||||
|
return _data[characterName]?.history.some((r) => r.historyKey === historyKey) ?? false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Total leaves for a specific character */
|
||||||
|
countForChar({ characterName }: { characterName: CharName }): number {
|
||||||
|
return _data[characterName]?.count ?? 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Total leaves across all characters owned by a user */
|
||||||
|
countForUser({ userKey }: { userKey: UserKey }): number {
|
||||||
|
return Object.values(_data)
|
||||||
|
.filter((c) => c.ownerKey === userKey)
|
||||||
|
.reduce((sum, c) => sum + c.count, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
historyForChar({ characterName }: { characterName: CharName }): LeaveRecord[] {
|
||||||
|
return _data[characterName]?.history ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
|
historyForUser({ userKey }: { userKey: UserKey }): { characterName: CharName; record: LeaveRecord }[] {
|
||||||
|
const result: { characterName: CharName; record: LeaveRecord }[] = [];
|
||||||
|
for (const [charName, data] of Object.entries(_data)) {
|
||||||
|
if (data.ownerKey !== userKey) continue;
|
||||||
|
for (const record of data.history) {
|
||||||
|
result.push({ characterName: charName, record });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.sort((a, b) => a.record.markedAt.localeCompare(b.record.markedAt));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the leave indicator for display in the poll.
|
||||||
|
* Output: <:cockroach:> <:wrank_3:> (count = total times this character left)
|
||||||
|
*/
|
||||||
|
formatIndicator({ characterName }: { characterName: CharName }): string {
|
||||||
|
const count = Leaves.countForChar({ characterName });
|
||||||
|
const cockroach = Emoji.get("cockroach") || "🪳"; //"🪲";
|
||||||
|
const countEmoji = Emoji.get(`wrank_${count}`) || `${count}`;
|
||||||
|
return `${cockroach}${countEmoji}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
import { Emoji } from "@systems/emojis";
|
import { Emoji } from "@systems/emojis";
|
||||||
import { format } from "@format";
|
import { format } from "@format";
|
||||||
import { PollLayout, PollRowContext, PollEmbedOptions } from "@ui/types";
|
import { PollLayout, PollRowContext, PollEmbedOptions } from "@ui/types";
|
||||||
|
import { Leaves } from "@systems/leaves";
|
||||||
|
|
||||||
// ─── Row formatting ───────────────────────────────────────────────────────────
|
// ─── Row formatting ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -56,6 +57,12 @@
|
||||||
row += ` · ${format.bringer(nation)}`;
|
row += ` · ${format.bringer(nation)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry.userKey && context.historyKey) {
|
||||||
|
if (Leaves.hasLeft({ userKey: entry.userKey, historyKey: context.historyKey })) {
|
||||||
|
row += ` ${Leaves.formatIndicator({ userKey: entry.userKey })}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.borrowedFrom) row += ` ${Emoji.get("borrowed") || "🔗"}`;
|
if (entry.borrowedFrom) row += ` ${Emoji.get("borrowed") || "🔗"}`;
|
||||||
if (context.showNationEmoji && nation) row = `${Emoji.nation(nation)} ${row}`;
|
if (context.showNationEmoji && nation) row = `${Emoji.nation(nation)} ${row}`;
|
||||||
|
|
@ -66,7 +73,7 @@
|
||||||
function buildContext(
|
function buildContext(
|
||||||
entries: VoteEntry[],
|
entries: VoteEntry[],
|
||||||
nation: Nation,
|
nation: Nation,
|
||||||
options?: { showNationEmoji?: boolean }
|
options?: { showNationEmoji?: boolean; historyKey?: string }
|
||||||
): PollRowContext {
|
): PollRowContext {
|
||||||
const nationHasRank = entries.some((e) =>
|
const nationHasRank = entries.some((e) =>
|
||||||
e.characterName && WRank.entry(e.characterName, nation) !== null
|
e.characterName && WRank.entry(e.characterName, nation) !== null
|
||||||
|
|
@ -76,10 +83,11 @@
|
||||||
return wr?.previousRank !== undefined;
|
return wr?.previousRank !== undefined;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
nationHasRank,
|
nationHasRank,
|
||||||
nationHasDelta,
|
nationHasDelta,
|
||||||
showNationEmoji: options?.showNationEmoji ?? false,
|
showNationEmoji: options?.showNationEmoji ?? false,
|
||||||
};
|
historyKey: options?.historyKey,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Embed building ───────────────────────────────────────────────────────────
|
// ─── Embed building ───────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
import { Emoji } from "@systems/emojis";
|
import { Emoji } from "@systems/emojis";
|
||||||
import { format } from "@format";
|
import { format } from "@format";
|
||||||
import { PollLayout, PollRowContext, PollEmbedOptions } from "@ui/types";
|
import { PollLayout, PollRowContext, PollEmbedOptions } from "@ui/types";
|
||||||
|
import { Leaves } from "@systems/leaves";
|
||||||
|
|
||||||
// ─── Row formatting (same as default) ────────────────────────────────────────
|
// ─── Row formatting (same as default) ────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -49,6 +50,12 @@
|
||||||
row += ` · ${format.bringer(nation)}`;
|
row += ` · ${format.bringer(nation)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry.userKey && entry.characterName && context.historyKey) {
|
||||||
|
if (Leaves.hasLeft({ characterName: entry.characterName, historyKey: context.historyKey })) {
|
||||||
|
row += ` ${Leaves.formatIndicator({ characterName: entry.characterName })}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.borrowedFrom) row += ` ${Emoji.get("borrowed") || "🔗"}`;
|
if (entry.borrowedFrom) row += ` ${Emoji.get("borrowed") || "🔗"}`;
|
||||||
if (context.showNationEmoji && nation) row = `${Emoji.nation(nation)} ${row}`;
|
if (context.showNationEmoji && nation) row = `${Emoji.nation(nation)} ${row}`;
|
||||||
|
|
@ -58,14 +65,19 @@
|
||||||
function buildContext(
|
function buildContext(
|
||||||
entries: VoteEntry[],
|
entries: VoteEntry[],
|
||||||
nation: Nation,
|
nation: Nation,
|
||||||
options?: { showNationEmoji?: boolean }
|
options?: { showNationEmoji?: boolean; historyKey?: string }
|
||||||
): PollRowContext {
|
): PollRowContext {
|
||||||
const nationHasRank = entries.some((e) => e.characterName && WRank.entry(e.characterName, nation) !== null);
|
const nationHasRank = entries.some((e) => e.characterName && WRank.entry(e.characterName, nation) !== null);
|
||||||
const nationHasDelta = entries.some((e) => {
|
const nationHasDelta = entries.some((e) => {
|
||||||
const wr = e.characterName ? WRank.entry(e.characterName, nation) : null;
|
const wr = e.characterName ? WRank.entry(e.characterName, nation) : null;
|
||||||
return wr?.previousRank !== undefined;
|
return wr?.previousRank !== undefined;
|
||||||
});
|
});
|
||||||
return { nationHasRank, nationHasDelta, showNationEmoji: options?.showNationEmoji ?? false };
|
return {
|
||||||
|
nationHasRank,
|
||||||
|
nationHasDelta,
|
||||||
|
showNationEmoji: options?.showNationEmoji ?? false,
|
||||||
|
historyKey: options?.historyKey,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Embed building ───────────────────────────────────────────────────────────
|
// ─── Embed building ───────────────────────────────────────────────────────────
|
||||||
|
|
@ -74,9 +86,10 @@
|
||||||
nation: Nation,
|
nation: Nation,
|
||||||
yesEntries: VoteEntry[],
|
yesEntries: VoteEntry[],
|
||||||
noVoters: VoteEntry[],
|
noVoters: VoteEntry[],
|
||||||
showNoInline: boolean
|
showNoInline: boolean,
|
||||||
|
historyKey?: string
|
||||||
): string {
|
): string {
|
||||||
const context = buildContext(yesEntries, nation);
|
const context = buildContext(yesEntries, nation, { historyKey });
|
||||||
const noEntries = showNoInline ? noVoters.filter((e) => e.characterNation === nation) : [];
|
const noEntries = showNoInline ? noVoters.filter((e) => e.characterNation === nation) : [];
|
||||||
const lines = [
|
const lines = [
|
||||||
...yesEntries.map((e) => formatRow(e, context)),
|
...yesEntries.map((e) => formatRow(e, context)),
|
||||||
|
|
@ -119,6 +132,8 @@
|
||||||
const capellaEmoji = Emoji.get("capella");
|
const capellaEmoji = Emoji.get("capella");
|
||||||
const procyonEmoji = Emoji.get("procyon");
|
const procyonEmoji = Emoji.get("procyon");
|
||||||
|
|
||||||
|
const historyKey = `${new Date().toISOString().slice(0, 10)}-${state.slot}`;
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
const counts = !state.locked && state.confirmed === null
|
const counts = !state.locked && state.confirmed === null
|
||||||
? ` ${capellaEmoji} ${yesByNation[Nation.Capella].length} ${procyonEmoji} ${yesByNation[Nation.Procyon].length}`
|
? ` ${capellaEmoji} ${yesByNation[Nation.Capella].length} ${procyonEmoji} ${yesByNation[Nation.Procyon].length}`
|
||||||
|
|
@ -141,12 +156,12 @@
|
||||||
// ← inline: true makes them side by side
|
// ← inline: true makes them side by side
|
||||||
{
|
{
|
||||||
name: `${capellaEmoji} Capella (${yesByNation[Nation.Capella].length})`,
|
name: `${capellaEmoji} Capella (${yesByNation[Nation.Capella].length})`,
|
||||||
value: formatNationField(Nation.Capella, yesByNation[Nation.Capella], noVoters, showNoInline),
|
value: formatNationField(Nation.Capella, yesByNation[Nation.Capella], noVoters, showNoInline, historyKey),
|
||||||
inline: true,
|
inline: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `${procyonEmoji} Procyon (${yesByNation[Nation.Procyon].length})`,
|
name: `${procyonEmoji} Procyon (${yesByNation[Nation.Procyon].length})`,
|
||||||
value: formatNationField(Nation.Procyon, yesByNation[Nation.Procyon], noVoters, showNoInline),
|
value: formatNationField(Nation.Procyon, yesByNation[Nation.Procyon], noVoters, showNoInline, historyKey),
|
||||||
inline: true,
|
inline: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@
|
||||||
// ─── Poll ─────────────────────────────────────────────────────────────────────
|
// ─── Poll ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface PollRowContext {
|
export interface PollRowContext {
|
||||||
nationHasRank: boolean;
|
nationHasRank: boolean;
|
||||||
nationHasDelta: boolean;
|
nationHasDelta: boolean;
|
||||||
showNationEmoji?: boolean;
|
showNationEmoji?: boolean;
|
||||||
|
historyKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PollEmbedOptions {
|
export interface PollEmbedOptions {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue