PageSourceSearch

https://www.40gold.de/assets/scripts/chat/core/state-vtZfZaz.js

js 40gold.de collected 2026-10-04 03:32:43 UTC 9,698 bytes, 251 lines download raw bytes

1/*
2 * Reiner In-Memory-State: Partnerliste, Konversationen, Unread-Zaehler.
3 * Keine WebSocket-/DOM-Kenntnis - nur Datenhaltung + kleine Ableitungen.
4 * Nutzt "partnerId" als Konversations-Schluessel; siehe
5 * docs/chat-panel-integration.md ("Protokoll-Migration") fuer den Hinweis,
6 * das nicht an vielen Stellen fest zu verdrahten, falls Phase 6 spaeter auf
7 * ein conversationId-Modell umstellt.
8 */
9
10export class ChatState {
11    partners = new Map(); // partnerId -> partner-Objekt aus GET /chat/partners
12    messages = new Map(); // partnerId -> Array von Message-Objekten (aufsteigend nach id)
13    typing = new Map(); // partnerId -> boolean
14    // Alle gerade geoeffneten Konversationen (nicht nur eine "aktive") - noetig
15    // seit mehrere Konversationsfenster gleichzeitig offen sein koennen (siehe
16    // docs/chat-panel-integration.md, Chat-Panel-Ansichten Modus B/C).
17    openPartnerIds = new Set();
18    // Unterscheidet "noch nie geladen" von "geladen und tatsaechlich leer" -
19    // chat_panel_controller.js#renderPartners() zeigt sonst faelschlich den
20    // Leerzustand (Plus-Button, "Noch keine Unterhaltungen") waehrend des
21    // allerersten Ladens an, obwohl evtl. laengst Kontakte existieren (Nutzer-
22    // Fund 2026-09-16). false bis zum ersten setPartners()-Aufruf, danach
23    // dauerhaft true (auch bei einer tatsaechlich leeren Liste).
24    partnersLoaded = false;
25    // Server liefert die Partnerliste jetzt seitenweise, sortiert nach `sortId`
26    // absteigend (docs/chat-todo.md #6.1) - true, sobald die zuletzt geladene
27    // Seite keine weitere Seite mehr ankuendigt (hasMore: false).
28    partnersExhausted = false;
29    // Aktiver Suchbegriff der Partnerlisten-Suche, '' = kein Filter. Wird von
30    // loadMorePartners() mitgeschickt, damit Nachladen waehrend einer aktiven
31    // Suche dieselbe gefilterte Menge fortsetzt statt auf alle Partner zurueckzufallen.
32    partnersSearch = '';
33    // Archivierte Partner (docs/chat-todo.md §39) - getrennt von der regulaeren Liste, die sie
34    // serverseitig gar nicht enthaelt. archivedCount kommt mit der ersten Seite der regulaeren
35    // Liste; geladen werden die archivierten Partner erst beim Aufklappen des Abschnitts.
36    archivedPartners = new Map();
37    archivedCount = 0;
38    archivedExpanded = false;
39    // Tags pro Konversation (docs/chat-todo.md §43): alle Tags des Nutzers ({id, name, count},
40    // alphabetisch) und die gerade im Suchfeld gewaehlten (IDs, UND-verknuepft). Wie
41    // partnersSearch wird der Filter beim Nachladen und Neuladen mitgeschickt.
42    tags = [];
43    partnersTagFilter = [];
44
45    setPartners(list) {
46        this.partners.clear();
47        for (const partner of list) {
48            this.partners.set(partner.id, partner);
49        }
50        this.partnersLoaded = true;
51    }
52
53    // Ersetzt die komplette Partnerliste durch eine frisch geladene erste Seite
54    // (initiales Laden, Full-Refresh-Trigger wie 'newfriend'/'partnerschanged',
55    // oder eine neue Suche) - Server liefert bereits sortiert, kein Re-Sort noetig.
56    resetPartners(list, hasMore) {
57        this.setPartners(list);
58        this.partnersExhausted = !hasMore;
59    }
60
61    // Haengt eine nachgeladene weitere Seite ans Ende an (Map-Einfuegereihenfolge
62    // = Serverreihenfolge). Pendant zu appendMessages() bei den Nachrichten, nur
63    // ohne Dedupe-Sort, da die Partnerliste nicht nach einer stabilen numerischen
64    // id, sondern nach dem vom Server gelieferten sortId sortiert ist.
65    appendPartnerPage(list, hasMore) {
66        for (const partner of list) {
67            this.partners.set(partner.id, partner);
68        }
69        this.partnersExhausted = !hasMore;
70    }
71
72    // Cursor fuer loadMorePartners(): sortId/id des zuletzt eingefuegten (in
73    // Serverreihenfolge letzten) Partners, oder null bei leerer Liste. Pendant
74    // zu lowestMessageId(), aber aus der Map abgeleitet statt aus einem sortierten
75    // Array, da die Partnerliste keine gesonderte Array-Repraesentation haelt.
76    lowestPartnerCursor() {
77        let last = null;
78        for (const partner of this.partners.values()) {
79            last = partner;
80        }
81        return last ? { sortId: last.sortId ?? 0, partnerId: last.id } : null;
82    }
83
84    getPartner(partnerId) {
85        return this.partners.get(partnerId) ?? this.archivedPartners.get(partnerId);
86    }
87
88    setArchivedPartners(list) {
89        this.archivedPartners.clear();
90        for (const partner of list) {
91            this.archivedPartners.set(partner.id, partner);
92        }
93    }
94
95    isArchived(partnerId) {
96        return this.archivedPartners.has(partnerId);
97    }
98
99    // Stand aus Endpunkt oder 'chatpartnersettings'-Event uebernehmen. Liefert true, wenn sich
100    // dabei Anheften/Archivieren geaendert hat - dann stimmt die Reihenfolge bzw. Aufteilung der
101    // Listen nicht mehr und der Aufrufer laedt neu.
102    applyPartnerSettings(partnerId, settings) {
103        const partner = this.getPartner(partnerId);
104        if (!partner || !settings) {
105            return true;
106        }
107        const listChanged = partner.pinned !== settings.pinned || partner.archived !== settings.archived;
108        partner.muted_until = settings.muted_until;
109        partner.muted_forever = settings.muted_forever;
110        partner.pinned = settings.pinned;
111        partner.archived = settings.archived;
112        return listChanged;
113    }
114
115    // Stumm, solange muted_until (Unix-Sekunden) in der Zukunft liegt - laeuft von selbst ab.
116    isMuted(partnerId) {
117        const until = this.getPartner(partnerId)?.muted_until;
118        return typeof until === 'number' && until * 1000 > Date.now();
119    }
120
121    addPartner(partner) {
122        if (!this.partners.has(partner.id)) {
123            this.partners.set(partner.id, partner);
124        }
125        return this.partners.get(partner.id);
126    }
127
128    patchPartnerStatus(partnerId, status) {
129        const partner = this.partners.get(partnerId);
130        if (partner) {
131            partner.status = status;
132        }
133    }
134
135    incrementUnread(partnerId) {
136        const partner = this.partners.get(partnerId);
137        if (partner) {
138            partner.unread = (partner.unread ?? 0) + 1;
139        }
140    }
141
142    get totalUnread() {
143        let sum = 0;
144        for (const partner of this.partners.values()) {
145            sum += partner.unread ?? 0;
146        }
147        return sum;
148    }
149
150    getMessages(partnerId) {
151        return this.messages.get(partnerId) ?? [];
152    }
153
154    setMessages(partnerId, list) {
155        this.messages.set(partnerId, [...list].sort(compareMessages));
156    }
157
158    prependHistory(partnerId, olderMessages) {
159        const existing = this.getMessages(partnerId);
160        const merged = [...olderMessages, ...existing];
161        this.setMessages(partnerId, dedupeById(merged));
162    }
163
164    appendMessages(partnerId, newMessages) {
165        const existing = this.getMessages(partnerId);
166        const merged = [...existing, ...newMessages];
167        this.setMessages(partnerId, dedupeById(merged));
168    }
169
170    replaceOptimisticMessage(partnerId, guid, serverMessage) {
171        const list = this.getMessages(partnerId);
172        const idx = list.findIndex((m) => m.guid === guid);
173        if (idx !== -1) {
174            list[idx] = { ...list[idx], ...serverMessage };
175        }
176    }
177
178    removeOptimisticMessage(partnerId, guid) {
179        const list = this.getMessages(partnerId);
180        this.messages.set(partnerId, list.filter((m) => m.guid !== guid));
181    }
182
183    highestMessageId(partnerId) {
184        const list = this.getMessages(partnerId);
185        return list.length ? list[list.length - 1].id : -1;
186    }
187
188    lowestMessageId(partnerId) {
189        const list = this.getMessages(partnerId);
190        return list.length ? list[0].id : -1;
191    }
192
193    // Eigene, an partnerId gesendete Nachrichten als von ihm gelesen markieren
194    // (id <= seenUntil) - ausgeloest durch dessen 'seen'-WS-Envelope (siehe
195    // chat_client.js#handleMessage(), Fall 'direct'/'seen'). Nur 'out'-
196    // Nachrichten betroffen, das ist der einzige Zweck des Lesestatus
197    // (chat_conversation_controller.js#renderSeenIndicator()).
198    markSeenUntil(partnerId, seenUntil) {
199        for (const message of this.getMessages(partnerId)) {
200            if (message.direction === 'out' && message.id !== undefined && message.id <= seenUntil) {
201                message.seen = 2;
202            }
203        }
204    }
205
206    setTyping(partnerId, isTyping) {
207        this.typing.set(partnerId, isTyping);
208    }
209
210    isTyping(partnerId) {
211        return this.typing.get(partnerId) === true;
212    }
213
214    openPartner(partnerId) {
215        this.openPartnerIds.add(partnerId);
216    }
217
218    closePartner(partnerId) {
219        this.openPartnerIds.delete(partnerId);
220    }
221
222    isOpen(partnerId) {
223        return this.openPartnerIds.has(partnerId);
224    }
225}
226
227function dedupeById(list) {
228    const seen = new Set();
229    const result = [];
230    for (const item of list) {
231        if (item.id === undefined || !seen.has(item.id)) {
232            if (item.id !== undefined) {
233                seen.add(item.id);
234            }
235            result.push(item);
236        }
237    }
238    return result.sort(compareMessages);
239}
240
241// Nachrichten ohne (noch) bestaetigte Server-id sind optimistisch angehaengte,
242// gerade erst selbst versendete Nachrichten (siehe replaceOptimisticMessage) -
243// muessen ans Ende sortiert werden, nicht per "?? 0" an den Anfang (kleiner
244// als jede echte id). Untereinander per stabilem Sort (result 0) in
245// Einfuege-, also Versandreihenfolge belassen.
246function compareMessages(a, b) {
247    if (a.id === undefined && b.id === undefined) return 0;
248    if (a.id === undefined) return 1;
249    if (b.id === undefined) return -1;
250    return a.id - b.id;
251}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.