Jeder darf seine eigene Chat-Nachricht bearbeiten

Filipe: "die nachrichten die man in den chat reinschreibt. jeder soll
seine eigene nachricht bearbeiten koennen. diese option soll jeder
fuer seine eigene nachrichten haben die er selber verfasst hat."

NUR DIE EIGENE -- OHNE AUSNAHME, auch nicht fuer DogFather und die
rechte Hand. Beim LOESCHEN gibt es diese Ausnahme seit dem 22.09.
(fuer den Notfall), und es waere naheliegend gewesen, sie
mitzunehmen. Das waere falsch: Eine fremde Nachricht zu entfernen
heisst "das soll hier nicht stehen". Eine fremde zu AENDERN heisst,
jemandem Worte in den Mund zu legen, die unter seinem Namen und
seinem Bild stehen bleiben. Nicht dieselbe Befugnis in groesser,
sondern eine andere. Genau das ist die wichtigste Pruefzeile:
DogFather darf loeschen und bekommt beim Bearbeiten 403/404.

AN DER NACHRICHT STEHT "BEARBEITET". Ein Text, der sich still
aendert, nachdem andere darauf geantwortet haben, ist ein
Vertrauensproblem und kein Komfort. Der Vermerk traegt die Zeit im
Titel. Derselbe Text setzt ihn NICHT -- sonst stuende er irgendwann
ueberall und waere nichts mehr wert.

ERWAEHNUNGEN BLEIBEN, WIE SIE BEIM SENDEN WAREN. Wer beim Bearbeiten
"@Anna" ergaenzt, spricht Anna damit nicht an. Sonst gaebe es nur
schlechte Wege: nachtraeglich benachrichtigen laesst sich beliebig
oft wiederholen, und still eintragen setzt jemanden auf eine Liste,
von der er nie erfaehrt. Ansprechen tut man mit einer neuen
Nachricht. (Falls das anders gewuenscht ist, ist es eine eigene
Entscheidung -- nicht etwas, das hier nebenbei mitpassiert.)

DER RAUM RUECKT NICHT NACH OBEN und niemand bekommt die Nachricht
als ungelesen: Eine Tippfehlerkorrektur ist keine Wortmeldung.
`letzte_am` wird deshalb nicht angefasst.

Leer geht nicht -- dafuer steht "loeschen" daneben, mit Rueckfrage.
Grenzen (4000 Zeichen) sind dieselben wie beim Senden; eine zweite
Rechnung waere die, die auseinanderlaeuft.

Das Feld sitzt AN der Nachricht, nicht im Schreibfeld unten: Wer
seinen Text zum Bearbeiten unten wiederfindet, schickt ihn beim
naechsten Enter als NEUE Nachricht ab und hat ihn zweimal im Raum.
Enter speichert, Shift+Enter macht eine Zeile, Escape bricht ab --
dieselben Tasten wie beim Schreiben. 16 px Schrift, sonst zoomt iOS
beim Hineintippen die ganze Seite heran.

