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:
@@ -383,6 +383,123 @@ console.log("=== Anhaenge ===");
|
||||
"der Anhang-Ordner selbst steht noch (die Pruefung misst die Datei, nicht den Ordner)");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
7b. 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."
|
||||
|
||||
GEMESSEN WIRD DER BESTAND, nicht die Antwort. Ein "ok" sagt nur,
|
||||
dass der Server nicht gestolpert ist; ob die Reaktion wirklich steht
|
||||
(und beim zweiten Klick wirklich weg ist), steht in der Datenbank.
|
||||
======================================================================= */
|
||||
console.log("");
|
||||
console.log("=== Reaktionen ===");
|
||||
{
|
||||
const g = await (await rufe(eins.keks, "/workspace/api/chat/gruppe",
|
||||
{ mit: [idZwei, idDrei], name: "Reaktionsrunde" }, "POST")).json();
|
||||
const n = await (await rufe(eins.keks,
|
||||
`/workspace/api/chat/raeume/${g.raum_id}/nachrichten`, { text: "Moin" }, "POST")).json();
|
||||
const nid = n.nachricht?.id;
|
||||
ok(Number.isInteger(nid), `eine Nachricht zum Reagieren (#${nid})`);
|
||||
|
||||
const auswahl = await holen(eins.keks, "/workspace/api/chat/reaktionen/auswahl");
|
||||
ok(Array.isArray(auswahl.zeichen) && auswahl.zeichen.length >= 4,
|
||||
`die Auswahl kommt vom Server (${(auswahl.zeichen || []).length} Zeichen)`);
|
||||
const daumen = auswahl.zeichen[0];
|
||||
const herz = auswahl.zeichen[1];
|
||||
|
||||
const zaehle = () => d.prepare(
|
||||
"SELECT COUNT(*) AS n FROM chat_reaktionen WHERE nachricht_id = ?").get(nid).n;
|
||||
|
||||
/* ---- Setzen ---------------------------------------------------- */
|
||||
const a1 = await (await rufe(zwei.keks,
|
||||
`/workspace/api/chat/nachrichten/${nid}/reaktion`, { zeichen: daumen }, "PUT")).json();
|
||||
ok(zaehle() === 1, `Ben setzt ein Zeichen -- es steht in der Datenbank (${zaehle()})`);
|
||||
ok(a1.reaktionen?.[0]?.anzahl === 1 && a1.reaktionen?.[0]?.selbst === true,
|
||||
"die Antwort nennt Anzahl und dass es seines ist");
|
||||
|
||||
/* ---- Derselbe Mensch, ZWEITES Zeichen -- das geht --------------- */
|
||||
await rufe(zwei.keks, `/workspace/api/chat/nachrichten/${nid}/reaktion`,
|
||||
{ zeichen: herz }, "PUT");
|
||||
ok(zaehle() === 2, `dasselbe Mensch darf ein ZWEITES Zeichen geben (${zaehle()})`);
|
||||
|
||||
/* ---- Derselbe Mensch, DASSELBE Zeichen -- nimmt zurueck --------- */
|
||||
const a3 = await (await rufe(zwei.keks,
|
||||
`/workspace/api/chat/nachrichten/${nid}/reaktion`, { zeichen: daumen }, "PUT")).json();
|
||||
ok(zaehle() === 1, `zweimal dasselbe nimmt es wieder weg (${zaehle()})`);
|
||||
ok(a3.gesetzt === false, "und die Antwort sagt, dass es weg ist");
|
||||
|
||||
/* ---- Mehrere Menschen, dasselbe Zeichen ------------------------- */
|
||||
await rufe(drei.keks, `/workspace/api/chat/nachrichten/${nid}/reaktion`,
|
||||
{ zeichen: herz }, "PUT");
|
||||
const stand = await (await rufe(eins.keks,
|
||||
`/workspace/api/chat/nachrichten/${nid}/reaktion`, { zeichen: herz }, "PUT")).json();
|
||||
const herzEintrag = (stand.reaktionen || []).find((r) => r.zeichen === herz);
|
||||
ok(herzEintrag?.anzahl === 3, `drei Menschen, ein Zeichen, Anzahl 3 (${herzEintrag?.anzahl})`);
|
||||
ok(Array.isArray(herzEintrag?.wer) && herzEintrag.wer.length === 3,
|
||||
`und die Namen stehen dabei (${(herzEintrag?.wer || []).join(", ")})`);
|
||||
|
||||
/* ---- Was NICHT geht --------------------------------------------- */
|
||||
const fremd = await rufe(hand.keks, `/workspace/api/chat/nachrichten/${nid}/reaktion`,
|
||||
{ zeichen: daumen }, "PUT");
|
||||
ok(fremd.status === 404,
|
||||
`wer nicht im Raum ist, kommt nicht heran (HTTP ${fremd.status})`);
|
||||
|
||||
const unsinn = await rufe(zwei.keks, `/workspace/api/chat/nachrichten/${nid}/reaktion`,
|
||||
{ zeichen: "<script>alert(1)</script>" }, "PUT");
|
||||
ok(unsinn.status === 400,
|
||||
`ein erfundenes Zeichen wird abgewiesen (HTTP ${unsinn.status})`);
|
||||
ok(zaehle() === 3, `und aendert nichts am Bestand (${zaehle()})`);
|
||||
|
||||
/* ---- Eine zurueckgenommene Nachricht ----------------------------- */
|
||||
const weg = await (await rufe(eins.keks,
|
||||
`/workspace/api/chat/raeume/${g.raum_id}/nachrichten`, { text: "Weg gleich" }, "POST")).json();
|
||||
await rufe(eins.keks, `/workspace/api/chat/nachrichten/${weg.nachricht.id}`,
|
||||
undefined, "DELETE");
|
||||
const drauf = await rufe(zwei.keks,
|
||||
`/workspace/api/chat/nachrichten/${weg.nachricht.id}/reaktion`, { zeichen: daumen }, "PUT");
|
||||
ok(drauf.status === 409,
|
||||
`auf eine zurueckgenommene Nachricht geht es nicht (HTTP ${drauf.status})`);
|
||||
|
||||
/* ---- Und sie ueberleben das Neuladen ----------------------------- */
|
||||
const verlauf = await holen(zwei.keks,
|
||||
`/workspace/api/chat/raeume/${g.raum_id}/nachrichten`);
|
||||
const wieder = (verlauf.nachrichten || []).find((x) => x.id === nid);
|
||||
ok(Array.isArray(wieder?.reaktionen) && wieder.reaktionen.length === 1,
|
||||
`beim Neuladen sind sie wieder da (${(wieder?.reaktionen || []).length} Sorte)`);
|
||||
ok(wieder?.reaktionen?.[0]?.selbst === true,
|
||||
"und Ben sieht, dass eines davon seines ist");
|
||||
|
||||
/* GEGENPROBE, DIE WIRKLICH UNTERSCHEIDET.
|
||||
|
||||
Der erste Entwurf pruefte "bei Cem ist ueberall selbst=true" -- und
|
||||
das war trivial wahr, weil es zu dem Zeitpunkt nur EINE Sorte gab.
|
||||
Eine Angabe, die nie `false` sein kann, sagt nichts.
|
||||
|
||||
Also bekommt Ayla ein drittes Zeichen, das Cem NICHT gegeben hat.
|
||||
Jetzt muss bei Cem eines auf `true` und eines auf `false` stehen --
|
||||
in derselben Antwort. */
|
||||
const lachen = auswahl.zeichen[2];
|
||||
await rufe(eins.keks, `/workspace/api/chat/nachrichten/${nid}/reaktion`,
|
||||
{ zeichen: lachen }, "PUT");
|
||||
const beiDrei = await holen(drei.keks,
|
||||
`/workspace/api/chat/raeume/${g.raum_id}/nachrichten`);
|
||||
const seins = (beiDrei.nachrichten || []).find((x) => x.id === nid);
|
||||
const meins = (seins?.reaktionen || []).find((r) => r.zeichen === herz);
|
||||
const fremdes = (seins?.reaktionen || []).find((r) => r.zeichen === lachen);
|
||||
ok(meins?.selbst === true && fremdes?.selbst === false,
|
||||
`bei Cem: sein Herz ist "selbst", Aylas Lachen nicht `
|
||||
+ `(${meins?.selbst} / ${fremdes?.selbst})`);
|
||||
|
||||
/* ---- Die Nachricht loeschen nimmt die Reaktionen mit ------------- */
|
||||
const vorherAnzahl = zaehle();
|
||||
ok(vorherAnzahl > 0, `vorher stehen ${vorherAnzahl} Reaktionen da`);
|
||||
d.prepare("DELETE FROM chat_nachrichten WHERE id = ?").run(nid);
|
||||
ok(zaehle() === 0,
|
||||
`mit der Nachricht verschwinden die Reaktionen (${zaehle()} uebrig)`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
8. Und wie es auf dem Bildschirm aussieht
|
||||
=======================================================================
|
||||
@@ -459,9 +576,75 @@ console.log("=== Das Neu-Fenster ===");
|
||||
if (process.env.SCHIRM) {
|
||||
await seite.screenshot({ path: join(HIER_BILD, "pruef-chat-neu.png") });
|
||||
}
|
||||
|
||||
await seite.click("#neu-zu");
|
||||
await seite.waitForTimeout(250);
|
||||
|
||||
/* ---- REAGIEREN AUF DEM BILDSCHIRM (14.09.2026) ------------------
|
||||
Das Recht und der Weg sind oben geprueft. Ob die Oberflaeche
|
||||
beides verbindet, ist eine eigene Frage -- genau daran ist am
|
||||
11.09. der Aufloesen-Knopf gescheitert: Recht richtig, Knopf
|
||||
unsichtbar. */
|
||||
const rRaum = await (await rufe(hand.keks, "/workspace/api/chat/gruppe",
|
||||
{ mit: [idEins], name: "Reagieren sichtbar" }, "POST")).json();
|
||||
await rufe(hand.keks, `/workspace/api/chat/raeume/${rRaum.raum_id}/nachrichten`,
|
||||
{ text: "Reagier mal drauf" }, "POST");
|
||||
await seite.goto(BASIS + `/workspace/chat.html?raum=${rRaum.raum_id}`,
|
||||
{ waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(1500);
|
||||
|
||||
const knopfDa = await seite.$$eval(".chat-nachricht__reag-knopf", (b) => b.length);
|
||||
ok(knopfDa > 0, `der Knopf "reagieren" steht an der Nachricht (${knopfDa})`);
|
||||
|
||||
await seite.click(".chat-nachricht__reag-knopf");
|
||||
await seite.waitForTimeout(600);
|
||||
const tafel = await seite.$$eval(".reagieren-tafel__zeichen",
|
||||
(b) => b.map((x) => x.textContent.trim()));
|
||||
ok(tafel.length >= 4, `die Auswahl klappt auf (${tafel.length} Zeichen)`);
|
||||
const tafelLage = await seite.evaluate(() => {
|
||||
const t = document.querySelector(".reagieren-tafel");
|
||||
const z = document.querySelector(".reagieren-tafel__zeichen");
|
||||
const k = (e) => { if (!e) return null; const r = e.getBoundingClientRect();
|
||||
const st = getComputedStyle(e);
|
||||
return { b: Math.round(r.width), h: Math.round(r.height), d: st.display,
|
||||
sicht: st.visibility, o: st.opacity, eltern: e.parentElement?.className }; };
|
||||
return { tafel: k(t), zeichen: k(z) };
|
||||
});
|
||||
ok(tafelLage.zeichen && tafelLage.zeichen.b > 0 && tafelLage.zeichen.h > 0,
|
||||
`die Zeichen haben eine Groesse (${JSON.stringify(tafelLage)})`);
|
||||
|
||||
if (process.env.SCHIRM) {
|
||||
await seite.locator(".chat-nachricht").last().screenshot(
|
||||
{ path: join(HIER_BILD, "pruef-chat-auswahl.png") });
|
||||
}
|
||||
|
||||
await seite.click(".reagieren-tafel__zeichen");
|
||||
await seite.waitForTimeout(900);
|
||||
const stueck = await seite.evaluate(() => {
|
||||
const b = document.querySelector(".reaktionen__stueck");
|
||||
return b ? { text: b.textContent.trim(), selbst: b.dataset.selbst,
|
||||
titel: b.title } : null;
|
||||
});
|
||||
ok(!!stueck, `unter der Nachricht steht die Reaktion ("${stueck?.text || "nichts"}")`);
|
||||
ok(stueck?.selbst === "ja", "und sie ist als die eigene gekennzeichnet");
|
||||
ok((stueck?.titel || "").includes("Vanessa"),
|
||||
`im Titel steht, wer reagiert hat ("${(stueck?.titel || "").slice(0, 40)}…")`);
|
||||
ok((await seite.$$(".reagieren-tafel")).length === 0,
|
||||
"die Auswahl schliesst sich nach dem Klick von selbst");
|
||||
|
||||
if (process.env.SCHIRM) {
|
||||
await seite.locator(".chat-nachricht").last().screenshot(
|
||||
{ path: join(HIER_BILD, "pruef-chat-reaktion.png") });
|
||||
}
|
||||
|
||||
/* ZWEITER KLICK NIMMT SIE ZURUECK -- an derselben Stelle, wo sie
|
||||
steht. Ohne diese Zeile hiesse alles oben nur, dass man etwas
|
||||
hinzufuegen kann. */
|
||||
await seite.click(".reaktionen__stueck");
|
||||
await seite.waitForTimeout(900);
|
||||
ok((await seite.$$(".reaktionen__stueck")).length === 0,
|
||||
"ein Klick darauf nimmt sie wieder weg");
|
||||
|
||||
/* ---- UND DER KNOPF SELBST -----------------------------------------
|
||||
Bis hierher ist nur das RECHT geprueft (darf_aufloesen) und der
|
||||
Weg dahinter. Ob die Oberflaeche beides verbindet, war unbewiesen
|
||||
|
||||
Reference in New Issue
Block a user