Drei Fächer, Gesichter, Zeichen an jedem Handgriff -- der Chat ist nicht wiederzuerkennen

Filipe: "ich will 3 kategorien haben. chats mit einzelnen personen,
gruppen chats und kanäle." Und: "ich will dass du überhaupt die
komplette kachel veränderst, ich will dass alles anderst aussieht und
gestaltet ist, mach wirklich was verrücktes und übertrieben krank
geiles ... dass die ganze community und team morgen total überrascht
sind und den chat nicht wieder erkennen."

DIE LISTE HAT DREI FÄCHER. Personen, Gruppen, Kanäle -- als Mulde mit
drei Schaltern über dem Suchfeld, nicht als drei freie Knöpfe: Drei
Dinge, die einander ausschließen, liest man nur als EINE Entscheidung,
wenn sie in einer gemeinsamen Fassung sitzen. Das gewählte Fach liegt
oben auf (Licht, Schatten, Akzentsaum), die anderen liegen darin -- man
sieht die Wahl an der Tiefe, nicht nur an der Farbe. Die Wahl überlebt
das Neuladen; beim Suchen gilt sie nicht, wer einen Namen tippt will
ihn finden und nicht raten, in welchem Fach er liegt.

WAS EIN GESCHLOSSENES FACH NICHT VERSCHLUCKEN DARF: die Ungelesenen und
den Ruf. Beides steht deshalb AM Fach -- die Zahl in der Warnfarbe, das
@ in der Akzentfarbe. Gefunden hat die Lücke nicht ein Blick, sondern
pruef-erwaehnung: Die Erwähnung lag in einer Gruppe, offen war
"Personen", und das @ war damit nirgends zu sehen.

EIN LEERES FACH MERKT MAN SICH NICHT. Wer nur einen Kanal hat -- jeder
Neue im Haus -- landete auf "Personen" und sah eine leere Liste neben
einem vollen Kanal. Beim ersten Zeichnen wird deshalb ins erste Fach
gewechselt, in dem etwas steht; Reihenfolge: gerufen, dann ungelesen,
dann überhaupt vorhanden. Gespeichert wird das NICHT -- es ist geraten,
nicht gewählt. Gefunden von pruef-gifs.

EIN GESICHT IM KOPF DES GESPRÄCHS. Links in der Liste trägt jedes
Gespräch sein Zeichen, und ausgerechnet beim Öffnen verschwand es. Es
ist dasselbe Zeichen, nicht ein ähnliches: `zeichenFuellen()` füllt jetzt
Liste und Kopf -- rund fünfzig Zeilen standen vorher mitten im Zeichnen
und hätten sonst ein zweites Mal dagestanden. Am Handy bleibt es weg,
nachgerechnet: mit ihm blieben dem Namen 126 px bei 128 Untergrenze, die
Knopfreihe fiele eine Zeile tiefer.

JEDER HANDGRIFF BEKOMMT SEIN ZEICHEN. Unter jeder Blase standen fünf
Wörter in Versalien -- bei zwölf Nachrichten sechzig. Jetzt Pfeil,
Gesicht, Papierkorb, Nadel und zwei Blätter, das Wort klein daneben.
Die Wörter bleiben: "anheften" und "lösen" sehen als Nadel gleich aus,
und "löschen (Notfall)" darf nie ein Rätsel sein. Breiter wird es
trotzdem nicht -- gesperrte Versalien kosten rund ein Viertel mehr
Breite, genau das, was die Zeichen brauchen. Die Zeichen sind Masken:
sie folgen `currentColor` und damit jedem Zustand der Schrift daneben.

AUS DER FUSSZEILE WIRD EINE MULDE, und der Grund wird dabei dunkler,
nie heller -- das ist die Bedingung dafür, dass die Kontrastzusage
gültig bleibt. Die Uhrzeit bekommt ein eigenes Schild: eine Angabe,
keine Bedienung.

