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:
@@ -428,6 +428,124 @@
|
||||
} catch { /* umgeleitet */ }
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
REAKTIONEN (14.09.2026)
|
||||
|
||||
Filipe: "ich will dass man auf die nachrichten auch reagieren kann
|
||||
mit einem emoji, die nachricht selbst ohne zu antworten."
|
||||
|
||||
DIE AUSWAHL KOMMT VOM SERVER. Stuende sie hier, gaebe es sie
|
||||
zweimal -- und die zweite Fassung ist die, in der ein Zeichen
|
||||
fehlt, das der Server noch annimmt. Faellt der Abruf aus, gibt es
|
||||
keinen Ersatz aus dem Aermel: Dann erscheint die Auswahl nicht,
|
||||
statt Zeichen anzubieten, die abgewiesen werden.
|
||||
=================================================================== */
|
||||
let reaktionsZeichen = null;
|
||||
let offeneAuswahl = null;
|
||||
|
||||
async function zeichenHolen() {
|
||||
if (reaktionsZeichen) return reaktionsZeichen;
|
||||
try {
|
||||
const a = await hole('/workspace/api/chat/reaktionen/auswahl');
|
||||
if (!a.ok) return null;
|
||||
const d = await a.json();
|
||||
reaktionsZeichen = Array.isArray(d.zeichen) && d.zeichen.length ? d.zeichen : null;
|
||||
return reaktionsZeichen;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function auswahlSchliessen() {
|
||||
if (offeneAuswahl) { offeneAuswahl.remove(); offeneAuswahl = null; }
|
||||
}
|
||||
|
||||
async function auswahlZeigen(knopf, nachrichtId) {
|
||||
/* Ein zweiter Klick auf denselben Knopf schliesst wieder -- sonst
|
||||
staendig eine offene Tafel, die man wegklicken muss. */
|
||||
const schonOffen = offeneAuswahl && offeneAuswahl.dataset.fuer === String(nachrichtId);
|
||||
auswahlSchliessen();
|
||||
if (schonOffen) return;
|
||||
|
||||
const zeichen = await zeichenHolen();
|
||||
if (!zeichen) { melde('Die Auswahl ließ sich nicht laden.'); return; }
|
||||
|
||||
const tafel = el('div', 'reagieren-tafel');
|
||||
tafel.dataset.fuer = String(nachrichtId);
|
||||
tafel.setAttribute('role', 'group');
|
||||
tafel.setAttribute('aria-label', 'Mit einem Zeichen reagieren');
|
||||
for (const z of zeichen) {
|
||||
const b = el('button', 'reagieren-tafel__zeichen', z);
|
||||
b.type = 'button';
|
||||
b.setAttribute('aria-label', `Mit ${z} reagieren`);
|
||||
b.addEventListener('click', () => { auswahlSchliessen(); reagieren(nachrichtId, z); });
|
||||
tafel.append(b);
|
||||
}
|
||||
knopf.closest('.chat-nachricht')?.append(tafel);
|
||||
offeneAuswahl = tafel;
|
||||
tafel.querySelector('button')?.focus();
|
||||
}
|
||||
|
||||
/* Wegklicken und Escape schliessen die Tafel. Ohne das bleibt sie
|
||||
stehen, wenn man es sich anders ueberlegt -- und sie sieht dann
|
||||
aus wie ein Fehler. */
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!offeneAuswahl) return;
|
||||
if (e.target.closest('.reagieren-tafel, .chat-nachricht__reag-knopf')) return;
|
||||
auswahlSchliessen();
|
||||
});
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') auswahlSchliessen(); });
|
||||
|
||||
async function reagieren(nachrichtId, zeichen) {
|
||||
try {
|
||||
const a = await hole(`/workspace/api/chat/nachrichten/${nachrichtId}/reaktion`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ zeichen }),
|
||||
});
|
||||
if (!a.ok) {
|
||||
const d = await a.json().catch(() => ({}));
|
||||
melde(d.fehler === 'zurueckgenommen'
|
||||
? 'Auf eine zurückgenommene Nachricht lässt sich nicht reagieren.'
|
||||
: 'Das Zeichen kam nicht an.');
|
||||
setTimeout(() => melde(''), 4000);
|
||||
return;
|
||||
}
|
||||
const d = await a.json();
|
||||
/* Sofort im Bild -- der Ereignisstrom bringt dasselbe gleich noch
|
||||
einmal fuer die anderen. Wer selbst klickt, soll nicht auf das
|
||||
Netz warten muessen. */
|
||||
standSetzen(d.id, d.reaktionen);
|
||||
} catch { melde('Keine Verbindung.'); }
|
||||
}
|
||||
|
||||
/** Die Reaktionen EINER Nachricht neu setzen -- in der Liste und auf
|
||||
* dem Bildschirm. An einer Stelle, damit der Klick und das Ereignis
|
||||
* nicht zwei verschiedene Wege nehmen. */
|
||||
function standSetzen(id, reaktionen) {
|
||||
const i = nachrichten.findIndex((x) => x.id === id);
|
||||
if (i >= 0) nachrichten[i].reaktionen = reaktionen;
|
||||
verlaufZeichnen();
|
||||
}
|
||||
|
||||
/** Die Zeile unter der Nachricht. */
|
||||
function reaktionenBauen(n) {
|
||||
const reihe = el('div', 'reaktionen');
|
||||
for (const r of n.reaktionen) {
|
||||
const b = el('button', 'reaktionen__stueck');
|
||||
b.type = 'button';
|
||||
if (r.selbst) b.dataset.selbst = 'ja';
|
||||
b.append(el('span', 'reaktionen__zeichen', r.zeichen));
|
||||
b.append(el('span', 'reaktionen__zahl', String(r.anzahl)));
|
||||
/* WER ES WAR, steht im Titel. "Drei Daumen" sagt weniger als die
|
||||
Namen -- und es sind Leute aus demselben Raum. */
|
||||
b.title = `${r.wer.join(', ')} · ${r.selbst ? 'noch einmal klicken nimmt deins zurück'
|
||||
: 'klicken, um dich anzuschließen'}`;
|
||||
b.setAttribute('aria-label', `${r.zeichen} von ${r.wer.join(', ')}`);
|
||||
b.addEventListener('click', () => reagieren(n.id, r.zeichen));
|
||||
reihe.append(b);
|
||||
}
|
||||
return reihe;
|
||||
}
|
||||
|
||||
function verlaufZeichnen() {
|
||||
const ziel = $('verlauf');
|
||||
/* NUR DIE NACHRICHTEN WEG — NICHT DER GANZE KASTEN.
|
||||
@@ -573,6 +691,21 @@
|
||||
ant.type = 'button';
|
||||
ant.addEventListener('click', () => antwortSetzen(n));
|
||||
fuss.append(ant);
|
||||
|
||||
/* REAGIEREN (14.09.2026).
|
||||
Filipe: "ich will dass man auf die nachrichten auch reagieren
|
||||
kann mit einem emoji, die nachricht selbst ohne zu
|
||||
antworten."
|
||||
|
||||
Der Knopf steht neben "antworten", weil es dieselbe Sorte
|
||||
Handgriff ist -- nur der kleinere. Die Auswahl klappt an der
|
||||
Nachricht auf und nicht in einem Fenster: Wer ein Zeichen
|
||||
anklickt, ist in zwei Sekunden fertig; ein Dialog dafuer
|
||||
waere mehr Weg als Zweck. */
|
||||
const reag = el('button', 'chat-nachricht__reag-knopf', 'reagieren');
|
||||
reag.type = 'button';
|
||||
reag.addEventListener('click', () => auswahlZeigen(reag, n.id));
|
||||
fuss.append(reag);
|
||||
}
|
||||
if (n.selbst && !n.zurueckgenommen) {
|
||||
const weg = el('button', 'chat-nachricht__weg-knopf', 'zurücknehmen');
|
||||
@@ -616,6 +749,14 @@
|
||||
fuss.append(kopie);
|
||||
}
|
||||
blase.append(fuss);
|
||||
|
||||
/* DIE REAKTIONEN STEHEN UNTER DER NACHRICHT, nicht in der
|
||||
Fusszeile: Sie sind Inhalt, keine Bedienung. Jedes Zeichen ein
|
||||
eigener Knopf -- ein Klick darauf setzt oder nimmt die eigene
|
||||
Reaktion, genau wie in der Auswahl. */
|
||||
if (Array.isArray(n.reaktionen) && n.reaktionen.length) {
|
||||
blase.append(reaktionenBauen(n));
|
||||
}
|
||||
zeile.append(blase);
|
||||
ziel.append(zeile);
|
||||
}
|
||||
@@ -1027,6 +1168,19 @@
|
||||
raeumeLaden();
|
||||
});
|
||||
|
||||
/* EINE REAKTION -- alle im Raum sehen sie sofort (14.09.2026).
|
||||
|
||||
Betroffen ist nur, wer denselben Raum offen hat. Wer ihn nicht
|
||||
offen hat, bekommt NICHTS: kein Ungelesen-Zeichen, keine Meldung
|
||||
aufs Handy. Ein Daumen ist keine Nachricht; wer dafuer geweckt
|
||||
wird, schaltet Meldungen ab. */
|
||||
strom.addEventListener('reaktion', (e) => {
|
||||
let n;
|
||||
try { n = JSON.parse(e.data); } catch { return; }
|
||||
if (!offen || n.raum_id !== offen.id) return;
|
||||
standSetzen(n.id, n.reaktionen || []);
|
||||
});
|
||||
|
||||
/* ANGEHEFTET ODER GELOEST -- alle im Raum sehen es sofort.
|
||||
Betroffen ist nur, wer denselben Raum offen hat; sonst genuegt
|
||||
die Liste. */
|
||||
|
||||
Reference in New Issue
Block a user