PageSourceSearch

https://grigsi.com/js/sendi-chat-live-notices.js?v=15

js grigsi.com collected 2026-09-24 14:45:21 UTC 27,687 bytes, 790 lines download raw bytes

1"use strict";
2
3/**
4 * Interactive live notices in Space chat — screen share, video, files, incoming calls.
5 * Remote AV calls ring first — Accept / Decline / Skip before connecting.
6 */
7(function (global) {
8  const KIND_META = {
9    screen: {
10      icon: "fa-display",
11      verb: "opened a live window",
12      ringVerb: "wants to share their screen",
13      cta: "Watch screen",
14      accent: "screen",
15    },
16    video: {
17      icon: "fa-video",
18      verb: "turned the camera on",
19      ringVerb: "is calling you (video)",
20      cta: "Join video",
21      accent: "video",
22    },
23    audio: {
24      icon: "fa-phone",
25      verb: "started a voice call",
26      ringVerb: "is calling you",
27      cta: "Listen",
28      accent: "video",
29    },
30    file: {
31      icon: "fa-file",
32      verb: "shared a file",
33      ringVerb: "shared a file",
34      cta: "Open menu",
35      accent: "file",
36    },
37  };
38
39  const active = new Map();
40  let hooks = null;
41  let chatRoot = null;
42  let chatScroll = null;
43
44  function resolveChatDom() {
45    chatScroll = document.getElementById("chat-messages");
46    chatRoot =
47      document.getElementById("chat-messages-feed") || chatScroll;
48  }
49
50  function now() {
51    return Date.now();
52  }
53
54  function noticeKey(kind, peerId, fileId) {
55    if (kind === "file") return "file|" + String(fileId || "");
56    return String(kind || "media") + "|" + String(peerId || "");
57  }
58
59  function isRemoteAvCall(record) {
60    if (!record || record.kind === "file") return false;
61    if (String(record.peerId || "").trim() === "local") return false;
62    return record.kind === "audio" || record.kind === "video" || record.kind === "screen";
63  }
64
65  function isLocalOutgoingCall(record) {
66    if (!record || record.kind === "file") return false;
67    return String(record.peerId || "").trim() === "local";
68  }
69
70  function getPeerName(peerId, fallback) {
71    if (hooks && hooks.getPeerName) {
72      const n = hooks.getPeerName(peerId);
73      if (n) return n;
74    }
75    return String(fallback || "Someone").trim() || "Someone";
76  }
77
78  function formatTime() {
79    try {
80      return new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
81    } catch (_) {
82      return "";
83    }
84  }
85
86  function scrollChat() {
87    if (!chatScroll) resolveChatDom();
88    if (!chatScroll) return;
89    chatScroll.scrollTop = chatScroll.scrollHeight;
90  }
91
92  function defaultRingHint(kind) {
93    if (kind === "audio") return "Voice call — Accept, Decline, or Skip.";
94    if (kind === "screen") return "Screen share — Accept, Decline, or Skip.";
95    return "Video call — Accept, Decline, or Skip.";
96  }
97
98  function setState(record, state, hint, ctaLabel) {
99    record.state = state;
100    if (hint != null) record.hint = hint;
101    if (ctaLabel != null) record.cta = ctaLabel;
102    record.at = now();
103    renderRecord(record);
104    scrollChat();
105  }
106
107  function noticeSummaryMeta(record, pill, hint) {
108    const bits = [pill];
109    const h = String(hint || "").trim();
110    if (h) bits.push(h.length > 48 ? h.slice(0, 45) + "…" : h);
111    return bits.join(" · ");
112  }
113
114  function addIncomingActionBtn(bar, label, variant, onClick) {
115    const btn = document.createElement("button");
116    btn.type = "button";
117    btn.className = "chat-live-notice__incoming-btn chat-live-notice__incoming-btn--" + variant;
118    btn.textContent = label;
119    btn.addEventListener("click", (e) => {
120      e.preventDefault();
121      e.stopPropagation();
122      onClick();
123    });
124    bar.appendChild(btn);
125    return btn;
126  }
127
128  function syncIncomingCallActions(record) {
129    if (!record.el || !isRemoteAvCall(record)) {
130      if (record.el) record.el.classList.remove("chat-live-notice--ringing");
131      return;
132    }
133    const main = record.el.querySelector(".chat-live-notice__main");
134    if (!main) return;
135    let bar = main.querySelector(".chat-live-notice__incoming-actions");
136    const showBar = record.state === "ringing" || record.state === "skipped";
137    if (!showBar) {
138      if (bar) bar.remove();
139      record.el.classList.remove("chat-live-notice--ringing");
140      return;
141    }
142    record.el.classList.toggle("chat-live-notice--ringing", record.state === "ringing");
143    if (!bar) {
144      bar = document.createElement("div");
145      bar.className = "chat-live-notice__incoming-actions";
146      bar.setAttribute("role", "group");
147      bar.setAttribute("aria-label", "Incoming call actions");
148      main.appendChild(bar);
149    }
150    bar.innerHTML = "";
151    if (record.state === "ringing") {
152      addIncomingActionBtn(bar, "Accept", "accept", () => void onAcceptCall(record));
153      addIncomingActionBtn(bar, "Decline", "decline", () => onDeclineCall(record));
154      addIncomingActionBtn(bar, "Skip", "skip", () => onSkipCall(record));
155    } else if (record.state === "skipped") {
156      addIncomingActionBtn(bar, "Accept", "accept", () => void onAcceptCall(record));
157      addIncomingActionBtn(bar, "Dismiss", "skip", () =>
158        setState(record, "ended", "Call dismissed.")
159      );
160    }
161  }
162
163  function syncOutgoingCallActions(record) {
164    if (!record.el || !isLocalOutgoingCall(record)) return;
165    const main = record.el.querySelector(".chat-live-notice__main");
166    if (!main) return;
167    let bar = main.querySelector(".chat-live-notice__outgoing-actions");
168    const showBar =
169      record.state === "calling" || record.state === "connecting" || record.state === "ready";
170    if (!showBar || record.state === "ended") {
171      if (bar) bar.remove();
172      record.el.classList.remove("chat-live-notice--calling");
173      return;
174    }
175    record.el.classList.toggle("chat-live-notice--calling", record.state === "calling");
176    if (!bar) {
177      bar = document.createElement("div");
178      bar.className = "chat-live-notice__outgoing-actions chat-live-notice__incoming-actions";
179      bar.setAttribute("role", "group");
180      bar.setAttribute("aria-label", "Outgoing call actions");
181      main.appendChild(bar);
182    }
183    bar.innerHTML = "";
184    addIncomingActionBtn(bar, "End call", "decline", () => onEndOutgoingCall(record));
185  }
186
187  function renderRecord(record) {
188    const el = record.el;
189    if (!el) return;
190    const meta = KIND_META[record.kind] || KIND_META.video;
191    const titleEl = el.querySelector(".space-object-card__title");
192    const metaEl = el.querySelector(".space-object-card__meta");
193    const ctaEl = el.querySelector(".space-object-card__live-cta");
194
195    const name = record.peerName || "Someone";
196    let title = "";
197    if (record.kind === "file") {
198      title = `${name} ${meta.verb}: ${record.fileName || "file"}`;
199    } else if (record.state === "ringing") {
200      title = `${name} ${meta.ringVerb || meta.verb}`;
201    } else if (isLocalOutgoingCall(record) && record.state === "calling") {
202      title =
203        record.kind === "audio"
204          ? "Calling…"
205          : record.kind === "screen"
206            ? "Sharing screen…"
207            : "Calling (video)…";
208    } else if (isLocalOutgoingCall(record)) {
209      title =
210        record.kind === "audio"
211          ? "Voice call"
212          : record.kind === "screen"
213            ? "Screen share"
214            : "Video call";
215    } else {
216      title = `${name} ${meta.verb}`;
217    }
218    if (titleEl) titleEl.textContent = title;
219
220    let hint = record.hint || "";
221    let pill = "Live";
222    let cta = record.cta || meta.cta;
223    let disabled = false;
224
225    if (record.state === "ringing") {
226      pill = "Incoming";
227      hint = hint || defaultRingHint(record.kind);
228      cta = "Choose below";
229      disabled = true;
230    } else if (record.state === "calling") {
231      pill = "Calling";
232      hint =
233        hint ||
234        (record.kind === "audio"
235          ? "Waiting for them to answer."
236          : record.kind === "screen"
237            ? "Waiting for them to watch."
238            : "Waiting for them to join.");
239      cta = "End below";
240      disabled = true;
241    } else if (record.state === "skipped") {
242      pill = "Skipped";
243      hint = hint || "Skipped for now. Tap Accept when you're ready.";
244      cta = "Accept below";
245      disabled = true;
246    } else if (record.state === "declined") {
247      pill = "Declined";
248      disabled = true;
249      cta = "Declined";
250      hint = hint || "You declined this call.";
251    } else if (record.state === "ready") {
252      pill = record.kind === "file" ? "Ready" : "Live now";
253      hint =
254        hint ||
255        (record.kind === "screen"
256          ? "Tap to focus their screen."
257          : record.kind === "video"
258            ? "Tap to open the video panel."
259            : record.kind === "audio"
260              ? "Voice is live — listen in this chat."
261              : "Tap to download or view.");
262    } else if (record.state === "connecting") {
263      pill = "Connecting";
264      hint = hint || "Linking to their stream…";
265      cta = "Open…";
266    } else if (record.state === "blocked") {
267      pill = "Paused";
268      hint = hint || "You stopped media from this person. Tap to try again.";
269      cta = "Try again";
270    } else if (record.state === "unsupported") {
271      pill = "Unavailable";
272      disabled = true;
273      cta = "Can't open";
274    } else if (record.state === "ended") {
275      pill = "Ended";
276      disabled = true;
277      cta = "Ended";
278      hint = hint || "They stopped sharing.";
279    }
280
281    record._liveHint = hint;
282    record._liveCta = cta;
283    record._liveDisabled = disabled;
284
285    el.dataset.state = record.state;
286    el.classList.toggle("chat-live-notice--disabled", disabled);
287    el.classList.toggle("chat-live-notice--ready", record.state === "ready");
288    el.classList.toggle("chat-live-notice--incoming", record.state === "ringing" || record.state === "skipped");
289    el.classList.toggle("chat-live-notice--calling", record.state === "calling");
290    if (metaEl) metaEl.textContent = noticeSummaryMeta(record, pill, hint);
291    if (ctaEl) {
292      ctaEl.textContent = cta;
293      ctaEl.disabled = disabled;
294    }
295    syncIncomingCallActions(record);
296    syncOutgoingCallActions(record);
297  }
298
299  function buildElement(record) {
300    const meta = KIND_META[record.kind] || KIND_META.video;
301    const OC = global.GriGsiSpaceObjectCard;
302    const isLocal = String(record.peerId || "").trim() === "local";
303    const laneClass = isLocal ? "sent" : "received";
304    const div = document.createElement("div");
305    div.className =
306      "chat-message chat-live-notice " +
307      laneClass +
308      " chat-live-notice--" +
309      meta.accent +
310      " chat-live-notice--" +
311      (record.state || "connecting");
312    div.dataset.noticeKey = record.key;
313    div.dataset.kind = record.kind;
314    div.dataset.state = record.state || "connecting";
315    if (!isLocal && record.peerId) div.dataset.fromPeer = String(record.peerId);
316
317    const name = record.peerName || "Someone";
318    let title = "";
319    if (record.kind === "file") {
320      title = `${name} ${meta.verb}: ${record.fileName || "file"}`;
321    } else if (record.state === "ringing") {
322      title = `${name} ${meta.ringVerb || meta.verb}`;
323    } else if (isLocal && record.state === "calling") {
324      title =
325        record.kind === "audio"
326          ? "Calling…"
327          : record.kind === "screen"
328            ? "Sharing screen…"
329            : "Calling (video)…";
330    } else if (isLocal) {
331      title =
332        record.kind === "audio"
333          ? "Voice call"
334          : record.kind === "screen"
335            ? "Screen share"
336            : "Video call";
337    } else {
338      title = `${name} ${meta.verb}`;
339    }
340
341    const main = document.createElement("div");
342    main.className = "chat-message-main chat-live-notice__main";
343
344    const bubble = document.createElement("div");
345    bubble.className =
346      "bubble bubble--object " + (isLocal ? "bubble--outbox bubble--owner" : "bubble--inbox");
347
348    if (OC) {
349      const iconClass =
350        record.kind === "screen" ? "fa-display" : record.kind === "audio" ? "fa-phone" : meta.icon;
351      const card = OC.create({
352        type: "live-" + record.kind,
353        title: title,
354        meta: noticeSummaryMeta(record, record.state === "ringing" ? "Incoming" : "Live", ""),
355        iconClass: iconClass,
356        onSummaryClick:
357          record.kind === "file"
358            ? () => void onNoticeClick(record)
359            : isRemoteAvCall(record) && record.state === "ringing"
360              ? undefined
361              : record.kind === "audio" || record.kind === "video" || record.kind === "screen"
362                ? () => void onNoticeClick(record)
363                : undefined,
364        onExpand(body) {
365          const hint = document.createElement("p");
366          hint.className = "space-object-card__live-hint";
367          hint.textContent = record._liveHint || "";
368          body.appendChild(hint);
369          if (isRemoteAvCall(record) && (record.state === "ringing" || record.state === "skipped")) {
370            const actions = document.createElement("div");
371            actions.className = "chat-live-notice__incoming-actions chat-live-notice__incoming-actions--expanded";
372            actions.setAttribute("role", "group");
373            if (record.state === "ringing") {
374              addIncomingActionBtn(actions, "Accept", "accept", () => void onAcceptCall(record));
375              addIncomingActionBtn(actions, "Decline", "decline", () => onDeclineCall(record));
376              addIncomingActionBtn(actions, "Skip", "skip", () => onSkipCall(record));
377            } else {
378              addIncomingActionBtn(actions, "Accept", "accept", () => void onAcceptCall(record));
379            }
380            body.appendChild(actions);
381            return;
382          }
383          if (isLocalOutgoingCall(record) && (record.state === "calling" || record.state === "connecting" || record.state === "ready")) {
384            const actions = document.createElement("div");
385            actions.className = "chat-live-notice__outgoing-actions chat-live-notice__incoming-actions chat-live-notice__incoming-actions--expanded";
386            actions.setAttribute("role", "group");
387            addIncomingActionBtn(actions, "End call", "decline", () => onEndOutgoingCall(record));
388            body.appendChild(actions);
389            return;
390          }
391          const cta = document.createElement("button");
392          cta.type = "button";
393          cta.className = "space-object-card__live-cta";
394          cta.textContent = record._liveCta || meta.cta;
395          cta.disabled = !!record._liveDisabled;
396          cta.addEventListener("click", (e) => {
397            e.stopPropagation();
398            void onNoticeClick(record);
399          });
400          body.appendChild(cta);
401        },
402      });
403      card.classList.add("space-object-card--inbox", "space-object-card--live");
404      if (isLocal) card.classList.add("space-object-card--live-local");
405      bubble.appendChild(card);
406    } else {
407      const fallback = document.createElement("button");
408      fallback.type = "button";
409      fallback.className = "chat-live-notice__card";
410      fallback.textContent = title;
411      fallback.addEventListener("click", () => void onNoticeClick(record));
412      bubble.appendChild(fallback);
413    }
414
415    main.appendChild(bubble);
416    div.appendChild(main);
417    div.dataset.reactionTarget = record.key;
418
419    const R = global.GriGsiSpaceChatReactions;
420    if (R && R.buildBar) {
421      const preview = R.previewForNotice ? R.previewForNotice(record) : record.peerName || "Live activity";
422      const actions = document.createElement("div");
423      actions.className = "chat-message-actions chat-live-notice__actions";
424      const bar = R.buildBar(record.key, preview, "notice");
425      if (bar) {
426        actions.appendChild(bar);
427        div.appendChild(actions);
428      }
429    }
430
431    record.el = div;
432    renderRecord(record);
433    const LC = global.GriGsiSpaceLiveCaptions;
434    if (LC && LC.attachToNotice && (record.kind === "audio" || record.kind === "video")) {
435      LC.attachToNotice(div, record);
436    }
437    return div;
438  }
439
440  async function onAcceptCall(record) {
441    if (!hooks || !isRemoteAvCall(record)) return;
442    if (record.state === "ended" || record.state === "declined") return;
443    if (record._acceptInFlight) return;
444    record._acceptInFlight = true;
445    setState(record, "connecting", "Connecting…");
446    if (hooks.acceptIncomingCall) {
447      try {
448        hooks.acceptIncomingCall(record.peerId, record.kind);
449      } catch (_) {}
450    }
451    scheduleMediaPoll(record);
452    record._acceptInFlight = false;
453  }
454
455  function onEndOutgoingCall(record) {
456    if (!hooks || !isLocalOutgoingCall(record)) return;
457    if (hooks.endOutgoingCall) {
458      try {
459        hooks.endOutgoingCall(record.kind);
460      } catch (_) {}
461    }
462    setState(record, "ended", "You ended the call.");
463  }
464
465  function onDeclineCall(record) {
466    if (!hooks || !isRemoteAvCall(record)) return;
467    if (hooks.declineIncomingCall) {
468      try {
469        hooks.declineIncomingCall(record.peerId, record.kind);
470      } catch (_) {}
471    }
472    setState(record, "declined", "You declined this call.");
473  }
474
475  function onSkipCall(record) {
476    if (!hooks || !isRemoteAvCall(record)) return;
477    if (hooks.skipIncomingCall) {
478      try {
479        hooks.skipIncomingCall(record.peerId, record.kind);
480      } catch (_) {}
481    }
482    setState(record, "skipped", "Skipped for now. Tap Accept when you're ready.");
483  }
484
485  async function onNoticeClick(record) {
486    if (!hooks) return;
487    if (record.state === "ended" || record.state === "unsupported" || record.state === "declined") return;
488    if (record.state === "ringing") return;
489
490    if (record.kind === "file") {
491      const file = hooks.findFile && hooks.findFile(record.fileRef);
492      if (!file) {
493        setState(record, "unsupported", "The file is no longer in this Space. Ask them to share it again.");
494        return;
495      }
496      if (!file.data) {
497        setState(record, "connecting", "Still receiving file data…");
498        return;
499      }
500      try {
501        if (hooks.activateFiles) hooks.activateFiles();
502        if (hooks.showIncomingFilePrompt) {
503          hooks.showIncomingFilePrompt(file, record.peerId);
504          setState(record, "ready", "Choose Open or Save in the menu.");
505        } else if (hooks.openFile) {
506          hooks.openFile(file);
507          setState(record, "ready", "Choose Open or Save.");
508        }
509      } catch (_) {
510        setState(record, "unsupported", "Could not show the file menu.");
511      }
512      return;
513    }
514
515    if (record.peerId === "local") {
516      if (record.kind === "audio") {
517        setState(record, "ready", record.hint || "You are in a voice call.");
518      } else if (record.kind === "screen") {
519        setState(record, "ready", record.hint || "You are sharing your screen.");
520      } else {
521        setState(record, "ready", record.hint || "Your stream is live.");
522      }
523      return;
524    }
525
526    if (record.state === "skipped") {
527      await onAcceptCall(record);
528      return;
529    }
530
531    if (record.kind === "audio") {
532      if (hooks.isLocallyBlocked && hooks.isLocallyBlocked(record.peerId)) {
533        setState(record, "blocked", "You paused media from this person. Tap Try again on the notice.");
534        if (hooks.resumeRemoteMedia) hooks.resumeRemoteMedia(record.peerId);
535        setState(record, "connecting", "Reconnecting to their voice…");
536        scheduleMediaPoll(record);
537        return;
538      }
539      if (hooks.resumeRemoteMedia) hooks.resumeRemoteMedia(record.peerId);
540      setState(record, "connecting", "Connecting voice…");
541      if (hooks.focusPeerMedia) {
542        const res = await hooks.focusPeerMedia(record.peerId, { audioOnly: true });
543        if (res && res.ok) {
544          setState(record, "ready", res.hint || "Voice is live — listen in this chat.");
545          return;
546        }
547      }
548      scheduleMediaPoll(record);
549      return;
550    }
551
552    if (hooks.isVideoRemoved && hooks.isVideoRemoved()) {
553      setState(
554        record,
555        "unsupported",
556        "Video and screen share are turned off in this Space build."
557      );
558      return;
559    }
560
561    if (hooks.isLocallyBlocked && hooks.isLocallyBlocked(record.peerId)) {
562      setState(record, "blocked", "You paused media from this person. Tap Try again on the notice.");
563      if (hooks.resumeRemoteMedia) hooks.resumeRemoteMedia(record.peerId);
564      setState(record, "connecting", "Reconnecting to their stream…");
565      scheduleMediaPoll(record);
566      return;
567    }
568
569    setState(record, "connecting", "Opening video…");
570    if (hooks.activateVideo) hooks.activateVideo();
571    if (hooks.focusPeerMedia) {
572      const res = await hooks.focusPeerMedia(record.peerId, {
573        screen: record.kind === "screen",
574      });
575      if (res && res.ok) {
576        setState(record, "ready", res.hint || "You're viewing their stream.");
577        return;
578      }
579      const reason = (res && res.reason) || "unknown";
580      if (reason === "no_tile") {
581        if (hooks.resumeRemoteMedia) hooks.resumeRemoteMedia(record.peerId);
582        setState(
583          record,
584          "connecting",
585          "Their stream is still connecting. We'll focus it when it appears."
586        );
587        scheduleMediaPoll(record);
588        return;
589      }
590      if (reason === "blocked") {
591        setState(record, "blocked", "You paused media from this person.");
592        return;
593      }
594      if (reason === "unsupported") {
595        setState(
596          record,
597          "unsupported",
598          hooks.explainRemoteUnsupported
599            ? hooks.explainRemoteUnsupported(record.kind)
600            : "Your browser can't show their stream here."
601        );
602        return;
603      }
604      setState(record, "connecting", "Still waiting for their video…");
605      scheduleMediaPoll(record);
606    }
607  }
608
609  function scheduleMediaPoll(record) {
610    let tries = 0;
611    const tick = () => {
612      if (!active.has(record.key)) return;
613      tries += 1;
614      const audioReady =
615        record.kind === "audio" &&
616        hooks &&
617        hooks.hasRemoteAudio &&
618        hooks.hasRemoteAudio(record.peerId);
619      const videoReady =
620        record.kind !== "audio" &&
621        hooks &&
622        hooks.hasVideoTile &&
623        hooks.hasVideoTile(record.peerId);
624      if (audioReady || videoReady) {
625        setState(
626          record,
627          "ready",
628          record.kind === "audio"
629            ? "Voice is live — listen in this chat."
630            : "Stream is live — tap again to focus."
631        );
632        if (hooks.focusPeerMedia) {
633          void hooks.focusPeerMedia(record.peerId, {
634            screen: record.kind === "screen",
635            audioOnly: record.kind === "audio",
636          });
637        }
638        return;
639      }
640      if (tries < 12) global.setTimeout(tick, 500);
641      else {
642        setState(
643          record,
644          "unsupported",
645          record.kind === "audio"
646            ? "Their voice didn't connect. Ask them to toggle the mic, or try again."
647            : record.kind === "screen"
648              ? "They started screen share, but nothing arrived. They may need to share again, or your network blocked it."
649              : "Their camera didn't connect. Ask them to toggle video, or try another browser."
650        );
651      }
652    };
653    global.setTimeout(tick, 400);
654  }
655
656  function push(opts) {
657    if (!chatRoot) resolveChatDom();
658    if (!chatRoot) return null;
659
660    const kind = String(opts.kind || "video").toLowerCase();
661    const peerId = String(opts.peerId || "").trim();
662    const fileRef = opts.fileRef || null;
663    const fileId =
664      fileRef && fileRef.name != null
665        ? String(fileRef.name) + "-" + String(fileRef.uploadedAt || 0)
666        : "";
667    const key = noticeKey(kind, peerId, fileId);
668
669    let record = active.get(key);
670    if (!record) {
671      record = {
672        key,
673        kind,
674        peerId,
675        peerName: getPeerName(peerId, opts.peerName),
676        fileRef,
677        fileName: opts.fileName || (fileRef && fileRef.name) || "",
678        state: opts.state || "connecting",
679        hint: opts.hint || "",
680        cta: opts.cta || null,
681        time: formatTime(),
682        el: null,
683        at: now(),
684      };
685      active.set(key, record);
686      const el = buildElement(record);
687      chatRoot.appendChild(el);
688      scrollChat();
689    } else {
690      record.peerName = getPeerName(peerId, opts.peerName || record.peerName);
691      if (opts.fileName) record.fileName = opts.fileName;
692      if (opts.fileRef) record.fileRef = opts.fileRef;
693      if (opts.state) record.state = opts.state;
694      if (opts.hint) record.hint = opts.hint;
695      renderRecord(record);
696      if (record.el && (record.kind === "audio" || record.kind === "video")) {
697        global.GriGsiSpaceLiveCaptions?.attachToNotice(record.el, record);
698      }
699      scrollChat();
700    }
701
702    if (kind !== "file") preflightMedia(record);
703    return record;
704  }
705
706  function preflightMedia(record) {
707    if (!hooks) return;
708    if (record.peerId === "local") return;
709    if (record.state === "connecting" || record.state === "ready" || record.state === "calling") return;
710    if (hooks.isVideoRemoved && hooks.isVideoRemoved()) {
711      setState(
712        record,
713        "unsupported",
714        "Video and screen share are turned off in this Space build."
715      );
716      return;
717    }
718    if (hooks.isLocallyBlocked && hooks.isLocallyBlocked(record.peerId)) {
719      setState(record, "blocked", "You paused media from this person.");
720      return;
721    }
722    if (record.kind === "audio") {
723      if (hooks.hasRemoteAudio && hooks.hasRemoteAudio(record.peerId)) {
724        setState(record, "ready", "Voice is live — listen in this chat.");
725      } else if (hooks.hasAcceptedIncoming && hooks.hasAcceptedIncoming(record.peerId, record.kind)) {
726        setState(record, "connecting", "Connecting to their voice…");
727      } else if (record.state !== "skipped" && record.state !== "declined") {
728        setState(record, "ringing", defaultRingHint("audio"));
729      }
730      return;
731    }
732    if (hooks.hasVideoTile && hooks.hasVideoTile(record.peerId)) {
733      setState(record, "ready", "Their stream is already live.");
734      return;
735    }
736    if (hooks.hasAcceptedIncoming && hooks.hasAcceptedIncoming(record.peerId, record.kind)) {
737      setState(record, "connecting", "Connecting…");
738      return;
739    }
740    if (record.state === "skipped" || record.state === "declined") return;
741    setState(record, "ringing", defaultRingHint(record.kind));
742  }
743
744  function markReady(peerId, kind, hint) {
745    const k = noticeKey(kind || "video", peerId, "");
746    const record = active.get(k);
747    if (!record) return;
748    setState(record, "ready", hint || "");
749  }
750
751  function markEnded(peerId, kind, hint) {
752    const k = noticeKey(kind || "video", peerId, "");
753    const record = active.get(k);
754    if (!record) return;
755    try {
756      global.GriGsiSpaceLiveCaptions?.detachFromNotice(peerId, kind || record.kind);
757    } catch (_) {}
758    setState(record, "ended", hint || "They stopped sharing.");
759  }
760
761  function refreshPeerLabel(peerId) {
762    const pid = String(peerId || "").trim();
763    if (!pid) return;
764    const norm =
765      hooks && hooks.normalizePeerId ? String(hooks.normalizePeerId(pid) || "").trim() : pid;
766    active.forEach((record) => {
767      if (!record) return;
768      const rid = String(record.peerId || "").trim();
769      const rnorm =
770        hooks && hooks.normalizePeerId ? String(hooks.normalizePeerId(rid) || "").trim() : rid;
771      if (rid !== pid && rnorm !== norm) return;
772      record.peerName = getPeerName(record.peerId, record.peerName);
773      renderRecord(record);
774    });
775  }
776
777  function init(options) {
778    hooks = options && typeof options === "object" ? options : {};
779    resolveChatDom();
780  }
781
782  global.GriGsiSpaceChatNotices = {
783    init,
784    push,
785    markReady,
786    markEnded,
787    refreshPeerLabel,
788    noticeKey,
789  };
790})(typeof window !== "undefined" ? window : globalThis);

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.