Reaktionen: Vorschlaege, ganzer Katalog, eigene Favoriten

Filipe: "soll viel besser und geiler sein und man soll auch auswaehlen
koennen, also paar als vorschlaege mit der moeglichkeit andere
auszuwaehlen und als favoriten zu speichern."

Aus einem Streifen mit sechs Zeichen ist eine Karte geworden:
Vorschlaege oben, Suchfeld, darunter der Katalog in sechs Gruppen --
82 Zeichen, jedes mit deutschen Suchwoertern. Wer "feuer" tippt, muss
nicht scrollen.

DREI LISTEN, DREI FRAGEN, und sie werden gern verwechselt:
  KATALOG    Was gibt es?              fest, workspace-reaktionen.js
  VORSCHLAG  Was steht vorn, solange   fest -- bis jemand eigene
             ich nichts gewaehlt habe? Favoriten hat
  FAVORITEN  Was hat DIESER Mensch     in der Datenbank
             sich gemerkt?

Nur der Katalog entscheidet, was ANGENOMMEN wird. Vorschlag und
Favoriten sind Reihenfolge, keine Erlaubnis -- sonst koennte man sich
ueber einen Favoriten etwas erlauben, das im Katalog nicht steht. Die
erlaubte Menge wird aus dem Katalog ABGELEITET; eine zweite Liste
daneben waere die, in der ein Zeichen fehlt, das die Oberflaeche
anbietet.

FAVORITEN HAENGEN AM MENSCHEN, NICHT AM GERAET. Im Browser abgelegt
waeren sie am Handy andere als am Rechner und beim naechsten Loeschen
der Seitendaten weg.

DER STERN SCHALTET EINEN MODUS, statt an jedem Zeichen einen zweiten
Knopf zu haben: Am Handy gibt es kein "daneben zeigen". Im Merk-Modus
faerbt sich die ganze Tafel, damit niemand reagiert, wenn er merken
wollte. Die Tafel bleibt dabei offen -- acht Favoriten waeren sonst
acht Mal Aufmachen -- und die Vorschlagszeile aendert sich sofort mit.

DIE GRENZE SAGT BESCHEID, statt still den aeltesten wegzuwerfen: Wer
einen neunten merken will, bekommt "nimm erst einen weg". Ein Favorit,
der ohne Ansage verschwindet, sieht wie ein Fehler aus.

Eine Selbstpruefung beim Laden meldet doppelte Zeichen im Katalog und
Vorschlaege, die nicht darin stehen -- beides waere in der Tafel still.

pruef-chat-aufloesen 85 -> 115. Gemessen wird, was PASSIERT, nicht dass
es Elemente gibt: dass die Suche wirklich eingrenzt (1 statt 82) und
das Richtige findet, dass ein Fehlgriff "Nichts zu ... gefunden" sagt
statt eine leere Flaeche zu zeigen, und vor allem, dass im Merk-Modus
KEINE Reaktion entsteht -- an der Zahl der Reaktionen gemessen, nicht
an der Absicht.

