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.