/* ===================================================================== WER IST MIT "@" GEMEINT? (20.09.2026) Filipe: "ich will das man die leute mit @ markieren kann im chat." --------------------------------------------------------------------- WARUM DAS EINE EIGENE DATEI IST Die Auflösung "Text -> Menschen" wird an DREI Stellen gebraucht und muss überall dasselbe ergeben: 1. beim Schreiben -- wer bekommt eine Benachrichtigung, 2. im Browser -- welcher Teil des Satzes wird hervorgehoben, 3. in der Prüfung -- ohne dafür einen Server hochzufahren. Stünde sie in workspace-chat.js, müsste die Prüfung die halbe Anwendung laden, um einen Satz zu zerlegen. Und der Browser hätte ohnehin eine zweite, eigene Fassung -- also genau die Stelle, an der zwei Rechnungen irgendwann auseinanderlaufen und die Seite jemanden farbig hervorhebt, den der Server nie benachrichtigt hat. DIE DATEI HAT DESHALB KEINE ABHÄNGIGKEITEN. Kein Datenbankzugriff, kein Import. Herein kommt ein Text und eine Liste von Menschen, heraus kommt, wer gemeint ist. Der Browser bekommt dieselbe Logik in `chat.js` -- bewusst als kurze, gleichlautende Fassung, die pruef-erwaehnung.mjs Zeile für Zeile gegen diese hier prüft. --------------------------------------------------------------------- DIE REGELN, UND WARUM JEDE EINZELNE NÖTIG IST (1) NUR WER IM RAUM IST. "@Marina" in einem Gespräch, in dem Marina nicht sitzt, ist kein Ansprechen, sondern ein Wort über sie. Sie bekäme sonst eine Meldung zu einem Gespräch, das sie nicht öffnen darf -- eine Benachrichtigung, die auf eine verschlossene Tür führt. (2) DAS ZEICHEN DAVOR DARF KEIN BUCHSTABE UND KEINE ZIFFER SEIN. Ohne diese Regel wird aus "filipe@dogfather-universe.com" ein Ansprechen von Dogfather -- jede E-Mail-Adresse im Chat würde jemanden anpiepsen. Gemessen an einer echten Adresse, siehe Prüfung. (3) DAS ZEICHEN DANACH DARF KEIN BUCHSTABE UND KEINE ZIFFER SEIN. Sonst wäre "@Tili" in "@Tilikum" enthalten. Satzzeichen dagegen schon: "@Tili, kannst du?" und "@Tili!" sind normale Sätze. (4) DER LÄNGSTE PASSENDE NAME GEWINNT. Gäbe es "Van" und "VanVan", träfe die kurze Fassung zuerst und ließe "Van" stehen. Heute hat niemand einen Namen, der im Namen eines anderen steckt -- aber das ist eine Tatsache von heute, kein Gesetz, und sie ändert sich mit dem nächsten Zugang. (5) GROSS UND KLEIN EGAL. Wer "@vanvan" tippt, meint VanVan. Auf einer Handytastatur entscheidet die Autokorrektur mit, nicht der Mensch. (6) JEDE PERSON HÖCHSTENS EINMAL. Dreimal "@Anna" in einem Satz ist trotzdem eine Ansprache. --------------------------------------------------------------------- --------------------------------------------------------------------- "@RUDEL" -- ALLE AUF EINMAL (23.09.2026) Hier stand bis heute: „KEIN @alle. Es wäre in fünf Minuten gebaut und ist der zuverlässigste Weg, dass alle die Benachrichtigungen abschalten -- und dann kommt auch die an, die wirklich für einen bestimmten Menschen gedacht war. Wenn Filipe es ausdrücklich will, gehört dazu eine Entscheidung, WER es benutzen darf; das ist eine Frage an ihn, keine, die ich hier beantworte." Filipe hat sie beantwortet, und zwar genau in dem Punkt, an dem sie offen war: „dan will ich auch dass nur die modis, rechte hand, linke hand und dogfather, alle auch auf einmal markieren können im chat mit einem, @rudel ,dann sollen alle eine benarichtigung bekommen." DIE EINSCHRÄNKUNG IST DIE ANTWORT AUF DIE SORGE. Nicht jeder kann das Rudel rufen -- nur Team Dogi und DogFather. In einem Raum, in dem auch Creator und Scouts sitzen, kann ein Creator „@rudel" schreiben, und es passiert nichts: kein Ruf, keine Hervorhebung, kein Eintrag. WER DARF, ENTSCHEIDET DIESE DATEI NICHT. Sie hat keine Abhängigkeiten und soll keine bekommen (siehe oben). Der Aufrufer sagt es ihr: `erwaehnungenFinden(text, leute, { rudel: true })`. Im Server kommt das aus `istTeamDogi(person)`, der einen Stelle des Hauses dafür. DAS SCHLÜSSELWORT GEWINNT GEGEN EINEN MENSCHEN, DER "RUDEL" HEISST. Heute heißt niemand so -- aber das ist eine Tatsache von heute, kein Gesetz (dieselbe Überlegung wie bei Regel 4). Andersherum wäre der Schaden größer: Ein einziger Zugang mit diesem Namen würde den Ruf ans ganze Team stillschweigend abschalten, und niemand fände je heraus, warum. So herum kostet es nichts -- wer „Rudel" heißt, ist im Rudel ohnehin dabei und bekommt seine Meldung. --------------------------------------------------------------------- AUFRUF (auch aus einer Prüfung heraus): import { erwaehnungenFinden } from "./chat-erwaehnung.js"; ===================================================================== */ /** Das Wort, das alle meint. Klein geschrieben -- verglichen wird * ohnehin kleingeschrieben (Regel 5). */ export const RUDEL_WORT = "rudel"; /** Ein Zeichen, das einen Namen fortsetzen könnte. * * BUCHSTABE ODER ZIFFER, und zwar auch mit Umlaut oder Akzent: * `\p{L}` statt `[a-z]`. Mit `[a-zA-Z0-9]` wäre "@Jose" in "@José" * ein Treffer gewesen -- das "é" hätte als Grenze gezählt. */ const WORTZEICHEN = /[\p{L}\p{N}_]/u; /** Steht an dieser Stelle ein Wortzeichen? * Außerhalb des Textes (Anfang/Ende) ist die Antwort NEIN -- ein * Satzanfang ist eine Grenze wie jede andere. */ function wortzeichenBei(text, i) { if (i < 0 || i >= text.length) return false; return WORTZEICHEN.test(text[i]); } /** * Wer ist in diesem Text mit "@" angesprochen? * * @param {string} text Die geschriebene Nachricht. * @param {Array<{id:number,name:string}>} leute * Die Teilnehmer DIESES Raums. Wer nicht darin steht, kann * nicht gemeint sein -- siehe Regel (1) oben. * @param {{rudel?: boolean}} [wie] * `rudel: true` heisst: WER DAS HIER SCHREIBT, darf das ganze * Rudel rufen. Vorgabe ist `false` -- wer nichts sagt, darf es * nicht. Das ist Absicht: Eine vergessene Angabe soll zu wenig * koennen, nicht zu viel. * @returns {Array<{id:number|null, name:string, von:number, bis:number, rudel?:boolean}>} * Je Treffer die Person und die Stelle im Text (`von` bis * `bis`, ohne das "@"-Zeichen gerechnet wie `slice`). Die * Stellen braucht der Browser zum Hervorheben; der Server * benutzt nur `id`. Sortiert nach Vorkommen im Text. * Beim Rudel ist `id` null und `rudel` true -- es ist keine * Person, sondern alle. */ export function erwaehnungenFinden(text, leute, wie = {}) { const darfRudel = wie?.rudel === true; const satz = String(text || ""); if (!satz.includes("@")) return []; /* REGEL (4): der längste Name zuerst. Einmal sortiert statt bei jedem Treffer neu -- und auf einer Kopie, damit die Liste des Aufrufers unberührt bleibt. */ const nachLaenge = (Array.isArray(leute) ? leute.slice() : []) .filter((p) => p && p.id != null && String(p.name || "").trim()) .sort((a, b) => String(b.name).length - String(a.name).length); if (!nachLaenge.length) return []; const klein = satz.toLowerCase(); const treffer = []; const schonDa = new Set(); // Regel (6) for (let i = klein.indexOf("@"); i !== -1; i = klein.indexOf("@", i + 1)) { /* REGEL (2): Was steht vor dem "@"? */ if (wortzeichenBei(satz, i - 1)) continue; /* DAS RUDEL WIRD ZUERST GEPRUEFT -- und genau das ist die Entscheidung aus dem Kopfkommentar: Das Schluesselwort gewinnt gegen einen Menschen, der "Rudel" heisst. Die Regeln (2), (3) und (5) gelten fuer es genauso wie fuer einen Namen, sonst riefe eine Mailadresse "post@rudelhaus.de" das ganze Team. */ if (darfRudel && klein.startsWith(RUDEL_WORT, i + 1) && !wortzeichenBei(satz, i + 1 + RUDEL_WORT.length)) { treffer.push({ id: null, rudel: true, name: RUDEL_WORT, von: i, bis: i + 1 + RUDEL_WORT.length }); continue; } for (const person of nachLaenge) { const name = String(person.name); const nk = name.toLowerCase(); if (!klein.startsWith(nk, i + 1)) continue; /* REGEL (3): Was steht hinter dem Namen? */ if (wortzeichenBei(satz, i + 1 + name.length)) continue; /* Die Stelle wird IMMER gemeldet, auch wenn dieselbe Person schon einmal vorkam: Der Browser soll jedes "@Anna" im Satz hervorheben, nicht nur das erste. Nur die ID-Liste für die Benachrichtigung wird entdoppelt -- das macht `ideeVon()` weiter unten. */ treffer.push({ id: person.id, name, von: i, bis: i + 1 + name.length }); schonDa.add(person.id); break; // Regel (4): der Längste hat gewonnen } } return treffer; } /** * Nur die Personennummern, jede einmal -- das, was der Server zum * Benachrichtigen braucht. * * ALS EIGENE FUNKTION, damit an der Aufrufstelle nicht steht * `[...new Set(x.map(t => t.id))]`. Diese Zeile wäre an drei Orten * gleich zu schreiben, und an einem davon irgendwann anders. * * DAS RUDEL STEHT HIER NICHT DRIN, und das ist Absicht. "@rudel" ist * keine Liste von Namen im Satz, sondern eine Gruppe -- und WER zu ihr * gehört, ist eine Frage des Hauses, nicht des Textes. Diese Datei hat * keine Abhängigkeiten und kann sie nicht beantworten (sie sähe einen * Raum voller `{id, name}` und wüsste nichts von Rollen). Sie sagt nur, * DASS gerufen wurde -- siehe rudelGerufen() darunter. */ export function erwaehnteIds(text, leute, wie = {}) { return [...new Set(erwaehnungenFinden(text, leute, wie) .filter((t) => t.id != null) .map((t) => t.id))]; } /** Wurde in diesem Text das ganze Rudel gerufen? * * ALS EIGENE FRAGE, weil der Server sie getrennt beantworten muss: * Die Nachricht merkt sich, DASS das Rudel gerufen wurde (Spalte * `rudel`), und daraus entsteht spaeter die Hervorhebung beim Lesen * und der Wortlaut der Meldung. Aus der ID-Liste liesse sich das * nicht ablesen -- in einem Raum zu zweit sieht "alle" genauso aus * wie "einer". * * WEN ES DANN TRIFFT, steht in workspace-chat.js: die Leute im Raum, * die zu Team Dogi gehoeren (istTeamDogi). NICHT jeder im Raum -- im * Treff sitzt die Community, und "Rudel" heisst das Team. Ein Ruf, * der zweihundert Zuschauer weckt, waere etwas anderes als der, den * Filipe bestellt hat. */ export function rudelGerufen(text, leute, wie = {}) { return erwaehnungenFinden(text, leute, wie).some((t) => t.rudel === true); }