Chat: auf eine Nachricht reagieren, ohne zu antworten
Filipe: "ich will dass man auf die nachrichten auch reagieren kann mit einem emoji, die nachricht selbst ohne zu antworten." Neben "antworten" steht jetzt "reagieren". Ein Klick klappt sechs Zeichen auf, ein zweiter Klick setzt es -- und unter der Nachricht steht, wer mit was reagiert hat. EINE FESTE AUSWAHL, KEIN FREIES FELD (👍 ❤️ 😂 😮 🔥 🙏). Drei Gruende, der erste ist der wichtigste: Eine feste Liste laesst sich pruefen -- was dort nicht drinsteht, kommt nicht in die Datenbank, kein beliebiger Text, keine 4-KB-Zeichenketten. Zweitens sehen alle dasselbe. Drittens kann man sechs Zeichen ueberfliegen; eine volle Tafel ist eine Suchaufgabe, und dann tippt man doch wieder "ok". DIE LISTE STEHT AUF DEM SERVER und wird an die Oberflaeche geliefert. Stuende sie im Browser, gaebe es sie zweimal -- und die zweite Fassung waere die, in der ein Zeichen fehlt, das der Server noch annimmt. Faellt der Abruf aus, erscheint KEINE Auswahl statt einer mit erfundenen Zeichen. DER PRIMAERSCHLUESSEL BEANTWORTET "WIE OFT?": (nachricht, person, zeichen). Eine Person darf verschiedene Zeichen geben, jedes aber nur einmal -- und zwar durchgesetzt von der Datenbank, nicht von einer Abfrage davor, die man vergessen kann. Derselbe Klick noch einmal nimmt es zurueck; das erspart einen zweiten Weg, den man auch absichern muesste. KEIN ZAEHLER IN chat_nachrichten. Eine mitgefuehrte Zahl muesste bei jedem Hin und Her stimmen und ist genau die Art Angabe, die irgendwann nicht mehr stimmt und die niemand nachrechnet. Gezaehlt wird beim Lesen -- in EINER Abfrage fuer alle 200 Nachrichten, nicht je Zeile eine. WER ES WAR, STEHT IM TITEL. "Drei Daumen" sagt weniger als "Ayla, Ben und Cem", und es sind Leute aus demselben Raum. KEINE BENACHRICHTIGUNG AUFS HANDY. Wer im Raum ist, sieht es sofort; wer nicht, bekommt nichts. Ein Daumen ist keine Nachricht -- wer dafuer geweckt wird, schaltet Meldungen ab. Was NICHT geht: wer nicht im Raum ist (404), ein erfundenes Zeichen (400), eine zurueckgenommene Nachricht (409). Und mit der Nachricht verschwinden die Reaktionen (CASCADE). pruef-chat-aufloesen 59 -> 85. Gemessen wird der BESTAND, nicht die Antwort -- und im Browser der KNOPF, nicht nur das Recht: dass die Auswahl aufklappt, sich nach dem Klick von selbst schliesst, die Reaktion darunter erscheint, als die eigene erkennbar ist und ein Klick darauf sie wieder wegnimmt. EINE EIGENE PRUEFUNG WAR ZU SCHWACH: "bei Cem ist ueberall selbst=true" war trivial wahr, weil es nur EINE Sorte gab. Jetzt bekommt eine andere Person ein drittes Zeichen, und in derselben Antwort muss eines auf true und eines auf false stehen. Eine Angabe, die nie `false` sein kann, sagt nichts. Im CSS keine vierte Abschrift: "reagieren" haengt an derselben Regel wie "antworten" -- neben anheften und kopieren waere es die vierte fast gleiche gewesen. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+124
-2
@@ -874,7 +874,14 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
|
||||
|| (a.anhang_art === "bild" ? "Foto" : a.anhang_art === "pdf" ? "PDF" : ""),
|
||||
})),
|
||||
/* Aufsteigend zurück -- gelesen wird von oben nach unten. */
|
||||
nachrichten: reihen.reverse().map((n) => ({
|
||||
/* Die Reaktionen in EINER Abfrage fuer alle Nachrichten -- nicht
|
||||
je Zeile eine. Bei 200 Zeilen waeren das 200 Abfragen fuer eine
|
||||
Ansicht, die man beim Scrollen staendig neu holt. */
|
||||
nachrichten: (() => {
|
||||
const gedreht = reihen.reverse();
|
||||
const reakt = reaktionenZu(gedreht.map((n) => n.id), req.person.id);
|
||||
return gedreht.map((n) => ({
|
||||
reaktionen: reakt.get(n.id) || [],
|
||||
id: n.id, von_id: n.person_id, von: n.von || "Gelöscht", rolle: n.rolle,
|
||||
von_bild: n.von_bild ? `/workspace/api/steckbrief/bild/${n.von_bild}` : null,
|
||||
text: n.weg_am ? null : n.text,
|
||||
@@ -904,7 +911,8 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => {
|
||||
breite: n.anhang_breite || 0, hoehe: n.anhang_hoehe || 0,
|
||||
weg: `/workspace/api/chat/anhang/${n.id}`,
|
||||
} : null,
|
||||
})),
|
||||
}));
|
||||
})(),
|
||||
});
|
||||
} catch (fehler) {
|
||||
console.error("[chat] Verlauf:", fehler?.message);
|
||||
@@ -1193,6 +1201,120 @@ function darfAufloesen(raum, person) {
|
||||
&& !!raum.erstellt_von && raum.erstellt_von === person.id;
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
REAKTIONEN — auf die Nachricht, ohne zu antworten (14.09.2026)
|
||||
|
||||
Filipe: "ich will dass man auf die nachrichten auch reagieren kann
|
||||
mit einem emoji, die nachricht selbst ohne zu antworten."
|
||||
|
||||
EINE FESTE AUSWAHL, KEIN FREIES FELD. Sechs Zeichen, hier
|
||||
aufgeschrieben. Drei Gruende, und der erste ist der wichtigste:
|
||||
|
||||
* Eine feste Liste laesst sich pruefen. Was hier nicht drinsteht,
|
||||
kommt nicht in die Datenbank -- kein beliebiger Text, keine
|
||||
Zeichenketten von 4 KB, keine unsichtbaren Steuerzeichen.
|
||||
* Alle sehen dasselbe. Ein selbst getipptes Zeichen sieht auf
|
||||
einem anderen Geraet anders aus oder gar nicht.
|
||||
* Sechs Zeichen kann man ueberfliegen. Eine volle Emoji-Tafel ist
|
||||
eine Suchaufgabe, und dann tippt man doch wieder "ok".
|
||||
|
||||
DIE LISTE STEHT AUF DEM SERVER und wird an die Oberflaeche geliefert
|
||||
(/reaktionen/auswahl). Stuende sie im Browser, gaebe es sie zweimal
|
||||
-- und die zweite Fassung waere die, in der ein Zeichen fehlt, das
|
||||
der Server noch annimmt.
|
||||
===================================================================== */
|
||||
export const REAKTIONEN = ["👍", "❤️", "😂", "😮", "🔥", "🙏"];
|
||||
|
||||
chatRouter.get("/workspace/api/chat/reaktionen/auswahl", (req, res) => {
|
||||
res.json({ zeichen: REAKTIONEN });
|
||||
});
|
||||
|
||||
/** Alle Reaktionen zu einer Menge von Nachrichten -- in EINER Abfrage.
|
||||
*
|
||||
* Je Nachricht einzeln zu fragen waere bei 200 Nachrichten 200
|
||||
* Abfragen fuer eine Ansicht. Zurueck kommt eine Karte:
|
||||
* nachricht_id -> [{ zeichen, anzahl, selbst, wer }].
|
||||
*
|
||||
* `wer` ist bewusst dabei: "drei Daumen" sagt weniger als "Ayla, Ben
|
||||
* und Cem". Es sind Namen aus DEMSELBEN Raum -- wer die Nachricht
|
||||
* sieht, sieht die Leute ohnehin.
|
||||
*/
|
||||
function reaktionenZu(ids, personId) {
|
||||
const karte = new Map();
|
||||
if (!ids.length) return karte;
|
||||
const platz = ids.map(() => "?").join(",");
|
||||
const reihen = db().prepare(`
|
||||
SELECT r.nachricht_id, r.zeichen, r.person_id, p.name
|
||||
FROM chat_reaktionen r
|
||||
LEFT JOIN personen p ON p.id = r.person_id
|
||||
WHERE r.nachricht_id IN (${platz})
|
||||
ORDER BY r.wann`).all(...ids);
|
||||
for (const z of reihen) {
|
||||
if (!karte.has(z.nachricht_id)) karte.set(z.nachricht_id, new Map());
|
||||
const proNachricht = karte.get(z.nachricht_id);
|
||||
if (!proNachricht.has(z.zeichen)) {
|
||||
proNachricht.set(z.zeichen, { zeichen: z.zeichen, anzahl: 0, selbst: false, wer: [] });
|
||||
}
|
||||
const eintrag = proNachricht.get(z.zeichen);
|
||||
eintrag.anzahl++;
|
||||
if (z.person_id === personId) eintrag.selbst = true;
|
||||
eintrag.wer.push(z.name || "Gelöscht");
|
||||
}
|
||||
const fertig = new Map();
|
||||
for (const [id, proNachricht] of karte) fertig.set(id, [...proNachricht.values()]);
|
||||
return fertig;
|
||||
}
|
||||
|
||||
/* REAGIEREN IST EIN UMSCHALTER, kein Hinzufuegen.
|
||||
Zweimal dasselbe Zeichen heisst: wieder weg. Das erspart einen
|
||||
zweiten Weg zum Zuruecknehmen -- und einen zweiten Weg, den man
|
||||
vergisst abzusichern. */
|
||||
chatRouter.put("/workspace/api/chat/nachrichten/:id/reaktion", gleicheHerkunft,
|
||||
express.json({ limit: "2kb" }), (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
|
||||
const zeichen = String(req.body?.zeichen ?? "");
|
||||
if (!REAKTIONEN.includes(zeichen)) {
|
||||
return res.status(400).json({ fehler: "unbekanntes_zeichen" });
|
||||
}
|
||||
|
||||
const d = db();
|
||||
const n = d.prepare(
|
||||
"SELECT id, raum_id, weg_am FROM chat_nachrichten WHERE id = ?").get(id);
|
||||
if (!n) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
/* Die Raumzugehoerigkeit ist die ganze Sicherheit dieses Wegs --
|
||||
dieselbe Pruefung wie beim Lesen des Verlaufs. */
|
||||
if (!istDrin(n.raum_id, req.person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
/* Auf eine zurueckgenommene Nachricht wird nicht reagiert. Dort
|
||||
steht nichts mehr, worauf man reagieren koennte. */
|
||||
if (n.weg_am) return res.status(409).json({ fehler: "zurueckgenommen" });
|
||||
|
||||
const da = d.prepare(`SELECT 1 FROM chat_reaktionen
|
||||
WHERE nachricht_id = ? AND person_id = ? AND zeichen = ?`)
|
||||
.get(id, req.person.id, zeichen);
|
||||
if (da) {
|
||||
d.prepare(`DELETE FROM chat_reaktionen
|
||||
WHERE nachricht_id = ? AND person_id = ? AND zeichen = ?`)
|
||||
.run(id, req.person.id, zeichen);
|
||||
} else {
|
||||
d.prepare(`INSERT INTO chat_reaktionen (nachricht_id, person_id, zeichen, wann)
|
||||
VALUES (?,?,?,?)`).run(id, req.person.id, zeichen, jetzt());
|
||||
}
|
||||
|
||||
const stand = reaktionenZu([id], req.person.id).get(id) || [];
|
||||
/* ALLE IM RAUM SEHEN ES SOFORT -- ohne Benachrichtigung aufs
|
||||
Handy. Ein Daumen ist keine Nachricht; wer dafuer eine Meldung
|
||||
bekaeme, schaltet Meldungen ab. */
|
||||
chatEreignis(n.raum_id, { id, raum_id: n.raum_id, reaktionen: stand }, [], "reaktion");
|
||||
|
||||
res.json({ id, reaktionen: stand, gesetzt: !da });
|
||||
} catch (fehler) {
|
||||
console.error("[chat] Reaktion:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/* =====================================================================
|
||||
EINE GRUPPE ODER EINEN KANAL AUFLOESEN — FUER ALLE (11.09.2026)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user