AUS DEM FARBFLECK WIRD EIN RING. Der Knopf für die eigene Kachel war
ein voller Kreis in der gewählten Farbe, direkt neben einer gleich
großen Marke -- man las ihn als Meldung, und er meldet nichts. Farbe
erscheint auf dieser Seite überall als Kontur; jetzt auch hier.

AUS DEM TOTEN TRENNER WIRD LICHT. Die senkrechte Linie am Verlauf
stammte aus der Zeit, als Liste und Verlauf EIN Kasten waren; seit dem
Umbau auf zwei Tafeln klebte sie ohne Aufgabe an der Kante. An ihrer
Stelle ein sehr weicher Schein oben rechts, unter vier Prozent Deckung
-- Tiefe, kein Leuchten.

DIE KONSOLE: Das Schreibfeld ist eine Rinne statt eines flachen
Kastens, die vier Werkzeuge sprechen dieselbe Sprache, und der
Absendeknopf ist als einziger gefüllt. Keine Maßzahl angefasst -- die
Zeile ist seit dem 23.09. auf den Pixel voll.

ZWEI PRÜFUNGEN WURDEN GENAUER, NICHT NACHSICHTIGER:

  pruef-chatkachel suchte ihre "freie Stelle" nicht, sie rechnete sie
  aus -- 6 px vom rechten Rand, halbe Höhe. Das lag mal auf dem
  Rollbalken, mal auf einer Blase, und meldete beides Mal "das
  Bühnenbild ist gar nicht da". Sie sucht die Stelle jetzt mit
  `elementFromPoint` und sagt es, wenn es keine gibt.

  pruef-erwaehnung prüft jetzt beides: dass das Fach den Ruf meldet,
  ohne geöffnet zu werden, UND dass die Zeile nach dem Wechsel dasteht
  -- mit der Gegenprobe, dass das Fach wirklich filtert. 126 -> 129.

Nachgebessert: die Ungelesen-Marke am Fach stand auf 0,64 rem = 10,24 px,
unter der Hausgrenze von 11,5. Gemeldet von pruef-css-klassen, bevor es
jemand auf einem Telefon sehen musste -- der zweite Anlauf desselben
Reflexes an einem Tag.

