Emojis zum Anklicken, und Wuensche als Forums-Beitraege mit Tags
Ueberall, wo ein Emoji nach Discord geht — Rollen-Menues, Ticket-Anliegen,
Wunsch-Bereiche, Portal-Kacheln — stand ein leeres Textfeld, in das man mit
Win+. hineintippen musste. Jetzt sitzt daneben ein Knopf mit einer Tafel:
sieben Gruppen, deutsche Suchbegriffe ("auto", "geld", "warnung"), ein Klick
schreibt ins Feld. Das Feld bleibt daneben stehen, denn Server-Emojis
(<:name:123>) kennt nur Discord.
Bewusst eine eigene Liste statt einer Bibliothek: die vollstaendige
Unicode-Tabelle waere ein halbes Megabyte fuer eine Handvoll Symbole, und die
Suchbegriffe duerfen deutsch sein.
## Forum-Tags
Ist der Voting-Kanal ein Forum, wird jeder Wunsch jetzt ein Forums-Beitrag
statt Nachricht-plus-Thread — und bekommt Tags fuer Bereich und Stand. Damit
laesst sich auch in Discord filtern, wofuer Tags ja da sind. Beim Wechsel des
Stands zieht das Tag mit: der alte fliegt raus, der neue kommt rein, der
Bereich bleibt stehen.
Zugeordnet wird ueber den Namen. Wer im Forum ein Tag "Fahrzeuge" anlegt,
bekommt es automatisch gesetzt; wer keines anlegt, verliert nichts. Bewusst
keine ID-Zuordnung im Panel — das waere eine zweite Liste, die man pflegen
muss und die beim ersten Umbenennen auseinanderlaeuft.
Dabei aufgefallen und mitgefixt: im Forum liegt die Startnachricht im Beitrag
selbst und nicht im Kanal. Der Status-Aktualisierer hat sie vorher im Kanal
gesucht und nicht gefunden — die Begruendung waere im Forum also nie im Post
gelandet. Und der Titel wird jetzt nur um den Stand ergaenzt statt
ueberschrieben, damit "🚗 Fahrzeuge" stehen bleibt.
Geprueft: tagsFuer gegen 20 Faelle — kein Forum, Medien-Kanal, fehlende Tags,
Gross/Kleinschreibung, Deckel bei fuenf (mehr nimmt Discord nicht), und alle
fuenf Staende einzeln. Im Browser die Emoji-Tafel an allen vier Stellen:
Suche, Escape, Klick daneben, Leeren, Uebernehmen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,16 @@ export async function execute(interaction) {
|
||||
.setFooter({ text: brandFooter('VOTING') });
|
||||
|
||||
const anzahl = listWishCategories().length;
|
||||
// In einem Forum-Kanal selbst kann man nichts posten — der Aufruf gehört
|
||||
// in einen normalen Kanal, die Wünsche landen trotzdem im Forum
|
||||
if (!interaction.channel?.send) {
|
||||
await interaction.reply({
|
||||
content: '❌ Hier lässt sich nichts posten. Führ den Befehl in einem normalen Textkanal aus — '
|
||||
+ 'die Ideen landen trotzdem im eingestellten Voting-Kanal.',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await interaction.channel.send({ embeds: [embed], components: wunschPanelComponents() });
|
||||
await interaction.reply({
|
||||
content: anzahl > 0
|
||||
|
||||
+85
-3
@@ -10,7 +10,7 @@
|
||||
// Unterhaltung nur zerreißen. Die Roadmap zeigt die Zahl der Beiträge und
|
||||
// verlinkt hinein.
|
||||
import {
|
||||
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, Events,
|
||||
ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, EmbedBuilder, Events,
|
||||
ModalBuilder, MessageFlags, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle,
|
||||
} from 'discord.js';
|
||||
import {
|
||||
@@ -21,6 +21,22 @@ import { renderTemplate } from '../templates.js';
|
||||
import { tuning } from '../tuning.js';
|
||||
import { alsEmoji } from './tickets.js';
|
||||
|
||||
/**
|
||||
* Der Stand eines Wunsches, wie er nach außen heißt. Steht hier und nicht in
|
||||
* der API, weil ihn beides braucht: das Panel zum Anzeigen und der Bot für
|
||||
* Embed-Titel und Forum-Tags.
|
||||
*/
|
||||
export const STATUS_LABEL = {
|
||||
offen: 'Wird geprüft',
|
||||
geplant: 'Geplant',
|
||||
in_arbeit: 'In Arbeit',
|
||||
umgesetzt: 'Umgesetzt',
|
||||
abgelehnt: 'Nicht geplant',
|
||||
};
|
||||
export const STATUS_EMOJI = {
|
||||
offen: '💡', geplant: '📌', in_arbeit: '🔨', umgesetzt: '✅', abgelehnt: '🚫',
|
||||
};
|
||||
|
||||
/** Knopf oder Auswahlmenü für den Aufruf-Post — je nachdem, ob es Bereiche gibt */
|
||||
export function wunschPanelComponents() {
|
||||
const bereiche = listWishCategories();
|
||||
@@ -67,15 +83,41 @@ export function wunschModal(categoryId = null) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Ist der Voting-Kanal ein Forum? Dann wird jeder Wunsch ein Forums-Beitrag. */
|
||||
const istForum = (channel) =>
|
||||
channel?.type === ChannelType.GuildForum || channel?.type === ChannelType.GuildMedia;
|
||||
|
||||
/**
|
||||
* Passende Forum-Tags zu Bereich und Stand heraussuchen — über den Namen.
|
||||
* Wer im Forum ein Tag „Fahrzeuge" anlegt, bekommt es automatisch gesetzt;
|
||||
* wer keines anlegt, verliert nichts. Deshalb bewusst keine ID-Zuordnung im
|
||||
* Panel: das wäre eine zweite Liste, die man pflegen muss.
|
||||
*/
|
||||
export function tagsFuer(channel, { bereich, statusLabel }) {
|
||||
if (!istForum(channel)) return [];
|
||||
const gesucht = [bereich?.name, statusLabel].filter(Boolean).map((s) => s.toLowerCase());
|
||||
return (channel.availableTags ?? [])
|
||||
.filter((t) => gesucht.includes(t.name.toLowerCase()))
|
||||
.map((t) => t.id)
|
||||
.slice(0, 5); // Discord erlaubt höchstens fünf je Beitrag
|
||||
}
|
||||
|
||||
/**
|
||||
* Wunsch posten, speichern und den Thread aufmachen.
|
||||
*
|
||||
* Im Forum ist der Beitrag selbst schon der Thread — dort wird er mit Tags für
|
||||
* Bereich und Stand angelegt, damit man auch in Discord filtern kann. In einem
|
||||
* normalen Textkanal bleibt es bei Nachricht + Thread darunter.
|
||||
*
|
||||
* @returns {Promise<{id: number, url: string}>}
|
||||
*/
|
||||
export async function wunschAnlegen(client, { idee, warum, autor, autorId, avatar, categoryId }) {
|
||||
const channelId = votingChannelId();
|
||||
if (!channelId) throw new Error('Feature-Voting ist gerade nicht aktiviert.');
|
||||
const channel = await client.channels.fetch(channelId);
|
||||
if (!channel?.isTextBased()) throw new Error('Voting-Kanal nicht gefunden.');
|
||||
if (!channel?.isTextBased() && !istForum(channel)) {
|
||||
throw new Error('Voting-Kanal nicht gefunden.');
|
||||
}
|
||||
|
||||
const bereich = categoryId ? getWishCategory(categoryId) : null;
|
||||
const kopf = bereich ? `${bereich.emoji || '💡'} ${bereich.name}` : '💡 Feature-Wunsch';
|
||||
@@ -87,6 +129,22 @@ export async function wunschAnlegen(client, { idee, warum, autor, autorId, avata
|
||||
.setURL(`${publicUrl()}/roadmap`)
|
||||
.setFooter({ text: `${brandFooter('VOTING')} • 👍 = will ich!` });
|
||||
|
||||
const archiv = tuning('thread_archive_days') * 1440;
|
||||
|
||||
if (istForum(channel)) {
|
||||
const thread = await channel.threads.create({
|
||||
name: idee.slice(0, 100),
|
||||
autoArchiveDuration: archiv,
|
||||
appliedTags: tagsFuer(channel, { bereich, statusLabel: STATUS_LABEL.offen }),
|
||||
message: { embeds: [embed] },
|
||||
});
|
||||
const starter = await thread.fetchStarterMessage().catch(() => null);
|
||||
await starter?.react('👍').catch(() => {});
|
||||
const id = saveWish(starter?.id ?? thread.id, autor, idee, autorId ?? null, categoryId ?? null);
|
||||
setWishThread(id, thread.id);
|
||||
return { id, url: `${thread.url}` };
|
||||
}
|
||||
|
||||
const message = await channel.send({ embeds: [embed] });
|
||||
await message.react('👍').catch(() => {});
|
||||
const id = saveWish(message.id, autor, idee, autorId ?? null, categoryId ?? null);
|
||||
@@ -96,7 +154,7 @@ export async function wunschAnlegen(client, { idee, warum, autor, autorId, avata
|
||||
try {
|
||||
const thread = await message.startThread({
|
||||
name: `💬 ${idee}`.slice(0, 100),
|
||||
autoArchiveDuration: tuning('thread_archive_days') * 1440,
|
||||
autoArchiveDuration: archiv,
|
||||
});
|
||||
setWishThread(id, thread.id);
|
||||
} catch (error) {
|
||||
@@ -105,6 +163,30 @@ export async function wunschAnlegen(client, { idee, warum, autor, autorId, avata
|
||||
return { id, url: message.url };
|
||||
}
|
||||
|
||||
/**
|
||||
* Nach einem Status-Wechsel die Tags am Forums-Beitrag nachziehen: der alte
|
||||
* Stand fliegt raus, der neue kommt rein, der Bereich bleibt.
|
||||
*/
|
||||
export async function wunschTagsAktualisieren(client, wunsch, statusLabel) {
|
||||
if (!wunsch.thread_id) return;
|
||||
const channelId = votingChannelId();
|
||||
if (!channelId) return;
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
if (!istForum(channel)) return;
|
||||
const thread = await client.channels.fetch(wunsch.thread_id).catch(() => null);
|
||||
if (!thread?.setAppliedTags) return;
|
||||
|
||||
const alleStaende = new Set(Object.values(STATUS_LABEL).map((s) => s.toLowerCase()));
|
||||
const behalten = (thread.appliedTags ?? []).filter((id) => {
|
||||
const tag = (channel.availableTags ?? []).find((t) => t.id === id);
|
||||
return tag && !alleStaende.has(tag.name.toLowerCase());
|
||||
});
|
||||
const neu = tagsFuer(channel, { bereich: null, statusLabel });
|
||||
await thread.setAppliedTags([...new Set([...behalten, ...neu])].slice(0, 5)).catch((e) =>
|
||||
console.error('[voting] Tags nicht gesetzt:', e.message)
|
||||
);
|
||||
}
|
||||
|
||||
/** Auswahl im Aufruf-Post → Eingabefenster für diesen Bereich */
|
||||
export async function handleWunschPick(interaction) {
|
||||
const gewaehlt = Number(interaction.values?.[0]);
|
||||
|
||||
Reference in New Issue
Block a user