Mit "@" jemanden im Chat ansprechen
Filipe: "ich will das man die leute mit @ markieren kann im chat."
MARKIEREN IST KEINE FARBE, SONDERN EINE ANSPRACHE. Sie funktioniert
nur, wenn drei Dinge zusammen stimmen: Der Richtige ist gemeint, er
MERKT es, und man sieht es im Satz. Fehlt eines davon, ist es eine
Attrappe -- und zwar eine, die gut aussieht.
WER GEMEINT IST, ENTSCHEIDET DER SERVER. Der Browser schickt nur Text.
Damit wirkt ein von Hand getipptes "@VanVan" genauso wie eines aus der
Auswahlliste, und niemand kann jemanden ansprechen, der gar nicht im
Raum sitzt -- auch nicht ueber die Schnittstelle.
Die Regeln stehen in server/chat-erwaehnung.js, jede mit Begruendung.
Zwei davon sind keine Feinheit, sondern der Unterschied zwischen
brauchbar und laestig:
* Das Zeichen VOR dem "@" darf kein Buchstabe sein. Sonst piepst
jede E-Mail-Adresse im Chat jemanden an
("[email protected]").
* Das Zeichen DANACH auch nicht, und der laengste Name gewinnt.
Sonst spricht "@Tilikum" Tili an.
MAN MERKT ES AUCH OHNE OFFENEN RAUM. Eine Zahl in der Gespraechsliste
sagt "hier ist etwas" -- nicht, ob es an dich war. Wer morgens vier
Raeume mit Zahlen sieht, macht den lautesten zuerst auf, und dort
steht selten das, was auf ihn wartet. Jetzt steht ein "@" daneben.
UND DIE MELDUNG AUFS HANDY SAGT ES: "VanVan hat dich erwaehnt" statt
"Nachricht von VanVan". Als EIGENE Art, die sich getrennt abschalten
laesst -- wer den lauten Chat stumm stellt, will trotzdem wissen, wenn
ihn jemand direkt anspricht. Ohne Ausnahme von der Ruhezeit: Ein Anruf
wartet auf eine Antwort, ein "@Anna" nicht.
DIE AUSWAHL BEIM TIPPEN loest drei Dinge, die man sonst selbst wissen
muesste: wie die Person genau heisst, wer ueberhaupt im Raum ist, und
ob man sich vertippt hat. Tastatur zuerst (Pfeile, Enter, Tab, Escape).
Sie erzwingt nichts -- wer weitertippt, schreibt einfach weiter.
ZWEI RECHNUNGEN, EINE ANTWORT: Der Browser braucht dieselbe Aufloesung,
um sofort hervorzuheben. Genau dort laufen Dinge auseinander, und dann
haette die Seite jemanden hervorgehoben, den niemand benachrichtigt
hat. pruef-erwaehnung LIEST die Browserfassung aus der Datei und fuehrt
sie aus -- ein Nachbau wuerde pruefen, ob ich zweimal dasselbe
schreiben kann.
---- DABEI GEFUNDEN: "gelesen bis 999999" ----------------------------
Der Server nahm fuer den Lesestand JEDE ganze Zahl an. Wer einmal eine
Zahl hinter allem Vorhandenen schickte, sah in diesem Gespraech nie
wieder einen Zaehler und nie wieder ein @-Zeichen: Alles Kuenftige galt
als gelesen, bevor es geschrieben war. Das faellt niemandem als
Zusammenhang auf -- man merkt nur, dass "die Benachrichtigungen nicht
gehen".
Der Browser schickt heute immer die letzte gesehene Nummer, ist also
nicht der Grund. Aber eine Grenze, die nur davon lebt, dass der
Aufrufer sich benimmt, ist keine. Jetzt wird auf die juengste
Nachricht des Raums begrenzt -- abgeleitet, nicht geraten.
Gefunden hat es meine eigene Pruefung: Sie setzte zum Aufraeumen 99999
und wunderte sich danach, warum das frische "@" nicht leuchtete.
WOHER MAN ERFAEHRT, DASS ES DAS GIBT: Im Chat gibt es keinen
Hilfeknopf. Der Hinweis steht deshalb im Leersatz einer neuen Gruppe --
dort, wo man beim ersten Oeffnen ohnehin hinsieht, und nur ab drei
Leuten. In einem Zweiergespraech waere er Ballast.
GEPRUEFT: pruef-erwaehnung 67/0 (neu) -- 13 Regelfaelle ohne Server,
der Weg durch die Schnittstelle, das Zeichen in der Liste samt
Gegenprobe bei jemandem, der dabei aber nicht gemeint war, beide
Aufloesungen nebeneinander, und der ganze Ablauf am echten Bildschirm
bis "Enter waehlt und schickt NICHT ab".
Dazu pruef-chat 48/0, pruef-chat-optik 36/0, pruef-treffchat 110/0,
pruef-chat-kanaele 79/0, pruef-glocke 31/0, pruef-push 24/0,
pruef-anruf-klingelt, pruef-css-klassen, pruef-meldungen,
pruef-nachfrage, pruef-tippziele.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+142
-10
@@ -47,6 +47,7 @@ import {
|
||||
fuehrtTeamDogi, MODI_KATEGORIEN, ROLLEN_GRUPPE,
|
||||
} from "./workspace.js";
|
||||
|
||||
import { erwaehnteIds } from "./chat-erwaehnung.js";
|
||||
import { KATALOG, ERLAUBT, VORSCHLAG, FAVORITEN_MAX } from "./workspace-reaktionen.js";
|
||||
|
||||
/* Der Treff-Chat: eigener Raum, eigene Nachtruhe, keine Anrufe.
|
||||
@@ -505,6 +506,32 @@ chatRouter.get("/workspace/api/chat/raeume", (req, res) => {
|
||||
WHERE raum_id = ? AND id > ? AND id > ? AND person_id <> ?`)
|
||||
.get(r.id, r.gelesen_bis, r.geloescht_bis, req.person.id).n;
|
||||
|
||||
/* STEHT IN DEN UNGELESENEN EIN "@" AN MICH? (20.09.2026)
|
||||
|
||||
Eine Zahl sagt "hier ist etwas". Sie sagt nicht, ob es an
|
||||
DICH war. Wer morgens vier Raeume mit Zahlen sieht, macht
|
||||
den lautesten zuerst auf -- und genau dort steht selten das,
|
||||
was auf ihn wartet. Das Zeichen daneben dreht die Reihenfolge
|
||||
um.
|
||||
|
||||
DIESELBEN ZWEI GRENZEN wie bei der Zahl darueber: was schon
|
||||
gelesen ist, und was weggeraeumt wurde. Ohne die zweite
|
||||
leuchtete ein Gespraech wieder auf, das jemand bewusst
|
||||
beiseitegelegt hat.
|
||||
|
||||
UND `weg_am IS NULL`: Eine zurueckgenommene Nachricht spricht
|
||||
niemanden mehr an. Der Eintrag bleibt stehen (er gehoert zur
|
||||
Nachricht), zaehlt hier aber nicht mehr mit -- gerechnet wird
|
||||
beim Lesen, nicht beim Zuruecknehmen, damit es keine zweite
|
||||
Stelle gibt, die man vergessen kann. */
|
||||
const erwaehnt = !!db().prepare(`
|
||||
SELECT 1 FROM chat_erwaehnungen e
|
||||
JOIN chat_nachrichten n ON n.id = e.nachricht_id
|
||||
WHERE e.person_id = ? AND n.raum_id = ?
|
||||
AND n.id > ? AND n.id > ? AND n.weg_am IS NULL
|
||||
LIMIT 1`)
|
||||
.get(req.person.id, r.id, r.gelesen_bis, r.geloescht_bis);
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
art: r.art,
|
||||
@@ -531,6 +558,7 @@ chatRouter.get("/workspace/api/chat/raeume", (req, res) => {
|
||||
von: letzte.von, erstellt: letzte.erstellt,
|
||||
} : null,
|
||||
ungelesen,
|
||||
erwaehnt,
|
||||
};
|
||||
});
|
||||
/* WELCHE KANAELE ES NOCH NICHT GIBT -- und nur fuer die, die sie
|
||||
@@ -1042,11 +1070,45 @@ chatRouter.post("/workspace/api/chat/raeume/:id/nachrichten", gleicheHerkunft,
|
||||
anhang: null,
|
||||
};
|
||||
|
||||
/* ==== WER IST MIT "@" GEMEINT? (20.09.2026) =====================
|
||||
|
||||
Aufgeloest wird HIER, gegen die Teilnehmer DIESES Raums -- nicht
|
||||
im Browser. Der Browser schickt nur Text. Wer daraus eine
|
||||
Person wird, entscheidet der Server, und damit gilt:
|
||||
|
||||
* Ein von Hand getipptes "@Anna" wirkt genauso wie eines aus
|
||||
der Auswahlliste. Wer schnell schreibt, soll nicht bestraft
|
||||
werden.
|
||||
* Niemand kann jemanden ansprechen, der gar nicht im Raum
|
||||
sitzt -- auch nicht, indem er die Schnittstelle direkt
|
||||
anspricht.
|
||||
|
||||
Die Regeln stehen in chat-erwaehnung.js, samt Begruendung je
|
||||
Regel. Sie sind bewusst ohne Abhaengigkeiten, damit die
|
||||
Pruefung sie lesen kann, ohne einen Server hochzufahren. */
|
||||
const alleLeute = teilnehmerVon(raumId);
|
||||
const erwaehnt = erwaehnteIds(text, alleLeute)
|
||||
/* SICH SELBST ANSPRECHEN LOEST NICHTS AUS. Man schreibt den
|
||||
eigenen Namen durchaus ("@Anna hat recht, ich auch") -- eine
|
||||
Meldung an sich selbst waere Unsinn. */
|
||||
.filter((pid) => pid !== req.person.id);
|
||||
if (erwaehnt.length) {
|
||||
const einf = d.prepare(
|
||||
"INSERT OR IGNORE INTO chat_erwaehnungen (nachricht_id, person_id) VALUES (?,?)");
|
||||
for (const pid of erwaehnt) einf.run(id, pid);
|
||||
}
|
||||
|
||||
/* Sofort an alle, die gerade zusehen -- und per Benachrichtigung
|
||||
an die, die es nicht tun. Beides läuft über chatEreignis(),
|
||||
damit die Entscheidung "wer bekommt was" an einer Stelle steht. */
|
||||
const empfaenger = teilnehmerVon(raumId).filter((t) => t.id !== req.person.id);
|
||||
chatEreignis(raumId, nachricht, empfaenger.map((t) => t.id));
|
||||
damit die Entscheidung "wer bekommt was" an einer Stelle steht.
|
||||
Auch die Erwaehnung: Sie ist keine ZWEITE Meldung, sondern eine
|
||||
ANDERE -- wer angesprochen wurde, bekommt statt "Nachricht von
|
||||
Anna" ein "Anna hat dich erwaehnt". Zwei Meldungen fuer eine
|
||||
Nachricht waeren der schnellste Weg, dass jemand beide
|
||||
abschaltet. */
|
||||
const empfaenger = alleLeute.filter((t) => t.id !== req.person.id);
|
||||
chatEreignis(raumId, nachricht, empfaenger.map((t) => t.id), "neu",
|
||||
new Set(erwaehnt));
|
||||
|
||||
res.status(201).json({ nachricht });
|
||||
} catch (fehler) {
|
||||
@@ -1696,12 +1758,42 @@ chatRouter.put("/workspace/api/chat/raeume/:id/gelesen", gleicheHerkunft,
|
||||
return res.status(400).json({ fehler: "ungueltig" });
|
||||
}
|
||||
if (!istDrin(raumId, req.person)) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
/* ==== NICHT WEITER ALS BIS ZUR LETZTEN NACHRICHT (20.09.2026) ====
|
||||
|
||||
Hier stand nur `Number.isInteger(bis)`. Damit liess sich der
|
||||
Stand auf JEDE Zahl setzen -- auch auf eine, die weit hinter
|
||||
allem liegt, was es gibt.
|
||||
|
||||
Die Folge ist still und dauerhaft: Wer einmal "gelesen bis
|
||||
999999" schickt, hat in diesem Gespraech nie wieder einen
|
||||
Zaehler und nie wieder ein @-Zeichen. Alles Kuenftige ist
|
||||
schon gelesen, bevor es geschrieben wurde. Niemand wuerde den
|
||||
Zusammenhang je finden -- man merkt nur, dass "die
|
||||
Benachrichtigungen nicht gehen".
|
||||
|
||||
Gefunden beim Bauen der Erwaehnungen: Die Pruefung setzte zum
|
||||
Aufraeumen 99999 und wunderte sich danach, warum das frische
|
||||
"@" nicht leuchtete. Der Fehler lag nicht in der Erwaehnung.
|
||||
|
||||
Der Browser schickt heute immer die Nummer der letzten
|
||||
gesehenen Nachricht -- er ist also nicht der Grund. Aber eine
|
||||
Grenze, die nur davon lebt, dass der Aufrufer sich benimmt,
|
||||
ist keine.
|
||||
|
||||
ABGELEITET STATT GERATEN: Weiter als bis zur juengsten
|
||||
Nachricht dieses Raums kann niemand gelesen haben. */
|
||||
const neueste = db().prepare(
|
||||
"SELECT COALESCE(MAX(id), 0) AS n FROM chat_nachrichten WHERE raum_id = ?")
|
||||
.get(raumId).n;
|
||||
const stand = Math.max(0, Math.min(bis, neueste));
|
||||
|
||||
/* NUR VORWÄRTS. Ein zu spät eintreffender Aufruf einer älteren
|
||||
Ansicht darf den Stand nicht zurückdrehen -- sonst stünde eine
|
||||
gelesene Nachricht wieder als ungelesen da. */
|
||||
db().prepare(`UPDATE chat_teilnehmer SET gelesen_bis = ?
|
||||
WHERE raum_id = ? AND person_id = ? AND gelesen_bis < ?`)
|
||||
.run(bis, raumId, req.person.id, bis);
|
||||
.run(stand, raumId, req.person.id, stand);
|
||||
res.json({ ok: true });
|
||||
} catch (fehler) {
|
||||
console.error("[chat] Gelesen:", fehler?.message);
|
||||
@@ -1863,7 +1955,15 @@ chatRouter.delete("/workspace/api/chat/nachrichten/:id", gleicheHerkunft, (req,
|
||||
* Eine Person kann an zwei Geräten sitzen. */
|
||||
const zuschauer = new Map();
|
||||
|
||||
function chatEreignis(raumId, nachricht, benachrichtigen, art = "neu") {
|
||||
/**
|
||||
* @param erwaehnte Wer in DIESER Nachricht mit "@" angesprochen wurde.
|
||||
* Diese Menge bekommt eine ANDERE Meldung, nicht eine zweite
|
||||
* (20.09.2026). Sie steht hier und nicht an der Aufrufstelle,
|
||||
* weil der Kommentar unten es verlangt: Die Entscheidung "wer
|
||||
* bekommt was" gehoert an EINE Stelle.
|
||||
*/
|
||||
function chatEreignis(raumId, nachricht, benachrichtigen, art = "neu",
|
||||
erwaehnte = new Set()) {
|
||||
/* 1. An alle offenen Verbindungen der Beteiligten. */
|
||||
const leute = teilnehmerVon(raumId).map((t) => t.id);
|
||||
for (const personId of leute) {
|
||||
@@ -1898,7 +1998,12 @@ function chatEreignis(raumId, nachricht, benachrichtigen, art = "neu") {
|
||||
sehen und behandelte deshalb JEDES Ereignis als Chatnachricht.
|
||||
Bei einem Anruf kam dabei woertlich „Nachricht von [object
|
||||
Object]" heraus. */
|
||||
void meldeNeueNachricht(personId, raumId, nachricht, art);
|
||||
/* ANGESPROCHEN ODER NUR DABEI? Der Unterschied entscheidet ueber
|
||||
Wortlaut UND Art -- und die Art kann jeder einzeln abschalten.
|
||||
Wer den Chat stumm stellt, weil dort viel geredet wird, will
|
||||
trotzdem wissen, wenn ihn jemand direkt anspricht. */
|
||||
void meldeNeueNachricht(personId, raumId, nachricht,
|
||||
art === "neu" && erwaehnte.has(personId) ? "erwaehnung" : art);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1946,9 +2051,30 @@ export function pushTextFuer(art, nachricht, raumId) {
|
||||
};
|
||||
}
|
||||
|
||||
const wer = typeof nachricht.von === "string"
|
||||
? nachricht.von : (nachricht.von?.name || "jemandem");
|
||||
|
||||
/* ---- ANGESPROCHEN IST ETWAS ANDERES ALS DABEI (20.09.2026) ----
|
||||
|
||||
"Nachricht von Anna" sagt nicht, dass sie DICH meint. In einem
|
||||
Raum mit acht Leuten ist das der Unterschied zwischen "kann
|
||||
warten" und "gleich nachsehen" -- und genau diesen Unterschied
|
||||
soll das Markieren mit "@" machen. Steht er nicht in der Meldung,
|
||||
ist das Markieren nur Farbe.
|
||||
|
||||
Der Anfang der Nachricht bleibt darunter stehen, wie sonst auch:
|
||||
Wer auf dem Sperrbildschirm liest, soll nicht oeffnen muessen, um
|
||||
zu erfahren, ob es wichtig war. */
|
||||
if (art === "erwaehnung") {
|
||||
return {
|
||||
titel: `${wer} hat dich erwähnt`,
|
||||
text: String(nachricht.text || "").slice(0, 120),
|
||||
ziel,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
titel: `Nachricht von ${typeof nachricht.von === "string"
|
||||
? nachricht.von : (nachricht.von?.name || "jemandem")}`,
|
||||
titel: `Nachricht von ${wer}`,
|
||||
/* Der Anfang der Nachricht steht in der Meldung -- eine
|
||||
Benachrichtigung, die nur "Neue Nachricht" sagt, zwingt zum
|
||||
Öffnen, um zu erfahren, ob es wichtig war. */
|
||||
@@ -1987,8 +2113,14 @@ async function meldeNeueNachricht(personId, raumId, nachricht, art = "neu") {
|
||||
wirklich klingelt. */
|
||||
/* ALS EIGENE ART, wenn es ein Anruf ist. Sie umgeht die Ruhezeit
|
||||
und laesst sich getrennt abschalten -- siehe workspace-push.js. */
|
||||
await benachrichtige(personId, art === "anruf" ? "anruf" : "chat_nachricht",
|
||||
pushTextFuer(art, nachricht, raumId));
|
||||
/* DREI ARTEN, DREI SCHALTER. Jede laesst sich einzeln abschalten
|
||||
(siehe ARTEN in workspace-push.js) -- und genau darum geht es:
|
||||
Wer den lauten Chat stumm stellt, verliert damit weder Anrufe
|
||||
noch die Faelle, in denen ihn jemand direkt anspricht. */
|
||||
const pushArt = art === "anruf" ? "anruf"
|
||||
: art === "erwaehnung" ? "chat_erwaehnung"
|
||||
: "chat_nachricht";
|
||||
await benachrichtige(personId, pushArt, pushTextFuer(art, nachricht, raumId));
|
||||
} catch (fehler) {
|
||||
console.error("[chat] Benachrichtigung:", fehler?.message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user