Geprüft: pruef-chat-optik, pruef-chatkachel (40), pruef-chat (ALLES IN
ORDNUNG), pruef-chat-neu (32), pruef-chat-ausbau (64),
pruef-chat-kanaele (81), pruef-chat-aufloesen (126),
pruef-chat-anhaenge (109), pruef-erwaehnung (129), pruef-css-klassen,
pruef-tippziele (11), pruef-lesbarkeit. pruef-gifs hat weiterhin die
zwei Fehler, die schon vor diesem Umbau da waren (an HEAD nachgemessen).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-25 02:25:06 +02:00
co-authored by Claude Opus 5
parent d47ccf0d5f
commit 24f9be8e4f
45 changed files with 1608 additions and 725 deletions
+242 -48
View File
@@ -792,15 +792,237 @@
ungelesenMelden();
}
/* ==== DIE DREI FAECHER (25.09.2026) ==============================
Filipe: „ich will 3 kategorien haben. chats mit einzelnen
personen, gruppen chats und kanäle."
DIE ART STEHT SCHON AN JEDEM GESPRAECH (`r.art`: direkt, gruppe,
kanal) -- es gibt also nichts zu erfinden, nur zu sortieren. Genau
deshalb ist diese Liste hier eine ABLEITUNG und keine zweite
Wahrheit: Kaeme morgen eine vierte Art dazu, faellt sie hier auf
(die Zeilen waeren in keinem Fach), statt still zu verschwinden.
GEMERKT WIRD DIE WAHL, und zwar ueber das Neuladen hinaus: Wer im
Kanal arbeitet, arbeitet meist den ganzen Tag im Kanal. Jedes Mal
neu zu waehlen waere ein Griff, den niemand will.
`try` UM DEN SPEICHER: In einem privaten Fenster wirft
localStorage. Ohne den Fang waere die ganze Liste weg -- wegen
einer Bequemlichkeit. */
const FAECHER = [
{ wert: 'direkt', name: 'Personen', leer: 'Noch kein Gespräch mit einer einzelnen Person.' },
{ wert: 'gruppe', name: 'Gruppen', leer: 'Noch keine Gruppe. Mit „Neu" legst du eine an.' },
{ wert: 'kanal', name: 'Kanäle', leer: 'Noch kein Kanal.' },
];
let fach = (() => {
try { return localStorage.getItem('chat-fach') || 'direkt'; } catch { return 'direkt'; }
})();
if (!FAECHER.some((f) => f.wert === fach)) fach = 'direkt';
/* Einmal je Seitenaufruf wird geprueft, ob das gemerkte Fach
ueberhaupt etwas enthaelt -- siehe raeumeZeichnen(). */
let fachGeprueft = false;
function fachWaehlen(wert) {
fach = wert;
try { localStorage.setItem('chat-fach', wert); } catch { /* egal */ }
raeumeZeichnen();
}
/* Die Leiste wird bei jedem Zeichnen neu gebaut -- die Zahlen daran
aendern sich mit jeder Nachricht. */
function faecherZeichnen() {
const leiste = $('chat-arten');
if (!leiste) return;
leiste.textContent = '';
for (const f of FAECHER) {
const drin = raeume.filter((r) => (r.art || 'direkt') === f.wert);
const neu = drin.reduce((n, r) => n + (Number(r.ungelesen) || 0), 0);
/* WURDE ICH IN DIESEM FACH GERUFEN? (25.09.2026)
Gefunden hat das nicht ein Blick, sondern pruef-erwaehnung:
Die Erwaehnung lag in einer GRUPPE, das offene Fach war
„Personen" -- und das @ an der Zeile war damit nirgends zu
sehen. Ein Ruf, den ein geschlossenes Fach verschluckt, ist
schlimmer als gar kein Ruf: Man glaubt, es wartet nichts.
Dieselbe Antwort wie bei den Ungelesenen eine Zeile weiter
oben, und aus demselben Grund. Der Unterschied: Das @ tritt
NICHT zurueck, wenn daneben eine Zahl steht -- es ist die
wichtigere Auskunft von beiden ("es ging an DICH"), genau wie
an der Zeile selbst. */
const gerufen = drin.some((r) => r.erwaehnt);
const k = el('button', 'chat-art');
k.type = 'button';
k.setAttribute('role', 'tab');
k.dataset.art = f.wert;
k.setAttribute('aria-selected', String(f.wert === fach));
/* NICHT AUSWAEHLBAR HEISST NICHT UNSICHTBAR: Ein leeres Fach
bleibt stehen und sagt "hier ist nichts". Waere es weg,
wuesste niemand, dass es die Art ueberhaupt gibt -- und die
Leiste haette mal zwei, mal drei Faecher. */
k.append(el('span', 'chat-art__name', f.name));
if (gerufen) {
const at = el('span', 'chat-art__at', '@');
at.title = 'In diesem Fach wurdest du erwähnt';
/* Ein nacktes "@" wird je nach Vorleseprogramm als "at",
"Klammeraffe" oder gar nicht gelesen -- derselbe Satz wie
an der Zeile. */
at.setAttribute('aria-label', 'In diesem Fach wurdest du erwähnt');
k.append(at);
}
k.append(el('span', 'chat-art__zahl', String(drin.length)));
if (neu) {
/* DIE UNGELESENEN DES FACHS STEHEN AM FACH. Sonst waere ein
geschlossenes Fach ein Ort, an dem Nachrichten liegen
bleiben, ohne dass man es sieht -- der schlimmste Fehler,
den eine Sortierung machen kann. */
const punkt = el('span', 'chat-art__neu', neu > 99 ? '99+' : String(neu));
punkt.setAttribute('aria-label',
neu === 1 ? '1 ungelesene Nachricht' : neu + ' ungelesene Nachrichten');
k.append(punkt);
}
k.addEventListener('click', () => fachWaehlen(f.wert));
/* Mit den Pfeiltasten zwischen den Faechern -- so erwartet es,
wer eine Registerleiste bedient. */
k.addEventListener('keydown', (e) => {
const i = FAECHER.findIndex((x) => x.wert === f.wert);
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
e.preventDefault();
const n = (i + (e.key === 'ArrowRight' ? 1 : FAECHER.length - 1)) % FAECHER.length;
fachWaehlen(FAECHER[n].wert);
$('chat-arten')?.querySelector(`[data-art="${FAECHER[n].wert}"]`)?.focus();
}
});
leiste.append(k);
}
}
/* ==== EIN ZEICHEN, EINE STELLE (25.09.2026) =====================
Das runde Zeichen – Raute für einen Kanal, Zahl für eine Gruppe,
Gesicht oder Anfangsbuchstaben für einen Menschen – stand bisher
nur in der Liste, als rund fünfzig Zeilen mitten im Zeichnen.
Seit der Kopf des Gesprächs dasselbe Zeichen trägt, gäbe es das
zweimal, und die zweite Abschrift ist immer die, in der beim
nächsten Umbau eine Zeile fehlt (siehe 11.09.2026, die
abgeschriebene Spaltenliste – sie hat Daten gekostet).
SIE FÜLLT EIN VORHANDENES ELEMENT, statt eines zu bauen: Der Kopf
hat sein Zeichen fest im HTML und tauscht nur den Inhalt. Deshalb
wird hier ZUERST aufgeräumt – sonst trüge ein Zweiergespräch noch
die Raute des Kanals von vorhin. */
function zeichenFuellen(ziel, r) {
ziel.textContent = '';
delete ziel.dataset.kanal;
delete ziel.dataset.gruppe;
delete ziel.dataset.rolle;
const art = r.art || 'direkt';
if (art === 'kanal') {
/* Das Rautenzeichen ist die Schreibweise, die jeder aus anderen
Messengern kennt -- es muss nicht erklaert werden. */
ziel.dataset.kanal = 'ja';
ziel.textContent = '#';
} else if (art === 'gruppe') {
/* Eine Gruppe zeigt ihre Groesse statt einer Initiale -- der
Name steht ohnehin daneben. */
ziel.dataset.gruppe = 'ja';
ziel.textContent = String(r.teilnehmer.length);
} else {
ziel.textContent = initialen(r.name);
const gegen = r.teilnehmer.find((t) => t.id !== ich.id);
if (gegen) {
/* DAS GESICHT STATT DER INITIALEN (20.09.2026).
Filipe: "da soll man im chat auch die profilfotos von den
leuten sehen wenn die schon eins drin haben."
DER BUCHSTABE BLEIBT ALS UNTERLAGE LIEGEN und wird nicht
ersetzt: Laedt das Bild nicht, steht dort weiterhin etwas
Sinnvolles statt eines kaputten Bildsymbols.
EINMAL, NICHT ZWEIMAL (behoben 24.09.2026): Dieser Block
stand einmal vor und einmal nach der Rollenzeile. Jedes
Gespraech mit Profilbild lud das Bild also zweimal -- zu
sehen war davon nichts, gekostet hat es trotzdem, bei jedem
Neuzeichnen der Liste. */
ziel.dataset.rolle = gegen.rolle;
if (gegen.bild) {
const b = document.createElement('img');
b.src = gegen.bild; b.alt = '';
b.addEventListener('error', () => b.remove());
ziel.append(b);
}
}
}
return ziel;
}
function raeumeZeichnen() {
const ziel = $('raeume');
/* ==== EIN LEERES FACH MERKT MAN SICH NICHT (25.09.2026) =========
Gefunden hat das pruef-gifs, nicht ein Blick: In der Pruefung
gibt es genau EIN Gespraech, den Treff -- einen Kanal. Das
voreingestellte Fach ist „Personen", und dort stand: „Noch kein
Gespräch mit einer einzelnen Person." Die Seite sah leer aus,
obwohl ein voller Kanal danebenlag, nur eine Lasche weiter.
Genau so geht es jedem Neuen im Haus: Der erste Raum, in dem er
landet, ist der Treff -- ein Kanal. Er haette beim ersten
Oeffnen eine leere Liste gesehen.
DIE WAHL WIRD NICHT GESPEICHERT. Das hier ist geraten, nicht
gewaehlt: Sobald jemand selbst auf ein Fach tippt, gilt seine
Wahl und ueberlebt das Neuladen. Wuerde diese Notlage mit
abgespeichert, waere sie beim naechsten Mal nicht mehr von
einer echten Entscheidung zu unterscheiden.
DIE REIHENFOLGE IST NICHT BELIEBIG: erst das Fach, in dem
jemand GERUFEN wurde, dann das mit Ungelesenem, dann das erste,
in dem ueberhaupt etwas steht. Dieselbe Rangfolge wie an jeder
Zeile -- „es ging an dich" wiegt schwerer als „es ist neu",
und das wiegt schwerer als „es ist da".
EINMAL, NICHT BEI JEDEM ZEICHNEN: Die Liste wird bei jeder
eintreffenden Nachricht neu gezeichnet. Ohne den Merker
spraenge das Fach mitten im Lesen um, sobald irgendwo etwas
hereinkommt -- eine Seite, die unter den Haenden wegwandert. */
if (!fachGeprueft && raeume.length) {
fachGeprueft = true;
const inFach = (w, pruefe) => raeume.some(
(r) => (r.art || 'direkt') === w && (!pruefe || pruefe(r)));
if (!inFach(fach)) {
const wahl = FAECHER.find((f) => inFach(f.wert, (r) => r.erwaehnt))
|| FAECHER.find((f) => inFach(f.wert, (r) => r.ungelesen))
|| FAECHER.find((f) => inFach(f.wert));
if (wahl) fach = wahl.wert;
}
}
const suchtext = ($('suche').value || '').trim().toLowerCase();
faecherZeichnen();
/* BEIM SUCHEN GILT DAS FACH NICHT. Wer einen Namen eintippt, will
ihn finden -- und nicht erst raten, in welchem der drei Faecher
er liegt. Ein Treffer, der wegen einer Voreinstellung nicht
erscheint, ist schlimmer als kein Suchfeld. */
const zeigen = suchtext
? raeume.filter((r) => r.name.toLowerCase().includes(suchtext))
: raeume;
: raeume.filter((r) => (r.art || 'direkt') === fach);
ziel.textContent = '';
$('liste-leer').hidden = raeume.length > 0;
/* DER LEER-SATZ GEHOERT ZUM FACH, nicht zur ganzen Liste. Vorher
stand er nur da, wenn es GAR kein Gespraech gab -- ein leeres
Fach neben zwei vollen zeigte einfach nichts an, und das sieht
aus wie ein Fehler beim Laden. */
const leerSatz = $('liste-leer');
if (leerSatz) {
leerSatz.hidden = zeigen.length > 0 || !!suchtext;
if (!zeigen.length && !suchtext) {
leerSatz.textContent = raeume.length
? (FAECHER.find((f) => f.wert === fach)?.leer || 'Hier ist noch nichts.')
: 'Noch keine Gespräche. Mit „Neu" schreibst du der ersten Person.';
}
}
/* WIE VIELE WARTEN AUF MICH (24.09.2026) -- die Summe, nicht die
Zahl der Gespraeche. „3 Gespraeche mit Neuem" und „17 neue
@@ -847,53 +1069,13 @@
const zeichen = el('span', 'chat-raum__zeichen');
zeichen.setAttribute('aria-hidden', 'true');
if (r.art === 'kanal') {
/* Das Rautenzeichen ist die Schreibweise, die jeder aus
anderen Messengern kennt -- es muss nicht erklaert werden. */
zeichen.dataset.kanal = 'ja';
zeichen.textContent = '#';
} else if (r.art === 'gruppe') {
zeichen.dataset.gruppe = 'ja';
zeichen.textContent = String(r.teilnehmer.length);
} else {
zeichen.textContent = initialen(r.name);
const gegen = r.teilnehmer.find((t) => t.id !== ich.id);
if (gegen) {
/* DAS GESICHT STATT DER INITIALEN (20.09.2026).
/* GEBAUT WIRD ES AN EINER STELLE (25.09.2026). Hier standen
rund fuenfzig Zeilen, die seit heute auch der Kopf des
Gespraechs braucht -- siehe zeichenFuellen() weiter oben.
Die Begruendungen zum Profilbild sind dorthin mitgewandert
und nicht verlorengegangen. */
zeichenFuellen(zeichen, r);
Filipe: "da soll man im chat auch die profilfotos von den
leuten sehen wenn die schon eins drin haben."
Im offenen Gespraech stand es laengst -- in der Liste
daneben ein Buchstabe, vom selben Menschen. Der Grund war
eine Zeile im Server, die das Bild wegwarf.
DER BUCHSTABE BLEIBT ALS UNTERLAGE LIEGEN und wird nicht
ersetzt: Laedt das Bild nicht, steht dort weiterhin etwas
Sinnvolles statt eines kaputten Bildsymbols. Dieselbe
Bauweise wie im Verlauf, in der Kopfleiste und in der
Personenauswahl -- vier Stellen, ein Verhalten. */
/* EINMAL, NICHT ZWEIMAL (behoben 24.09.2026).
Dieser Block stand hier doppelt -- Zeichen fuer Zeichen
gleich, einmal vor und einmal nach der Rollenzeile. Jedes
Gespraech mit Profilbild lud das Bild also zweimal und
haengte zwei <img> uebereinander.
Zu SEHEN war davon nichts: Beide liegen deckend
uebereinander und zeigen dasselbe Bild. Gekostet hat es
trotzdem -- doppelte Ladelast bei jedem Neuzeichnen der
Liste, und die wird bei jeder eintreffenden Nachricht neu
gezeichnet. */
zeichen.dataset.rolle = gegen.rolle;
if (gegen.bild) {
const b = document.createElement('img');
b.src = gegen.bild; b.alt = '';
b.addEventListener('error', () => b.remove());
zeichen.append(b);
}
}
}
knopf.append(zeichen);
const mitte = el('span', 'chat-raum__mitte');
@@ -1104,6 +1286,13 @@
antwortAuf = null;
} catch { return; }
/* DAS GESICHT ZUM NAMEN (25.09.2026) -- dieselbe Funktion, die
auch jede Zeile der Liste fuellt. */
const kopfZeichen = $('verlauf-zeichen');
if (kopfZeichen) {
zeichenFuellen(kopfZeichen, offen);
kopfZeichen.hidden = false;
}
$('verlauf-titel').textContent = offen.name;
/* Bei allem, was mehr als zwei Leute hat, stehen die Namen darunter
-- bei einem Zweier-Gespraech die Rolle des Gegenuebers. */
@@ -4041,6 +4230,11 @@
$('gespraech-weg').hidden = true;
$('raum-aufloesen').hidden = true;
$('kanal-leute').hidden = true;
/* UND ES GEHT AUCH WIEDER WEG. Ein Gesicht neben „Wähle ein
Gespräch" gehörte zu einem Gespräch, das gerade geschlossen
wurde – es zeigte auf niemanden. */
const kopfZeichenWeg = $('verlauf-zeichen');
if (kopfZeichenWeg) { kopfZeichenWeg.hidden = true; kopfZeichenWeg.textContent = ''; }
$('verlauf-titel').textContent = 'Wähle ein Gespräch';
$('verlauf-unter').textContent = '';
$('verlauf-leer').hidden = false;