Der Stift traegt sich in die Familie der Handgriffe ein, wie es der
Hinweis in chat.css ausdruecklich verlangt ("wer einen sechsten
Handgriff baut, traegt ihn hier ein und bekommt sein Zeichen").

EIN FEHLER, DEN NUR DAS BILD GEZEIGT HAT. Ich hatte im Code
behauptet, die Gespraechsliste aendere sich beim Bearbeiten nicht,
und darum auf das Nachladen verzichtet. Auf dem Bildschirmfoto stand
rechts "Treffen um 15 Uhr" und links in der Liste weiter "Du:
Treffen um 15 Urh" -- derselbe Satz, zweimal verschieden, auf einem
Schirm. Richtig ist: Die REIHENFOLGE aendert sich nicht, die
VORSCHAU sehr wohl. Beide Haelften waren fuer sich gemessen und
gruen; keine Zahl hat es gemerkt.

pruef-chat 17 neue Pruefungen: eigene geht, fremde nicht, DogFather
nicht, Vermerk kommt mit nach draussen (auch im SELECT -- genau das
hat am 03.10. bei den Anhaengen einen halben Tag gekostet), Raum
rueckt nicht, Vorschau zieht nach, leer/zu lang abgelehnt,
unveraendert ohne Vermerk, geloeschte nicht bearbeitbar, wer nicht
im Raum ist bekommt 404 statt 403.

pruef-chat-optik 71 -> 84: im echten Browser, mit zwei Sitzungen.
Darunter die Zeile, auf die es ankommt -- der neue Text steht bei
Patrick, OHNE Neuladen. Ein Bearbeiten, das nur der Schreibende
sieht, waere schlimmer als keins.

Dabei zwei eigene Messfehler behoben: Die Sitzungen von oben waren
laengst geschlossen (Playwright meldet nur "Target page has been
closed"), und beide klickten "das oberste Gespraech" statt
denselben Raum -- wodurch die Pruefung "an ihr steht KEIN
bearbeiten" gruen war, weil die Nachricht gar nicht da war. Sie
haengt jetzt daran, dass er sie wirklich sieht.

Datenbank vorher gesichert und zurueckgelesen (integrity_check,
324 Nachrichten, 20 Personen).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-04 00:06:29 +02:00
co-authored by Claude Opus 5
parent 5d9c8cd377
commit b625d901a5
52 changed files with 1314 additions and 686 deletions
+175 -1
View File
@@ -626,6 +626,11 @@
let raeume = [];
let offen = null; // der gerade angesehene Raum
let nachrichten = [];
/* WELCHE NACHRICHT GERADE BEARBEITET WIRD -- 0 heisst: keine.
EINE Zahl und kein Merkmal an der Nachricht: Zwei offene Felder
gleichzeitig waeren zwei Staende desselben Textes, und beim
Speichern gewinnt der Zufall. */
let bearbeiteId = 0;
let strom = null; // die EventSource
let gewaehlt = new Set(); // im Neu-Dialog
@@ -2291,7 +2296,8 @@
NUR WENN WIRKLICH TEXT DA IST: Eine Nachricht, die nur aus
einem Foto besteht, hätte sonst einen leeren Absatz darunter
-- und der nimmt Platz, den niemand erklären kann. */
if (n.text) blase.append(textAbsatz(n.text, !!n.rudel));
if (bearbeiteId === n.id) blase.append(bearbeitenFeld(n));
else if (n.text) blase.append(textAbsatz(n.text, !!n.rudel));
}
/* ==== DAS WORT KOMMT INS ELEMENT, NICHT AN DEN KNOPF ==========
@@ -2335,6 +2341,17 @@
zeit.title = wann(n.erstellt, true);
fuss.append(zeit);
/* DER VERMERK STEHT AN DER NACHRICHT, nicht im Protokoll
(03.10.2026). Ein Text, der sich still aendert, nachdem
andere darauf geantwortet haben, ist ein Vertrauensproblem.
Er steht direkt hinter der Zeit, weil er zur Zeit gehoert:
beides sagt, WANN hier was passiert ist. */
if (n.bearbeitet_am) {
const v = el('span', 'chat-nachricht__bearbeitet', 'bearbeitet');
v.title = `bearbeitet ${wann(n.bearbeitet_am, true)}`;
fuss.append(v);
}
/* Antworten geht auf JEDE Nachricht, auch die eigene -- in einer
Gruppe bezieht man sich durchaus auf sich selbst. Nur auf eine
zurueckgenommene nicht: Das Zitat waere leer. */
@@ -2373,6 +2390,27 @@
das kein gewöhnlicher Handgriff ist. Die Schranke steht
ohnehin im Server; hier geht es darum, sie nicht aus Versehen
zu benutzen. */
/* BEARBEITEN -- NUR DIE EIGENE (03.10.2026).
Filipe: "jeder soll seine eigene nachricht bearbeiten
koennen."
`n.selbst` OHNE die Ausnahme fuer DogFather und die rechte
Hand, die es beim Loeschen gibt. Eine fremde Nachricht zu
entfernen heisst "das soll hier nicht stehen"; sie zu AENDERN
heisst, jemandem Worte in den Mund zu legen, die unter seinem
Namen und seinem Bild stehen bleiben. Zwei verschiedene
Dinge, nicht dasselbe in groesser.
NUR WO TEXT IST: Ein reines Foto oder eine Sprachnachricht
hat nichts zu bearbeiten -- dort waere der Knopf ein Angebot,
das ins Leere fuehrt. Wer einen Begleittext hat, kann ihn
aendern; der Anhang bleibt davon unberuehrt. */
if (n.id > 0 && n.selbst && !n.zurueckgenommen && n.text && bearbeiteId !== n.id) {
const bea = wortKnopf('chat-nachricht__bearb-knopf', 'bearbeiten');
bea.addEventListener('click', () => { bearbeiteId = n.id; verlaufZeichnen(); });
fuss.append(bea);
}
if (n.id > 0 && (n.selbst || darfAllesLoeschen)) {
const fremd = !n.selbst;
const weg = wortKnopf('chat-nachricht__weg-knopf',
@@ -3403,6 +3441,105 @@
+ `mit „Nochmal senden“ kannst du es gleich wiederholen.`);
}
/** Das Feld, in dem eine eigene Nachricht geaendert wird.
*
* AN DER STELLE DER NACHRICHT, nicht im Schreibfeld unten. Wer
* seinen Text zum Bearbeiten unten wiederfindet, schickt ihn beim
* naechsten Enter als NEUE Nachricht ab -- und hat ihn dann
* zweimal im Raum. Hier kann das nicht passieren: Was im Feld
* steht, hat nur einen Weg, und der fuehrt auf dieselbe Nummer.
*/
function bearbeitenFeld(n) {
const kasten = el('div', 'chat-bearb');
const feld = el('textarea', 'chat-bearb__feld');
feld.value = n.text || '';
feld.rows = 1;
feld.maxLength = 4000;
feld.setAttribute('aria-label', 'Nachricht bearbeiten');
/* Mitwachsen wie das Schreibfeld unten -- bis zu einer Grenze,
sonst verdraengt eine lange Nachricht den ganzen Verlauf. */
const wachsen = () => {
feld.style.height = 'auto';
feld.style.height = Math.min(feld.scrollHeight, 200) + 'px';
};
feld.addEventListener('input', wachsen);
const fertig = () => { bearbeiteId = 0; verlaufZeichnen(); };
const senden = () => { void bearbeitenSenden(n, feld.value); };
/* DIESELBEN TASTEN WIE BEIM SCHREIBEN: Enter schickt ab,
Shift+Enter macht eine Zeile. Eine zweite Regel hier waere eine,
die man erst beim Danebengreifen merkt. Escape bricht ab -- der
Weg, den jeder ohnehin probiert. */
feld.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); senden(); }
else if (e.key === 'Escape') { e.preventDefault(); fertig(); }
});
const reihe = el('div', 'chat-bearb__reihe');
const ja = el('button', 'chat-bearb__knopf chat-bearb__knopf--ja', 'Speichern');
ja.type = 'button';
ja.addEventListener('click', senden);
const nein = el('button', 'chat-bearb__knopf', 'Abbrechen');
nein.type = 'button';
nein.addEventListener('click', fertig);
reihe.append(ja, nein);
kasten.append(feld, reihe);
/* NACH dem Einhaengen scharfstellen: Vorher hat das Feld keine
Hoehe und `focus()` rollt den Verlauf an die falsche Stelle.
Der Zeiger steht am Ende, nicht am Anfang -- man will meistens
etwas anfuegen oder das letzte Wort richten. */
setTimeout(() => {
wachsen();
feld.focus();
feld.setSelectionRange(feld.value.length, feld.value.length);
}, 0);
return kasten;
}
async function bearbeitenSenden(n, roh) {
const text = String(roh || '').trim();
if (!text) {
/* LEEREN IST LOESCHEN, und dafuer steht der Knopf daneben --
mit Rueckfrage. Hier waere es derselbe Eingriff ohne sie. */
melde('Leer geht nicht – zum Entfernen gibt es „löschen“ daneben.');
return;
}
if (text === (n.text || '')) { bearbeiteId = 0; verlaufZeichnen(); return; }
try {
const a = await hole(`/workspace/api/chat/nachrichten/${n.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
if (!a.ok) {
const d = await a.json().catch(() => ({}));
melde(d.fehler ? `Ging nicht: ${d.fehler}` : 'Ging nicht.');
return;
}
const d = await a.json().catch(() => ({}));
/* NICHT auf das Ereignis vom Server warten. Es kommt zwar auch
bei mir an, aber erst nach dem Umweg ueber die Leitung -- und
bis dahin stuende mein alter Text noch da. Der Strom holt
spaeter dasselbe Ergebnis und schreibt dasselbe hin. */
const i = nachrichten.findIndex((x) => x.id === n.id);
if (i >= 0) {
nachrichten[i].text = text;
nachrichten[i].bearbeitet_am = d.bearbeitet_am || new Date().toISOString();
}
for (const x of nachrichten) {
if (x.antwort?.id === n.id) x.antwort = { ...x.antwort, text: text.slice(0, 140) };
}
bearbeiteId = 0;
verlaufZeichnen();
/* Die Vorschau in der Gespraechsliste zeigt denselben Satz --
sie muss mitkommen, sonst steht links der alte Text. */
raeumeLaden();
} catch { /* umgeleitet */ }
}
async function zuruecknehmen(id, fremd = false) {
if (!await frageNach({
titel: fremd ? 'Diese Nachricht löschen?' : 'Nachricht löschen?',
@@ -3598,6 +3735,43 @@
raeumeLaden();
});
/* EINE NACHRICHT WURDE BEARBEITET (03.10.2026).
Sie bleibt an ihrer Stelle -- nur Text und Vermerk wechseln.
UND DIE RAUMLISTE WIRD DOCH NEU GEHOLT (berichtigt 03.10.2026).
Hier stand das Gegenteil, mit der Begruendung, die Liste aendere
sich ja nicht. Das war falsch, und ein Bildschirmfoto hat es
gezeigt: Links in der Gespraechsliste stand weiter "Du: Treffen
um 15 Urh", waehrend rechts schon "Treffen um 15 Uhr" stand --
derselbe Satz, zweimal verschieden, auf einem Schirm.
Richtig ist: Die REIHENFOLGE aendert sich nicht (der Server
ruehrt `letzte_am` bewusst nicht an), die VORSCHAU sehr wohl.
Ein Abruf je Bearbeitung ist dafuer kein Preis -- bearbeitet
wird selten, und die Alternative waere eine veraltete Zeile.
Wer die Nachricht gerade selbst offen im Bearbeiten-Feld hat,
behaelt sein Feld: Sein eigener Stand ist neuer als das, was
hier ankommt -- es ist ja seine eigene Aenderung, die
zurueckkommt. */
strom.addEventListener('geaendert', (e) => {
let n;
try { n = JSON.parse(e.data); } catch { return; }
const i = nachrichten.findIndex((x) => x.id === n.id);
if (i < 0) return;
nachrichten[i].text = n.text;
nachrichten[i].bearbeitet_am = n.bearbeitet_am;
/* Auch das Zitat an ANDEREN Nachrichten zieht nach -- sonst
stuende oben der alte Satz und unten der neue, und beide
behaupten, dasselbe zu sein. */
for (const x of nachrichten) {
if (x.antwort?.id === n.id) x.antwort = { ...x.antwort, text: String(n.text).slice(0, 140) };
}
verlaufZeichnen();
raeumeLaden();
});
strom.addEventListener('weg', (e) => {
let n;
try { n = JSON.parse(e.data); } catch { return; }