NEBENBEFUND AUS DER EIGENEN PRUEFUNG: pruef-css-klassen hat meine
Gruppenueberschrift mit 10,56 px abgelehnt (Grenze 11,5). Richtig so --
eine gesperrte Versalzeile ist ohnehin schwerer zu lesen, die darf
nicht auch noch die kleinste sein. Auf .72rem angehoben, statt die
Grundlinie zu verschieben.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-14 14:04:57 +02:00
co-authored by Claude Opus 5
parent 47269bda21
commit 9f277435a5
35 changed files with 1113 additions and 386 deletions
+90 -4
View File
@@ -47,6 +47,8 @@ import {
fuehrtTeamDogi, MODI_KATEGORIEN, ROLLEN_GRUPPE,
} from "./workspace.js";
import { KATALOG, ERLAUBT, VORSCHLAG, FAVORITEN_MAX } from "./workspace-reaktionen.js";
export const chatRouter = express.Router();
const TEXT_MAX = 4000;
@@ -1223,13 +1225,97 @@ function darfAufloesen(raum, person) {
-- und die zweite Fassung waere die, in der ein Zeichen fehlt, das
der Server noch annimmt.
===================================================================== */
export const REAKTIONEN = ["👍", "❤️", "😂", "😮", "🔥", "🙏"];
/* Die drei Listen stehen in workspace-reaktionen.js -- Katalog,
Vorschlag, erlaubte Zeichen. Hier stehen nur die Wege dorthin. */
/** Die Favoriten EINES Menschen, in seiner Reihenfolge. */
function favoritenVon(personId) {
return db().prepare(
"SELECT zeichen FROM chat_reaktion_favoriten WHERE person_id = ? ORDER BY platz, zeichen")
.all(personId).map((z) => z.zeichen)
/* Was nicht mehr im Katalog steht, faellt still weg -- aber nur
beim LESEN. Die Zeile bleibt stehen; kaeme das Zeichen zurueck,
ist der Favorit wieder da. Ein Loeschen waere eine Entscheidung
ueber fremde Daten wegen einer Katalogaenderung. */
.filter((z) => ERLAUBT.has(z));
}
/* WAS DIE OBERFLAECHE BRAUCHT, IN EINER ANTWORT: die Vorschlagszeile,
den ganzen Katalog mit Suchwoertern und die eigenen Favoriten.
Drei Abrufe daraus zu machen waere sparsamer aussehend und in
Wahrheit teurer: Die Tafel kann erst erscheinen, wenn alle drei da
sind. */
chatRouter.get("/workspace/api/chat/reaktionen/auswahl", (req, res) => {
res.json({ zeichen: REAKTIONEN });
try {
const favoriten = favoritenVon(req.person.id);
res.json({
/* Die Vorschlagszeile sind die FAVORITEN, sobald es welche gibt
-- sonst die Vorgabe. Beides an einer Stelle entschieden,
damit die Oberflaeche nicht selbst waehlen muss. */
zeichen: favoriten.length ? favoriten : VORSCHLAG,
eigene: favoriten.length > 0,
favoriten,
favoriten_max: FAVORITEN_MAX,
katalog: KATALOG,
});
} catch (fehler) {
console.error("[chat] Auswahl:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** Alle Reaktionen zu einer Menge von Nachrichten -- in EINER Abfrage.
/* EINEN FAVORITEN SETZEN ODER WEGNEHMEN -- wieder ein Umschalter,
wie das Reagieren selbst. Ein eigener Weg zum Entfernen waere ein
zweiter Weg, den man absichern muss. */
chatRouter.put("/workspace/api/chat/reaktionen/favorit", gleicheHerkunft,
express.json({ limit: "2kb" }), (req, res) => {
try {
const zeichen = String(req.body?.zeichen ?? "");
if (!ERLAUBT.has(zeichen)) return res.status(400).json({ fehler: "unbekanntes_zeichen" });
const d = db();
const da = d.prepare(
"SELECT 1 FROM chat_reaktion_favoriten WHERE person_id = ? AND zeichen = ?")
.get(req.person.id, zeichen);
if (da) {
d.prepare("DELETE FROM chat_reaktion_favoriten WHERE person_id = ? AND zeichen = ?")
.run(req.person.id, zeichen);
} else {
const wieViele = d.prepare(
"SELECT COUNT(*) AS n FROM chat_reaktion_favoriten WHERE person_id = ?")
.get(req.person.id).n;
if (wieViele >= FAVORITEN_MAX) {
/* NICHT STILL DEN AELTESTEN WEGWERFEN. Wer neun ablegen will,
soll erfahren, dass acht das Maximum sind -- und selbst
entscheiden, welcher geht. Ein Favorit, der ohne Ansage
verschwindet, sieht wie ein Fehler aus. */
return res.status(409).json({
fehler: `Mehr als ${FAVORITEN_MAX} Favoriten passen nicht in die Zeile. `
+ "Nimm erst einen weg.",
favoriten: favoritenVon(req.person.id),
});
}
/* Ans ENDE, nicht an den Anfang: Die vorhandenen sollen nicht
wandern -- man greift sonst daneben. */
const platz = (d.prepare(
"SELECT COALESCE(MAX(platz), 0) AS p FROM chat_reaktion_favoriten WHERE person_id = ?")
.get(req.person.id).p) + 1;
d.prepare(`INSERT INTO chat_reaktion_favoriten (person_id, zeichen, platz)
VALUES (?,?,?)`).run(req.person.id, zeichen, platz);
}
const favoriten = favoritenVon(req.person.id);
res.json({ favoriten, gesetzt: !da,
zeichen: favoriten.length ? favoriten : VORSCHLAG });
} catch (fehler) {
console.error("[chat] Favorit:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });
}
});
/** Alle Reaktionen zu einer Menge von Nachrichten/** Alle Reaktionen zu einer Menge von Nachrichten -- in EINER Abfrage.
*
* Je Nachricht einzeln zu fragen waere bei 200 Nachrichten 200
* Abfragen fuer eine Ansicht. Zurueck kommt eine Karte:
@@ -1275,7 +1361,7 @@ chatRouter.put("/workspace/api/chat/nachrichten/:id/reaktion", gleicheHerkunft,
const id = Number(req.params.id);
if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" });
const zeichen = String(req.body?.zeichen ?? "");
if (!REAKTIONEN.includes(zeichen)) {
if (!ERLAUBT.has(zeichen)) {
return res.status(400).json({ fehler: "unbekanntes_zeichen" });
}