vibedate 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +6 -1
- package/dist/cli.js +673 -54
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -44,5 +44,10 @@ declare function formatAgo(iso: string, now?: Date): string;
|
|
|
44
44
|
* Interactive TTY behavior is unchanged: Ctrl+D / EOF still exits.
|
|
45
45
|
*/
|
|
46
46
|
declare function shouldKeepAlive(flag: boolean, stdinIsTTY: boolean | undefined): boolean;
|
|
47
|
+
declare function parseSendCommand(text: string): {
|
|
48
|
+
path: string;
|
|
49
|
+
} | {
|
|
50
|
+
error: 'missing_path';
|
|
51
|
+
} | null;
|
|
47
52
|
|
|
48
|
-
export { type Command, type ParsedArgs, formatAgo, parseArgs, shouldKeepAlive };
|
|
53
|
+
export { type Command, type ParsedArgs, formatAgo, parseArgs, parseSendCommand, shouldKeepAlive };
|
package/dist/cli.js
CHANGED
|
@@ -56,6 +56,9 @@ import process2 from "process";
|
|
|
56
56
|
// src/server.ts
|
|
57
57
|
import http from "http";
|
|
58
58
|
import { randomUUID } from "crypto";
|
|
59
|
+
import fs from "fs";
|
|
60
|
+
import os from "os";
|
|
61
|
+
import path from "path";
|
|
59
62
|
import { makeEvent, notify as vibeCoreNotify } from "@pooriaarab/vibe-core";
|
|
60
63
|
|
|
61
64
|
// src/web-app-html.ts
|
|
@@ -599,15 +602,23 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
599
602
|
max-width: 85%; padding: 7px 11px; border-radius: 13px;
|
|
600
603
|
font-size: .82rem; line-height: 1.4; word-break: break-word; white-space: pre-wrap;
|
|
601
604
|
}
|
|
605
|
+
.cp-msg img, .cp-msg video { max-width: 100%; border-radius: 8px; margin-top: 4px; display: block; }
|
|
606
|
+
.cp-msg a.media-link { color: var(--coral); text-decoration: underline; word-break: break-all; }
|
|
602
607
|
.cp-msg.them{ align-self: flex-start; background: rgba(248,239,232,.07); border: 1px solid var(--border); }
|
|
603
608
|
.cp-msg.you{ align-self: flex-end; background: rgba(255,122,104,.16); border: 1px solid rgba(255,122,104,.32); }
|
|
604
609
|
.cp-msg.sys{ align-self: center; background: transparent; border: 0; color: var(--muted-2); font-size: .72rem; padding: 2px 6px; }
|
|
605
610
|
.chat-panel .cp-inputrow{ display: flex; gap: 8px; padding: 10px; border-top: 1px solid var(--border); }
|
|
606
|
-
.chat-panel .cp-inputrow input{
|
|
611
|
+
.chat-panel .cp-inputrow input[type="text"]{
|
|
607
612
|
flex: 1; border: 1px solid var(--border-2); border-radius: 10px; background: rgba(0,0,0,.22);
|
|
608
613
|
color: var(--fg); padding: 9px 11px; font: inherit; font-size: .82rem; outline: none; min-width: 0;
|
|
609
614
|
}
|
|
610
|
-
.chat-panel .cp-inputrow input:focus{ border-color: var(--coral); }
|
|
615
|
+
.chat-panel .cp-inputrow input[type="text"]:focus{ border-color: var(--coral); }
|
|
616
|
+
.chat-panel .cp-attach{
|
|
617
|
+
border: 1px solid var(--border-2); border-radius: 10px; background: rgba(0,0,0,.22);
|
|
618
|
+
color: var(--muted); padding: 9px 12px; font-size: 1rem; flex-shrink: 0;
|
|
619
|
+
transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease;
|
|
620
|
+
}
|
|
621
|
+
.chat-panel .cp-attach:hover{ color: var(--fg); border-color: var(--muted-2); }
|
|
611
622
|
.chat-panel .cp-send{
|
|
612
623
|
border: 0; border-radius: 10px; padding: 9px 14px; font-weight: 700; font-size: .8rem;
|
|
613
624
|
background: linear-gradient(180deg, var(--coral), var(--coral-dim)); color: #2a1109;
|
|
@@ -615,6 +626,187 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
615
626
|
}
|
|
616
627
|
.chat-panel .cp-send:hover{ filter: brightness(1.06); }
|
|
617
628
|
.chat-panel .cp-send:active{ transform: scale(.95); }
|
|
629
|
+
|
|
630
|
+
/* ---- you identity chip (header) ---- */
|
|
631
|
+
.you-chip{
|
|
632
|
+
display:none; align-items:center; gap:10px;
|
|
633
|
+
padding: 6px 12px 6px 7px; border-radius: 999px;
|
|
634
|
+
background: rgba(0,0,0,.28); border: 1px solid var(--border-2);
|
|
635
|
+
max-width: min(420px, 52vw); min-width: 0;
|
|
636
|
+
}
|
|
637
|
+
.you-chip.is-on{ display:inline-flex; }
|
|
638
|
+
.you-chip .yc-avatar{
|
|
639
|
+
width:30px; height:30px; border-radius:50%; overflow:hidden; flex-shrink:0;
|
|
640
|
+
border: 1px solid var(--border-2); background: var(--bg-1);
|
|
641
|
+
}
|
|
642
|
+
.you-chip .yc-avatar svg{ width:100%; height:100%; display:block; }
|
|
643
|
+
.you-chip .yc-meta{ min-width:0; display:flex; flex-direction:column; gap:1px; }
|
|
644
|
+
.you-chip .yc-handle{
|
|
645
|
+
font-size:.78rem; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
|
|
646
|
+
}
|
|
647
|
+
.you-chip .yc-sub{
|
|
648
|
+
font-size:.68rem; color: var(--muted-2); display:flex; align-items:center; gap:6px; flex-wrap:wrap;
|
|
649
|
+
}
|
|
650
|
+
.topbar-right{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end; }
|
|
651
|
+
|
|
652
|
+
/* ---- identity / status marks ---- */
|
|
653
|
+
.mark{
|
|
654
|
+
display:inline-flex; align-items:center; gap:3px;
|
|
655
|
+
font-size:.66rem; font-weight:700; letter-spacing:.01em;
|
|
656
|
+
padding: 2px 7px; border-radius:999px; line-height:1.3; white-space:nowrap;
|
|
657
|
+
}
|
|
658
|
+
.mark-verified{
|
|
659
|
+
color: #bff2df; background: rgba(127,227,192,.12); border: 1px solid rgba(127,227,192,.32);
|
|
660
|
+
}
|
|
661
|
+
.mark-identity{
|
|
662
|
+
color: #e8d9ff; background: rgba(203,168,255,.12); border: 1px solid rgba(203,168,255,.32);
|
|
663
|
+
}
|
|
664
|
+
.mark-demo{
|
|
665
|
+
color: var(--muted-2); background: rgba(248,239,232,.05); border: 1px solid var(--border);
|
|
666
|
+
text-transform: uppercase; letter-spacing: .06em;
|
|
667
|
+
}
|
|
668
|
+
.mark-live{
|
|
669
|
+
color: #ffd0c8; background: rgba(255,122,104,.12); border: 1px solid rgba(255,122,104,.35);
|
|
670
|
+
}
|
|
671
|
+
.lg-pill{
|
|
672
|
+
display:inline-flex; align-items:center; gap:5px;
|
|
673
|
+
font-size:.7rem; font-weight:700; padding: 3px 9px; border-radius:999px;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/* ---- people column (live + demo) ---- */
|
|
677
|
+
.people-col{ display:flex; flex-direction:column; gap: 22px; min-width: 0; }
|
|
678
|
+
.section-head{
|
|
679
|
+
display:flex; align-items:baseline; justify-content:space-between; gap:10px;
|
|
680
|
+
margin: 0 0 12px;
|
|
681
|
+
}
|
|
682
|
+
.section-head h2{
|
|
683
|
+
font-size: 1.02rem; font-weight: 700; margin:0;
|
|
684
|
+
display:flex; align-items:center; gap:8px;
|
|
685
|
+
}
|
|
686
|
+
.section-head .count{
|
|
687
|
+
font-size:.72rem; font-weight:700; color: var(--muted-2);
|
|
688
|
+
background: rgba(0,0,0,.22); border: 1px solid var(--border);
|
|
689
|
+
padding: 3px 8px; border-radius:999px;
|
|
690
|
+
}
|
|
691
|
+
.section-sub{
|
|
692
|
+
font-size:.78rem; color: var(--muted-2); margin: -6px 0 12px; line-height:1.45;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* Live now roster cards */
|
|
696
|
+
.live-roster{ display:flex; flex-direction:column; gap:10px; }
|
|
697
|
+
.live-card{
|
|
698
|
+
display:flex; align-items:center; gap:12px;
|
|
699
|
+
background: linear-gradient(165deg, rgba(255,122,104,.08), transparent 55%),
|
|
700
|
+
linear-gradient(180deg, var(--bg-card), var(--bg-card-2));
|
|
701
|
+
border: 1px solid rgba(255,122,104,.22);
|
|
702
|
+
border-radius: 16px; padding: 14px 14px;
|
|
703
|
+
box-shadow: var(--shadow-1);
|
|
704
|
+
transition: border-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
|
|
705
|
+
}
|
|
706
|
+
.live-card:hover{ border-color: rgba(255,122,104,.4); transform: translateY(-1px); }
|
|
707
|
+
.live-card .lc-avatar{
|
|
708
|
+
width:48px; height:48px; border-radius:50%; overflow:hidden; flex-shrink:0;
|
|
709
|
+
border:1px solid var(--border-2); background:var(--bg-1);
|
|
710
|
+
}
|
|
711
|
+
.live-card .lc-avatar svg{ width:100%; height:100%; display:block; }
|
|
712
|
+
.live-card .lc-body{ flex:1; min-width:0; }
|
|
713
|
+
.live-card .lc-handle-row{
|
|
714
|
+
display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:4px;
|
|
715
|
+
}
|
|
716
|
+
.live-card .lc-handle{ font-weight:700; font-size:.92rem; word-break:break-all; }
|
|
717
|
+
.live-card .lc-meta{
|
|
718
|
+
display:flex; align-items:center; gap:6px; flex-wrap:wrap;
|
|
719
|
+
font-size:.72rem; color: var(--muted-2);
|
|
720
|
+
}
|
|
721
|
+
.live-card .lc-actions{ display:flex; flex-direction:column; gap:6px; flex-shrink:0; }
|
|
722
|
+
.live-card .lc-actions button{
|
|
723
|
+
border:0; border-radius:10px; padding:8px 14px; font-weight:700; font-size:.76rem;
|
|
724
|
+
min-width: 72px;
|
|
725
|
+
transition: transform var(--dur-fast) var(--ease-out), filter var(--dur-fast) ease, border-color var(--dur-fast) ease, color var(--dur-fast) ease;
|
|
726
|
+
}
|
|
727
|
+
.live-card .lc-actions button:active{ transform: scale(.96); }
|
|
728
|
+
.live-card .btn-call{
|
|
729
|
+
background: linear-gradient(180deg, var(--coral), var(--coral-dim)); color: #2a1109;
|
|
730
|
+
box-shadow: 0 8px 18px -10px rgba(255,122,104,.55);
|
|
731
|
+
}
|
|
732
|
+
.live-card .btn-call:hover{ filter: brightness(1.06); }
|
|
733
|
+
.live-card .btn-call:disabled{ opacity:.4; cursor:not-allowed; filter:none; box-shadow:none; }
|
|
734
|
+
.live-card .btn-chat{
|
|
735
|
+
background: transparent; color: var(--muted); border: 1px solid var(--border-2);
|
|
736
|
+
}
|
|
737
|
+
.live-card .btn-chat:hover{ color: var(--fg); border-color: var(--muted-2); }
|
|
738
|
+
.live-card .btn-chat.has-unread{
|
|
739
|
+
color: var(--coral); border-color: rgba(255,122,104,.45);
|
|
740
|
+
background: rgba(255,122,104,.08);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.live-empty{
|
|
744
|
+
border: 1px dashed var(--border-2); border-radius: 16px;
|
|
745
|
+
padding: 18px 16px; text-align:center;
|
|
746
|
+
background: rgba(0,0,0,.14);
|
|
747
|
+
}
|
|
748
|
+
.live-empty strong{ display:block; font-size:.9rem; margin-bottom:4px; }
|
|
749
|
+
.live-empty p{ margin:0; font-size:.8rem; color: var(--muted-2); line-height:1.45; }
|
|
750
|
+
|
|
751
|
+
.live-now-wrap.has-live .section-head h2 .pulse-live{
|
|
752
|
+
width:8px; height:8px; border-radius:50%; background: var(--coral);
|
|
753
|
+
box-shadow: 0 0 0 3px rgba(255,122,104,.2);
|
|
754
|
+
animation: pulse-dot 2s ease-in-out infinite; display:inline-block;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* Demo badge on match cards */
|
|
758
|
+
.mc-demo-badge{
|
|
759
|
+
position:absolute; top:14px; right:14px; z-index:2;
|
|
760
|
+
}
|
|
761
|
+
.match-card{ position:absolute; /* ensure badge anchors */ }
|
|
762
|
+
.mc-handle-row{ display:flex; align-items:center; gap:7px; flex-wrap:wrap; }
|
|
763
|
+
.demo-note{
|
|
764
|
+
font-size:.74rem; color: var(--muted-2); margin: 0 0 10px; line-height:1.4;
|
|
765
|
+
}
|
|
766
|
+
.demo-note b{ color: var(--muted); font-weight:600; }
|
|
767
|
+
|
|
768
|
+
/* Stack polish when demo secondary */
|
|
769
|
+
.stack-wrap.is-secondary{ opacity: .92; }
|
|
770
|
+
.people-col .stack-wrap{ height: 430px; }
|
|
771
|
+
|
|
772
|
+
/* Video / room / chat polish */
|
|
773
|
+
.video-modal .vtile video{
|
|
774
|
+
box-shadow: 0 24px 60px -28px rgba(0,0,0,.8), 0 0 0 1px rgba(255,122,104,.12);
|
|
775
|
+
}
|
|
776
|
+
.video-modal .vlabel{
|
|
777
|
+
background: rgba(0,0,0,.35); padding: 4px 10px; border-radius:999px;
|
|
778
|
+
border: 1px solid var(--border);
|
|
779
|
+
}
|
|
780
|
+
.room-video .rv-tile video{
|
|
781
|
+
box-shadow: 0 14px 36px -20px rgba(0,0,0,.7);
|
|
782
|
+
}
|
|
783
|
+
.room-video .rv-head{
|
|
784
|
+
background: rgba(0,0,0,.28); border: 1px solid var(--border);
|
|
785
|
+
border-radius: 12px; padding: 10px 14px;
|
|
786
|
+
}
|
|
787
|
+
.chat-panel{
|
|
788
|
+
border-color: rgba(255,122,104,.18);
|
|
789
|
+
}
|
|
790
|
+
.chat-panel .cp-head{
|
|
791
|
+
background: rgba(0,0,0,.18);
|
|
792
|
+
}
|
|
793
|
+
.live-panel, .room-panel{
|
|
794
|
+
border-color: rgba(255,122,104,.18);
|
|
795
|
+
}
|
|
796
|
+
.live-panel .lp-title .lp-live-count,
|
|
797
|
+
.room-panel .rp-title .rp-count{
|
|
798
|
+
margin-left:auto; font-size:.68rem; font-weight:700; color: var(--muted-2);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
@media (max-width: 1020px){
|
|
802
|
+
.you-chip{ max-width: 100%; }
|
|
803
|
+
}
|
|
804
|
+
@media (max-width: 480px){
|
|
805
|
+
.live-card{ flex-wrap: wrap; }
|
|
806
|
+
.live-card .lc-actions{ flex-direction: row; width: 100%; }
|
|
807
|
+
.live-card .lc-actions button{ flex:1; }
|
|
808
|
+
}
|
|
809
|
+
|
|
618
810
|
</style>
|
|
619
811
|
</head>
|
|
620
812
|
<body>
|
|
@@ -624,7 +816,16 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
624
816
|
<span class="name">vibe<span>dating</span></span>
|
|
625
817
|
<span class="tag">matched by how hard you push the model</span>
|
|
626
818
|
</div>
|
|
627
|
-
<div class="
|
|
819
|
+
<div class="topbar-right">
|
|
820
|
+
<div class="you-chip" id="youChip" aria-live="polite">
|
|
821
|
+
<div class="yc-avatar" id="youChipAvatar"></div>
|
|
822
|
+
<div class="yc-meta">
|
|
823
|
+
<div class="yc-handle" id="youChipHandle">@you</div>
|
|
824
|
+
<div class="yc-sub" id="youChipSub"></div>
|
|
825
|
+
</div>
|
|
826
|
+
</div>
|
|
827
|
+
<div class="local-badge"><span class="dot" aria-hidden="true"></span> raw usage stays local · only league shared</div>
|
|
828
|
+
</div>
|
|
628
829
|
</header>
|
|
629
830
|
|
|
630
831
|
<div class="hero">
|
|
@@ -718,12 +919,27 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
718
919
|
</div>
|
|
719
920
|
</section>
|
|
720
921
|
|
|
721
|
-
<
|
|
722
|
-
<
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
922
|
+
<div class="people-col panel" aria-label="People">
|
|
923
|
+
<section class="live-now-wrap" id="liveNowWrap" aria-label="Live now">
|
|
924
|
+
<div class="section-head">
|
|
925
|
+
<h2><span class="pulse-live" aria-hidden="true"></span> Live now</h2>
|
|
926
|
+
<span class="count" id="liveNowCount">0</span>
|
|
927
|
+
</div>
|
|
928
|
+
<p class="section-sub">Real discovered peers on the wire — usage-verified and identity marks from their hello.</p>
|
|
929
|
+
<div class="live-roster" id="liveRoster"></div>
|
|
930
|
+
</section>
|
|
931
|
+
|
|
932
|
+
<section aria-label="Demo profiles">
|
|
933
|
+
<div class="section-head">
|
|
934
|
+
<h2>Demo profiles</h2>
|
|
935
|
+
<span class="mark mark-demo" title="Seeded local pool \u2014 not real people">demo</span>
|
|
936
|
+
</div>
|
|
937
|
+
<p class="demo-note">Seeded local filler from <b>CANDIDATES</b> — not live peers. Swipe to practice matching by league.</p>
|
|
938
|
+
<div class="stack-wrap" id="demoStackWrap">
|
|
939
|
+
<div class="card-stack" id="cardStack"></div>
|
|
940
|
+
</div>
|
|
941
|
+
</section>
|
|
942
|
+
</div>
|
|
727
943
|
|
|
728
944
|
<section class="panel" aria-label="League ladder">
|
|
729
945
|
<h2 class="panel-title">League ladder</h2>
|
|
@@ -759,10 +975,10 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
759
975
|
</div>
|
|
760
976
|
</div>
|
|
761
977
|
|
|
762
|
-
<aside class="live-panel" id="livePanel" aria-label="Live peers">
|
|
763
|
-
<div class="lp-title"><span class="lp-dot" aria-hidden="true"></span> Live
|
|
978
|
+
<aside class="live-panel" id="livePanel" aria-label="Live peers compact">
|
|
979
|
+
<div class="lp-title"><span class="lp-dot" aria-hidden="true"></span> Live now <span class="lp-live-count" id="lpLiveCount"></span></div>
|
|
764
980
|
<div id="liveRows"></div>
|
|
765
|
-
<div class="lp-legend">✓ usage verified · 🔑 identity verified —
|
|
981
|
+
<div class="lp-legend">✓ usage verified · 🔑 identity verified — real peers only (not demo)</div>
|
|
766
982
|
</aside>
|
|
767
983
|
|
|
768
984
|
<div class="incoming-call" id="incomingCall" role="dialog" aria-modal="true" aria-label="Incoming call">
|
|
@@ -780,12 +996,14 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
780
996
|
<div class="cp-head">
|
|
781
997
|
<div>
|
|
782
998
|
<div class="cp-title" id="chatTitle">@peer</div>
|
|
783
|
-
<div class="cp-sub" id="chatSub">live
|
|
999
|
+
<div class="cp-sub" id="chatSub">live peer · P2P text</div>
|
|
784
1000
|
</div>
|
|
785
1001
|
<button class="cp-close" id="chatClose" type="button" aria-label="Close chat">×</button>
|
|
786
1002
|
</div>
|
|
787
1003
|
<div class="cp-msgs" id="chatMsgs"></div>
|
|
788
1004
|
<div class="cp-inputrow">
|
|
1005
|
+
<button class="cp-attach" id="chatAttachBtn" type="button" aria-label="Attach file" title="Attach file">\u{1F4CE}</button>
|
|
1006
|
+
<input type="file" id="chatFileInput" style="display:none;">
|
|
789
1007
|
<input id="chatInput" type="text" maxlength="4000" placeholder="message…" autocomplete="off">
|
|
790
1008
|
<button class="cp-send" id="chatSend" type="button">Send</button>
|
|
791
1009
|
</div>
|
|
@@ -798,7 +1016,7 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
798
1016
|
</div>
|
|
799
1017
|
|
|
800
1018
|
<aside class="room-panel" id="roomPanel" aria-label="Room">
|
|
801
|
-
<div class="rp-title"><span class="lp-dot" aria-hidden="true"></span> Room
|
|
1019
|
+
<div class="rp-title"><span class="lp-dot" aria-hidden="true"></span> Room <span class="rp-count" id="rpCount"></span></div>
|
|
802
1020
|
<div class="rp-meta" id="roomMeta">not in a room</div>
|
|
803
1021
|
<div class="rp-actions">
|
|
804
1022
|
<button class="rp-btn" id="roomJoinBtn" type="button">Join video</button>
|
|
@@ -855,6 +1073,12 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
855
1073
|
function fmt(n){ return Math.round(n).toLocaleString("en-US"); }
|
|
856
1074
|
function variantFor(handle){ var h=0; for (var i=0;i<handle.length;i++){ h=(h*31+handle.charCodeAt(i))>>>0; } return h % BLOB_PATHS.length; }
|
|
857
1075
|
|
|
1076
|
+
// Shared with the live A/V script (separate IIFE) \u2014 roster cards need avatars + league colors.
|
|
1077
|
+
window.__vdAvatarSVG = avatarSVG;
|
|
1078
|
+
window.__vdVariantFor = variantFor;
|
|
1079
|
+
window.__vdLeagueById = leagueById;
|
|
1080
|
+
window.__vdLeagueColorHex = leagueColorHex;
|
|
1081
|
+
|
|
858
1082
|
var state = { connected:false };
|
|
859
1083
|
var stackIndex = 0;
|
|
860
1084
|
var provider = "claude-code";
|
|
@@ -919,17 +1143,54 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
919
1143
|
state = s || { connected:false };
|
|
920
1144
|
if (state.connected){
|
|
921
1145
|
renderReveal(state);
|
|
1146
|
+
renderYouChip(state);
|
|
922
1147
|
renderYouMarker(state.league);
|
|
923
1148
|
stackIndex = 0;
|
|
924
1149
|
renderStack();
|
|
925
1150
|
showStep("reveal");
|
|
926
1151
|
} else {
|
|
1152
|
+
renderYouChip(null);
|
|
927
1153
|
stackIndex = 0;
|
|
928
1154
|
renderStack();
|
|
929
1155
|
showStep("idle");
|
|
930
1156
|
}
|
|
931
1157
|
}
|
|
932
1158
|
|
|
1159
|
+
function renderYouChip(s){
|
|
1160
|
+
var chip = document.getElementById("youChip");
|
|
1161
|
+
if (!chip) return;
|
|
1162
|
+
if (!s || !s.connected){
|
|
1163
|
+
chip.classList.remove("is-on");
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
var lg = leagueById(s.league);
|
|
1167
|
+
var hex = lg ? lg.hex : "#ffcf6b";
|
|
1168
|
+
document.getElementById("youChipAvatar").innerHTML = avatarSVG(3, hex);
|
|
1169
|
+
document.getElementById("youChipHandle").textContent = s.handle || "@you";
|
|
1170
|
+
var sub = document.getElementById("youChipSub");
|
|
1171
|
+
sub.innerHTML = "";
|
|
1172
|
+
var pill = document.createElement("span");
|
|
1173
|
+
pill.className = "lg-pill";
|
|
1174
|
+
pill.style.background = "color-mix(in srgb, " + hex + " 16%, transparent)";
|
|
1175
|
+
pill.style.border = "1px solid color-mix(in srgb, " + hex + " 40%, transparent)";
|
|
1176
|
+
pill.style.color = hex;
|
|
1177
|
+
pill.textContent = lg ? lg.label : ((s.league || "?") + " League");
|
|
1178
|
+
sub.appendChild(pill);
|
|
1179
|
+
if (s.verified){
|
|
1180
|
+
var vm = document.createElement("span");
|
|
1181
|
+
vm.className = "mark mark-verified";
|
|
1182
|
+
vm.textContent = "\u2713 verified";
|
|
1183
|
+
sub.appendChild(vm);
|
|
1184
|
+
} else {
|
|
1185
|
+
var lm = document.createElement("span");
|
|
1186
|
+
lm.className = "mark mark-demo";
|
|
1187
|
+
lm.textContent = "local";
|
|
1188
|
+
lm.title = "Usage read locally / self-reported";
|
|
1189
|
+
sub.appendChild(lm);
|
|
1190
|
+
}
|
|
1191
|
+
chip.classList.add("is-on");
|
|
1192
|
+
}
|
|
1193
|
+
|
|
933
1194
|
function renderReveal(s){
|
|
934
1195
|
var lg = leagueById(s.league);
|
|
935
1196
|
var hex = lg ? lg.hex : "#ffcf6b";
|
|
@@ -979,6 +1240,7 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
979
1240
|
rawToggle.setAttribute("aria-expanded","false");
|
|
980
1241
|
rawToggle.textContent = "show raw usage (visible only to you)";
|
|
981
1242
|
document.querySelectorAll(".you-marker").forEach(function(m){ m.remove(); });
|
|
1243
|
+
renderYouChip(null);
|
|
982
1244
|
stackIndex = 0;
|
|
983
1245
|
renderStack();
|
|
984
1246
|
showStep("idle");
|
|
@@ -1026,8 +1288,8 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1026
1288
|
if (!state.connected){
|
|
1027
1289
|
empty.innerHTML = "<strong>Connect to see matches.</strong><p>Read your usage to get sorted into a league and start matching.</p>";
|
|
1028
1290
|
} else {
|
|
1029
|
-
empty.innerHTML = "<strong>
|
|
1030
|
-
'<button class="btn btn-primary" id="btnRestart" type="button" style="width:auto;padding:11px 20px;">
|
|
1291
|
+
empty.innerHTML = "<strong>End of demo pool.</strong><p>These were seeded demo profiles \u2014 not live peers. Real people show up under Live now.</p>" +
|
|
1292
|
+
'<button class="btn btn-primary" id="btnRestart" type="button" style="width:auto;padding:11px 20px;">Restart demo</button>';
|
|
1031
1293
|
}
|
|
1032
1294
|
cardStackEl.appendChild(empty);
|
|
1033
1295
|
var restart = empty.querySelector("#btnRestart");
|
|
@@ -1043,12 +1305,13 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1043
1305
|
var label = lg ? lg.label : (cand.league + " League");
|
|
1044
1306
|
var bio = cand.bio || [];
|
|
1045
1307
|
el.innerHTML =
|
|
1308
|
+
'<span class="mc-demo-badge mark mark-demo" title="Seeded demo profile \u2014 not a live peer">demo</span>' +
|
|
1046
1309
|
'<div class="mc-top">' +
|
|
1047
1310
|
'<div class="mc-avatar">' + avatarSVG(variantFor(cand.handle), hex) + '</div>' +
|
|
1048
1311
|
'<div>' +
|
|
1049
|
-
'<div class="mc-handle">' + cand.handle + '</div>' +
|
|
1312
|
+
'<div class="mc-handle-row"><div class="mc-handle">' + cand.handle + '</div></div>' +
|
|
1050
1313
|
'<span class="mc-league" style="background:color-mix(in srgb,' + hex + ' 16%, transparent);border:1px solid color-mix(in srgb,' + hex + ' 40%, transparent);color:' + hex + '">' + label + '</span>' +
|
|
1051
|
-
'<div class="mc-verified"
|
|
1314
|
+
'<div class="mc-verified" style="color:var(--muted-2)">seeded local profile</div>' +
|
|
1052
1315
|
'</div>' +
|
|
1053
1316
|
'</div>' +
|
|
1054
1317
|
'<div class="mc-bio"><p>' + (bio[0] || "") + '</p><p>' + (bio[1] || "") + '</p></div>' +
|
|
@@ -1056,7 +1319,7 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1056
1319
|
'<div class="mc-actions">' +
|
|
1057
1320
|
'<button class="round-btn pass" type="button" title="Pass">✕</button>' +
|
|
1058
1321
|
'<button class="round-btn like" type="button" title="Like">♥</button>' +
|
|
1059
|
-
'<span class="hint"
|
|
1322
|
+
'<span class="hint">demo</span>' +
|
|
1060
1323
|
'</div>' : '');
|
|
1061
1324
|
cardStackEl.appendChild(el);
|
|
1062
1325
|
if (depth === 0){
|
|
@@ -1304,6 +1567,8 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1304
1567
|
var chatSub = document.getElementById("chatSub");
|
|
1305
1568
|
var chatMsgs = document.getElementById("chatMsgs");
|
|
1306
1569
|
var chatInput = document.getElementById("chatInput");
|
|
1570
|
+
var chatAttachBtn = document.getElementById("chatAttachBtn");
|
|
1571
|
+
var chatFileInput = document.getElementById("chatFileInput");
|
|
1307
1572
|
var chatSend = document.getElementById("chatSend");
|
|
1308
1573
|
var chatClose = document.getElementById("chatClose");
|
|
1309
1574
|
|
|
@@ -1311,6 +1576,7 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1311
1576
|
var conversations = {}; // handle -> [{from:"you"|"them"|"sys", text}]
|
|
1312
1577
|
var unread = {}; // handle -> count of unseen incoming messages
|
|
1313
1578
|
var chatLoops = {}; // handle -> true while its poll loop is running
|
|
1579
|
+
var mediaLoops = {}; // handle -> true while its poll loop is running
|
|
1314
1580
|
var MAX_CHAT_KEPT = 200; // per-conversation local cap (mirrors the server)
|
|
1315
1581
|
|
|
1316
1582
|
function fetchMessage(handle, timeoutMs){
|
|
@@ -1321,6 +1587,14 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1321
1587
|
.catch(function(e){ clearTimeout(t); throw e; });
|
|
1322
1588
|
}
|
|
1323
1589
|
|
|
1590
|
+
function fetchMedia(handle, timeoutMs){
|
|
1591
|
+
var ctrl = new AbortController();
|
|
1592
|
+
var t = setTimeout(function(){ ctrl.abort(); }, timeoutMs);
|
|
1593
|
+
return fetch("/live/media?handle=" + encodeURIComponent(handle), { signal: ctrl.signal })
|
|
1594
|
+
.then(function(r){ clearTimeout(t); return r; })
|
|
1595
|
+
.catch(function(e){ clearTimeout(t); throw e; });
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1324
1598
|
function postChat(handle, text){
|
|
1325
1599
|
return fetch("/live/message", {
|
|
1326
1600
|
method: "POST",
|
|
@@ -1329,6 +1603,14 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1329
1603
|
});
|
|
1330
1604
|
}
|
|
1331
1605
|
|
|
1606
|
+
function postMedia(handle, name, mime, dataB64){
|
|
1607
|
+
return fetch("/live/media", {
|
|
1608
|
+
method: "POST",
|
|
1609
|
+
headers: { "content-type": "application/json" },
|
|
1610
|
+
body: JSON.stringify({ handle: handle, name: name, mime: mime, dataB64: dataB64 })
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1332
1614
|
function peerKnown(handle){
|
|
1333
1615
|
for (var i = 0; i < knownPeers.length; i++){ if (knownPeers[i].handle === handle) return true; }
|
|
1334
1616
|
return false;
|
|
@@ -1343,27 +1625,51 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1343
1625
|
// One long-poll loop per connected peer: started when the peer is first
|
|
1344
1626
|
// seen, stopped when it vanishes (its mailbox on the server is gone).
|
|
1345
1627
|
function ensureChatLoop(handle){
|
|
1346
|
-
if (chatLoops[handle])
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1628
|
+
if (!chatLoops[handle]) {
|
|
1629
|
+
chatLoops[handle] = true;
|
|
1630
|
+
(async function(){
|
|
1631
|
+
while (peerKnown(handle)) {
|
|
1632
|
+
var res;
|
|
1633
|
+
try { res = await fetchMessage(handle, 30000); }
|
|
1634
|
+
catch(e){ await sleep(1000); continue; }
|
|
1635
|
+
if (!res || res.status !== 200) { await sleep(1000); continue; }
|
|
1636
|
+
var data = await res.json();
|
|
1637
|
+
var m = data && data.message;
|
|
1638
|
+
if (!m) continue; // timed out empty
|
|
1639
|
+
pushChat(handle, { from: "them", text: m.text });
|
|
1640
|
+
if (chatWith === handle) {
|
|
1641
|
+
renderChat();
|
|
1642
|
+
} else {
|
|
1643
|
+
unread[handle] = (unread[handle] || 0) + 1;
|
|
1644
|
+
renderLiveRows(knownPeers);
|
|
1645
|
+
}
|
|
1363
1646
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
}
|
|
1647
|
+
delete chatLoops[handle];
|
|
1648
|
+
})();
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
if (!mediaLoops[handle]) {
|
|
1652
|
+
mediaLoops[handle] = true;
|
|
1653
|
+
(async function(){
|
|
1654
|
+
while (peerKnown(handle)) {
|
|
1655
|
+
var res;
|
|
1656
|
+
try { res = await fetchMedia(handle, 30000); }
|
|
1657
|
+
catch(e){ await sleep(1000); continue; }
|
|
1658
|
+
if (!res || res.status !== 200) { await sleep(1000); continue; }
|
|
1659
|
+
var data = await res.json();
|
|
1660
|
+
var m = data && data.media;
|
|
1661
|
+
if (!m) continue; // timed out empty
|
|
1662
|
+
pushChat(handle, { from: "them", media: m });
|
|
1663
|
+
if (chatWith === handle) {
|
|
1664
|
+
renderChat();
|
|
1665
|
+
} else {
|
|
1666
|
+
unread[handle] = (unread[handle] || 0) + 1;
|
|
1667
|
+
renderLiveRows(knownPeers);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
delete mediaLoops[handle];
|
|
1671
|
+
})();
|
|
1672
|
+
}
|
|
1367
1673
|
}
|
|
1368
1674
|
|
|
1369
1675
|
function renderChat(){
|
|
@@ -1379,8 +1685,30 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1379
1685
|
conv.forEach(function(m){
|
|
1380
1686
|
var el = document.createElement("div");
|
|
1381
1687
|
el.className = "cp-msg " + (m.from === "you" ? "you" : m.from === "sys" ? "sys" : "them");
|
|
1382
|
-
|
|
1383
|
-
|
|
1688
|
+
if (m.media) {
|
|
1689
|
+
var isImage = m.media.mime.indexOf("image/") === 0;
|
|
1690
|
+
var isVideo = m.media.mime.indexOf("video/") === 0;
|
|
1691
|
+
if (isImage) {
|
|
1692
|
+
var img = document.createElement("img");
|
|
1693
|
+
img.src = "data:" + m.media.mime + ";base64," + m.media.dataB64;
|
|
1694
|
+
el.appendChild(img);
|
|
1695
|
+
} else if (isVideo) {
|
|
1696
|
+
var vid = document.createElement("video");
|
|
1697
|
+
vid.src = "data:" + m.media.mime + ";base64," + m.media.dataB64;
|
|
1698
|
+
vid.controls = true;
|
|
1699
|
+
el.appendChild(vid);
|
|
1700
|
+
} else {
|
|
1701
|
+
var a = document.createElement("a");
|
|
1702
|
+
a.className = "media-link";
|
|
1703
|
+
a.href = "data:" + (m.media.mime || "application/octet-stream") + ";base64," + m.media.dataB64;
|
|
1704
|
+
a.download = m.media.name;
|
|
1705
|
+
a.textContent = "\u{1F4CE} " + m.media.name;
|
|
1706
|
+
el.appendChild(a);
|
|
1707
|
+
}
|
|
1708
|
+
} else {
|
|
1709
|
+
// textContent only \u2014 a peer's text is never parsed as HTML.
|
|
1710
|
+
el.textContent = m.text;
|
|
1711
|
+
}
|
|
1384
1712
|
chatMsgs.appendChild(el);
|
|
1385
1713
|
});
|
|
1386
1714
|
chatMsgs.scrollTop = chatMsgs.scrollHeight;
|
|
@@ -1391,7 +1719,7 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1391
1719
|
delete unread[handle];
|
|
1392
1720
|
chatTitle.textContent = handle;
|
|
1393
1721
|
chatSub.textContent = peerKnown(handle)
|
|
1394
|
-
? "live
|
|
1722
|
+
? "live peer \xB7 P2P text \xB7 not a demo profile"
|
|
1395
1723
|
: "peer offline \xB7 messages will not deliver";
|
|
1396
1724
|
chatPanel.classList.add("is-open");
|
|
1397
1725
|
renderChat();
|
|
@@ -1422,17 +1750,148 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1422
1750
|
chatSend.addEventListener("click", sendChat);
|
|
1423
1751
|
chatInput.addEventListener("keydown", function(e){ if (e.key === "Enter") sendChat(); });
|
|
1424
1752
|
|
|
1753
|
+
chatAttachBtn.addEventListener("click", function(){
|
|
1754
|
+
if (!chatWith) return;
|
|
1755
|
+
chatFileInput.click();
|
|
1756
|
+
});
|
|
1757
|
+
chatFileInput.addEventListener("change", function(e){
|
|
1758
|
+
var target = chatWith;
|
|
1759
|
+
if (!target) return;
|
|
1760
|
+
var file = e.target.files[0];
|
|
1761
|
+
if (!file) return;
|
|
1762
|
+
chatFileInput.value = ""; // clear
|
|
1763
|
+
var reader = new FileReader();
|
|
1764
|
+
reader.onload = function(evt) {
|
|
1765
|
+
var res = evt.target.result; // data:mime;base64,.....
|
|
1766
|
+
var mime = res.split(';')[0].split(':')[1] || '';
|
|
1767
|
+
var b64 = res.split(',')[1];
|
|
1768
|
+
postMedia(target, file.name, mime, b64).then(function(r){
|
|
1769
|
+
pushChat(target, r && r.status === 200
|
|
1770
|
+
? { from: "you", media: { name: file.name, mime: mime, dataB64: b64 } }
|
|
1771
|
+
: { from: "sys", text: "(file not delivered \u2014 too large or peer offline?)" });
|
|
1772
|
+
if (chatWith === target) renderChat();
|
|
1773
|
+
}).catch(function(){
|
|
1774
|
+
pushChat(target, { from: "sys", text: "(file not delivered \u2014 server unreachable)" });
|
|
1775
|
+
if (chatWith === target) renderChat();
|
|
1776
|
+
});
|
|
1777
|
+
};
|
|
1778
|
+
reader.readAsDataURL(file);
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1425
1781
|
// Render the live-peers rows: one per connected peer with its verification
|
|
1426
1782
|
// marks, a Chat button (opens the conversation), and a Call button that
|
|
1427
1783
|
// rings THAT peer specifically.
|
|
1784
|
+
// Marks from the peer's hello: \u2713 usage verified (real logs), \u{1F511} identity (signed).
|
|
1785
|
+
function peerMarksHtml(p){
|
|
1786
|
+
var html = "";
|
|
1787
|
+
if (p.verified === true) html += '<span class="mark mark-verified" title="Usage verified from real local logs">\u2713 verified</span>';
|
|
1788
|
+
if (p.identityVerified === true) html += '<span class="mark mark-identity" title="Identity signature verified">\u{1F511} identity</span>';
|
|
1789
|
+
return html;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function leaguePillHtml(leagueId){
|
|
1793
|
+
var FALLBACK = { "1M":"#d69a6e","5M":"#cfd8e6","10M":"#ffcf6b","100M":"#f0839c","1B+":"#cba8ff" };
|
|
1794
|
+
var LABELS = { "1M":"1M League","5M":"5M League","10M":"10M League","100M":"100M League","1B+":"1B+ League" };
|
|
1795
|
+
var lg = typeof window.__vdLeagueById === "function" ? window.__vdLeagueById(leagueId) : null;
|
|
1796
|
+
var hex = lg ? lg.hex : (FALLBACK[leagueId] || "#ffcf6b");
|
|
1797
|
+
var label = lg ? lg.label : (LABELS[leagueId] || ((leagueId || "?") + " League"));
|
|
1798
|
+
return '<span class="lg-pill" style="background:color-mix(in srgb,' + hex + ' 16%, transparent);border:1px solid color-mix(in srgb,' + hex + ' 40%, transparent);color:' + hex + '">' + label + '</span>';
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function avatarForPeer(handle, leagueId){
|
|
1802
|
+
var FALLBACK = { "1M":"#d69a6e","5M":"#cfd8e6","10M":"#ffcf6b","100M":"#f0839c","1B+":"#cba8ff" };
|
|
1803
|
+
var hex = (typeof window.__vdLeagueColorHex === "function")
|
|
1804
|
+
? window.__vdLeagueColorHex(leagueId)
|
|
1805
|
+
: (FALLBACK[leagueId] || "#ffcf6b");
|
|
1806
|
+
if (typeof window.__vdAvatarSVG === "function" && typeof window.__vdVariantFor === "function") {
|
|
1807
|
+
return window.__vdAvatarSVG(window.__vdVariantFor(handle), hex);
|
|
1808
|
+
}
|
|
1809
|
+
return "";
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// Compact dock rows (floating panel) + full Live now roster cards (main column).
|
|
1428
1813
|
function renderLiveRows(peers){
|
|
1429
1814
|
liveRows.innerHTML = "";
|
|
1815
|
+
var roster = document.getElementById("liveRoster");
|
|
1816
|
+
var wrap = document.getElementById("liveNowWrap");
|
|
1817
|
+
var countEl = document.getElementById("liveNowCount");
|
|
1818
|
+
var lpCount = document.getElementById("lpLiveCount");
|
|
1819
|
+
var demoWrap = document.getElementById("demoStackWrap");
|
|
1820
|
+
|
|
1821
|
+
if (countEl) countEl.textContent = String(peers.length);
|
|
1822
|
+
if (lpCount) lpCount.textContent = peers.length ? (peers.length + " online") : "";
|
|
1823
|
+
if (wrap) wrap.classList.toggle("has-live", peers.length > 0);
|
|
1824
|
+
if (demoWrap) demoWrap.classList.toggle("is-secondary", peers.length > 0);
|
|
1825
|
+
|
|
1826
|
+
if (roster) {
|
|
1827
|
+
roster.innerHTML = "";
|
|
1828
|
+
if (peers.length === 0) {
|
|
1829
|
+
var empty = document.createElement("div");
|
|
1830
|
+
empty.className = "live-empty";
|
|
1831
|
+
empty.innerHTML = "<strong>No live peers right now</strong><p>When someone on your league topic connects, they show up here \u2014 clearly separate from the demo pool below.</p>";
|
|
1832
|
+
roster.appendChild(empty);
|
|
1833
|
+
} else {
|
|
1834
|
+
peers.forEach(function(p){
|
|
1835
|
+
var card = document.createElement("article");
|
|
1836
|
+
card.className = "live-card";
|
|
1837
|
+
|
|
1838
|
+
var av = document.createElement("div");
|
|
1839
|
+
av.className = "lc-avatar";
|
|
1840
|
+
av.innerHTML = avatarForPeer(p.handle, p.league);
|
|
1841
|
+
|
|
1842
|
+
var body = document.createElement("div");
|
|
1843
|
+
body.className = "lc-body";
|
|
1844
|
+
var handleRow = document.createElement("div");
|
|
1845
|
+
handleRow.className = "lc-handle-row";
|
|
1846
|
+
var handleEl = document.createElement("div");
|
|
1847
|
+
handleEl.className = "lc-handle";
|
|
1848
|
+
handleEl.textContent = p.handle;
|
|
1849
|
+
handleRow.appendChild(handleEl);
|
|
1850
|
+
var liveMark = document.createElement("span");
|
|
1851
|
+
liveMark.className = "mark mark-live";
|
|
1852
|
+
liveMark.textContent = "live";
|
|
1853
|
+
handleRow.appendChild(liveMark);
|
|
1854
|
+
body.appendChild(handleRow);
|
|
1855
|
+
|
|
1856
|
+
var meta = document.createElement("div");
|
|
1857
|
+
meta.className = "lc-meta";
|
|
1858
|
+
meta.innerHTML = leaguePillHtml(p.league) + peerMarksHtml(p) +
|
|
1859
|
+
'<span style="opacity:.75">' + (p.harness || "") + "</span>";
|
|
1860
|
+
body.appendChild(meta);
|
|
1861
|
+
|
|
1862
|
+
var actions = document.createElement("div");
|
|
1863
|
+
actions.className = "lc-actions";
|
|
1864
|
+
var chatBtn = document.createElement("button");
|
|
1865
|
+
chatBtn.className = "btn-chat" + (unread[p.handle] ? " has-unread" : "");
|
|
1866
|
+
chatBtn.type = "button";
|
|
1867
|
+
chatBtn.textContent = "Chat" + (unread[p.handle] ? " (" + unread[p.handle] + ")" : "");
|
|
1868
|
+
chatBtn.addEventListener("click", function(){ openChat(p.handle); });
|
|
1869
|
+
var callBtn = document.createElement("button");
|
|
1870
|
+
callBtn.className = "btn-call";
|
|
1871
|
+
callBtn.type = "button";
|
|
1872
|
+
callBtn.textContent = "Call";
|
|
1873
|
+
callBtn.disabled = !!(rtc.remoteHandle || pendingOffer);
|
|
1874
|
+
callBtn.addEventListener("click", function(){
|
|
1875
|
+
if (rtc.remoteHandle || pendingOffer) return;
|
|
1876
|
+
callBtn.disabled = true;
|
|
1877
|
+
startCallAsOfferer(p.handle).catch(function(){ hangup(); });
|
|
1878
|
+
});
|
|
1879
|
+
actions.appendChild(chatBtn);
|
|
1880
|
+
actions.appendChild(callBtn);
|
|
1881
|
+
|
|
1882
|
+
card.appendChild(av);
|
|
1883
|
+
card.appendChild(body);
|
|
1884
|
+
card.appendChild(actions);
|
|
1885
|
+
roster.appendChild(card);
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// Compact floating dock \u2014 still handy when scrolled away from the roster.
|
|
1430
1891
|
peers.forEach(function(p){
|
|
1431
1892
|
var row = document.createElement("div");
|
|
1432
1893
|
row.className = "live-row";
|
|
1433
1894
|
var who = document.createElement("div");
|
|
1434
|
-
// Marks from the peer's hello, shown only when present: \u2713 usage
|
|
1435
|
-
// verified (real local logs), \u{1F511} identity-verified (signed hello).
|
|
1436
1895
|
var marks = (p.verified === true ? " \u2713" : "") + (p.identityVerified === true ? " \u{1F511}" : "");
|
|
1437
1896
|
var h = document.createElement("div"); h.className = "h"; h.textContent = p.handle + marks;
|
|
1438
1897
|
var s = document.createElement("div"); s.className = "s"; s.textContent = p.league + " \xB7 " + p.harness;
|
|
@@ -1447,9 +1906,8 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1447
1906
|
var btn = document.createElement("button");
|
|
1448
1907
|
btn.className = "vbtn"; btn.type = "button"; btn.textContent = "Call";
|
|
1449
1908
|
btn.addEventListener("click", function(){
|
|
1450
|
-
if (rtc.remoteHandle || pendingOffer) return;
|
|
1909
|
+
if (rtc.remoteHandle || pendingOffer) return;
|
|
1451
1910
|
btn.disabled = true;
|
|
1452
|
-
// DIRECTED call: the rtc-offer is relayed only to this peer's handle.
|
|
1453
1911
|
startCallAsOfferer(p.handle).catch(function(){ hangup(); });
|
|
1454
1912
|
});
|
|
1455
1913
|
actions.appendChild(chatBtn);
|
|
@@ -1470,14 +1928,16 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1470
1928
|
peers.forEach(function(p){ ensureChatLoop(p.handle); });
|
|
1471
1929
|
if (chatWith) {
|
|
1472
1930
|
chatSub.textContent = peerKnown(chatWith)
|
|
1473
|
-
? "live
|
|
1931
|
+
? "live peer \xB7 P2P text"
|
|
1474
1932
|
: "peer offline \xB7 messages will not deliver";
|
|
1475
1933
|
}
|
|
1476
|
-
if (peers.length === 0) { livePanel.classList.remove("is-open"); return; }
|
|
1477
|
-
livePanel.classList.add("is-open");
|
|
1478
1934
|
renderLiveRows(peers);
|
|
1935
|
+
if (peers.length === 0) { livePanel.classList.remove("is-open"); }
|
|
1936
|
+
else { livePanel.classList.add("is-open"); }
|
|
1479
1937
|
} catch(e){}
|
|
1480
1938
|
}
|
|
1939
|
+
// Initial empty state for the main Live now section, then poll.
|
|
1940
|
+
renderLiveRows([]);
|
|
1481
1941
|
refreshPeers();
|
|
1482
1942
|
setInterval(refreshPeers, 4000);
|
|
1483
1943
|
|
|
@@ -1718,6 +2178,8 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1718
2178
|
roomMeta.textContent = roomName +
|
|
1719
2179
|
(roomVid.self ? " \xB7 you " + roomVid.self : "") +
|
|
1720
2180
|
" \xB7 " + n + " other" + (n === 1 ? "" : "s");
|
|
2181
|
+
var rpCount = document.getElementById("rpCount");
|
|
2182
|
+
if (rpCount) rpCount.textContent = n ? (n + " other" + (n === 1 ? "" : "s")) : "empty";
|
|
1721
2183
|
roomPanel.classList.add("is-open");
|
|
1722
2184
|
roomJoinBtn.disabled = roomVid.joined || !roomVid.self;
|
|
1723
2185
|
roomLeaveBtn.disabled = !roomVid.joined;
|
|
@@ -1750,7 +2212,7 @@ function createLiveBridge() {
|
|
|
1750
2212
|
},
|
|
1751
2213
|
addLink(link) {
|
|
1752
2214
|
const handle2 = link.hello.handle;
|
|
1753
|
-
boxes.set(handle2, { link, incoming: [], messages: [] });
|
|
2215
|
+
boxes.set(handle2, { link, incoming: [], messages: [], media: [] });
|
|
1754
2216
|
link.onSignal((f) => {
|
|
1755
2217
|
const mb = boxes.get(handle2);
|
|
1756
2218
|
if (mb) mb.incoming.push(f);
|
|
@@ -1763,6 +2225,20 @@ function createLiveBridge() {
|
|
|
1763
2225
|
mb.messages.splice(0, mb.messages.length - MAX_QUEUED_MESSAGES);
|
|
1764
2226
|
}
|
|
1765
2227
|
});
|
|
2228
|
+
link.onMedia((m) => {
|
|
2229
|
+
const mb = boxes.get(handle2);
|
|
2230
|
+
if (!mb) {
|
|
2231
|
+
fs.unlink(m.path, () => {
|
|
2232
|
+
});
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
mb.media.push({ ...m, name: sanitizePeerText(m.name), mime: sanitizePeerText(m.mime) });
|
|
2236
|
+
if (mb.media.length > MAX_QUEUED_MESSAGES) {
|
|
2237
|
+
const dropped = mb.media.splice(0, mb.media.length - MAX_QUEUED_MESSAGES);
|
|
2238
|
+
for (const d of dropped) fs.unlink(d.path, () => {
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
});
|
|
1766
2242
|
link.onClose(() => {
|
|
1767
2243
|
const cur = boxes.get(handle2);
|
|
1768
2244
|
if (cur && cur.link === link) boxes.delete(handle2);
|
|
@@ -1799,6 +2275,39 @@ function createLiveBridge() {
|
|
|
1799
2275
|
if (cur.messages.length > 0) return cur.messages.shift() ?? null;
|
|
1800
2276
|
}
|
|
1801
2277
|
return null;
|
|
2278
|
+
},
|
|
2279
|
+
async sendMedia(handle2, filePath) {
|
|
2280
|
+
const mb = boxes.get(handle2);
|
|
2281
|
+
if (!mb) throw new Error("Peer not found");
|
|
2282
|
+
await mb.link.sendMedia(filePath);
|
|
2283
|
+
},
|
|
2284
|
+
async pollMedia(handle2, timeoutMs) {
|
|
2285
|
+
const mb = boxes.get(handle2);
|
|
2286
|
+
if (!mb) return null;
|
|
2287
|
+
let targetMedia = null;
|
|
2288
|
+
if (mb.media.length > 0) {
|
|
2289
|
+
targetMedia = mb.media.shift() ?? null;
|
|
2290
|
+
} else {
|
|
2291
|
+
const deadline = Date.now() + timeoutMs;
|
|
2292
|
+
while (Date.now() < deadline) {
|
|
2293
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2294
|
+
const cur = boxes.get(handle2);
|
|
2295
|
+
if (!cur) return null;
|
|
2296
|
+
if (cur.media.length > 0) {
|
|
2297
|
+
targetMedia = cur.media.shift() ?? null;
|
|
2298
|
+
break;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
if (!targetMedia) return null;
|
|
2303
|
+
try {
|
|
2304
|
+
const buf = await fs.promises.readFile(targetMedia.path);
|
|
2305
|
+
await fs.promises.unlink(targetMedia.path).catch(() => {
|
|
2306
|
+
});
|
|
2307
|
+
return { name: targetMedia.name, mime: targetMedia.mime, dataB64: buf.toString("base64") };
|
|
2308
|
+
} catch (err) {
|
|
2309
|
+
return null;
|
|
2310
|
+
}
|
|
1802
2311
|
}
|
|
1803
2312
|
};
|
|
1804
2313
|
return bridge;
|
|
@@ -1916,10 +2425,18 @@ function send(res, status, contentType, body) {
|
|
|
1916
2425
|
function sendJson(res, status, data) {
|
|
1917
2426
|
send(res, status, "application/json; charset=utf-8", JSON.stringify(data));
|
|
1918
2427
|
}
|
|
1919
|
-
function readBody(req) {
|
|
2428
|
+
function readBody(req, maxLength) {
|
|
1920
2429
|
return new Promise((resolve, reject) => {
|
|
1921
2430
|
const chunks = [];
|
|
1922
|
-
|
|
2431
|
+
let len = 0;
|
|
2432
|
+
req.on("data", (c) => {
|
|
2433
|
+
chunks.push(c);
|
|
2434
|
+
len += c.length;
|
|
2435
|
+
if (maxLength !== void 0 && len > maxLength) {
|
|
2436
|
+
req.destroy();
|
|
2437
|
+
reject(new Error("payload too large"));
|
|
2438
|
+
}
|
|
2439
|
+
});
|
|
1923
2440
|
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
1924
2441
|
req.on("error", reject);
|
|
1925
2442
|
});
|
|
@@ -2110,6 +2627,69 @@ async function handle(req, res, opts) {
|
|
|
2110
2627
|
sendJson(res, 200, { ok: true });
|
|
2111
2628
|
return;
|
|
2112
2629
|
}
|
|
2630
|
+
if (req.method === "GET" && pathname === "/live/media") {
|
|
2631
|
+
const live = opts.live;
|
|
2632
|
+
if (!live) {
|
|
2633
|
+
sendJson(res, 200, { media: null, reason: "live-not-attached" });
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
const handle2 = url.searchParams.get("handle") ?? "";
|
|
2637
|
+
if (handle2 === "") {
|
|
2638
|
+
sendJson(res, 400, { error: "missing handle" });
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
const media = await live.pollMedia(handle2, 25e3);
|
|
2642
|
+
if (req.destroyed || res.writableEnded) return;
|
|
2643
|
+
sendJson(res, 200, { media });
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
if (req.method === "POST" && pathname === "/live/media") {
|
|
2647
|
+
const live = opts.live;
|
|
2648
|
+
if (!live) {
|
|
2649
|
+
sendJson(res, 400, { error: "live-not-attached" });
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
let body;
|
|
2653
|
+
try {
|
|
2654
|
+
body = await readBody(req, 35 * 1024 * 1024);
|
|
2655
|
+
} catch (e) {
|
|
2656
|
+
sendJson(res, 413, { error: "payload too large" });
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
let parsed = {};
|
|
2660
|
+
try {
|
|
2661
|
+
parsed = JSON.parse(body);
|
|
2662
|
+
} catch {
|
|
2663
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
const handle2 = typeof parsed["handle"] === "string" ? parsed["handle"] : "";
|
|
2667
|
+
const name = typeof parsed["name"] === "string" ? parsed["name"] : "";
|
|
2668
|
+
const mime = typeof parsed["mime"] === "string" ? parsed["mime"] : "";
|
|
2669
|
+
const dataB64 = typeof parsed["dataB64"] === "string" ? parsed["dataB64"] : "";
|
|
2670
|
+
if (handle2 === "" || name === "" || mime === "" || dataB64 === "") {
|
|
2671
|
+
sendJson(res, 400, { error: "missing handle, name, mime, or dataB64" });
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
const decoded = Buffer.from(dataB64, "base64");
|
|
2675
|
+
if (decoded.length > 25 * 1024 * 1024) {
|
|
2676
|
+
sendJson(res, 413, { error: "file exceeds 25MiB limit" });
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
const tmpPath = path.join(os.tmpdir(), `vibe-media-send-${randomUUID()}`);
|
|
2680
|
+
await fs.promises.writeFile(tmpPath, decoded);
|
|
2681
|
+
try {
|
|
2682
|
+
await live.sendMedia(handle2, tmpPath);
|
|
2683
|
+
} catch (err) {
|
|
2684
|
+
sendJson(res, 500, { error: err.message });
|
|
2685
|
+
return;
|
|
2686
|
+
} finally {
|
|
2687
|
+
await fs.promises.unlink(tmpPath).catch(() => {
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
sendJson(res, 200, { ok: true });
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2113
2693
|
if (req.method === "GET" && pathname === "/api/room") {
|
|
2114
2694
|
const room = opts.room;
|
|
2115
2695
|
sendJson(res, 200, room ? { room: room.name, self: room.self ?? null, members: room.members } : { room: null, self: null, members: [] });
|
|
@@ -2204,7 +2784,7 @@ async function handle(req, res, opts) {
|
|
|
2204
2784
|
}
|
|
2205
2785
|
|
|
2206
2786
|
// src/cli.ts
|
|
2207
|
-
var VERSION = "0.8.
|
|
2787
|
+
var VERSION = "0.8.2";
|
|
2208
2788
|
function parsePort(raw) {
|
|
2209
2789
|
const n = Number(raw);
|
|
2210
2790
|
if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
|
|
@@ -2630,6 +3210,16 @@ async function cmdOpen(port, any, room) {
|
|
|
2630
3210
|
function shouldKeepAlive(flag, stdinIsTTY) {
|
|
2631
3211
|
return flag || stdinIsTTY !== true;
|
|
2632
3212
|
}
|
|
3213
|
+
function parseSendCommand(text) {
|
|
3214
|
+
const parts = text.split(" ");
|
|
3215
|
+
const cmd = parts[0];
|
|
3216
|
+
if (cmd === "/send" || cmd === "/file" || cmd === "/image") {
|
|
3217
|
+
const path2 = text.slice(cmd.length).trim();
|
|
3218
|
+
if (!path2) return { error: "missing_path" };
|
|
3219
|
+
return { path: path2 };
|
|
3220
|
+
}
|
|
3221
|
+
return null;
|
|
3222
|
+
}
|
|
2633
3223
|
async function cmdLiveViaRelay(profile, to) {
|
|
2634
3224
|
const target = to !== void 0 ? normalizeHandle(to) : null;
|
|
2635
3225
|
if (to !== void 0 && target === null) {
|
|
@@ -2799,6 +3389,12 @@ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
|
|
|
2799
3389
|
process2.stdout.write(` \u2605 found ${sanitizePeerText(link.hello.handle)} \u2014 auto-opening
|
|
2800
3390
|
`);
|
|
2801
3391
|
}
|
|
3392
|
+
link.onMedia((m) => {
|
|
3393
|
+
process2.stdout.write(
|
|
3394
|
+
` \u{1F4CE} <${sanitizePeerText(link.hello.handle)}> sent ${sanitizePeerText(m.name)} \u2014 saved to ${m.path}
|
|
3395
|
+
`
|
|
3396
|
+
);
|
|
3397
|
+
});
|
|
2802
3398
|
pairing.add(link);
|
|
2803
3399
|
}
|
|
2804
3400
|
});
|
|
@@ -2806,7 +3402,7 @@ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
|
|
|
2806
3402
|
` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026${topics.length > 1 ? ` (+${topics.length - 1} more)` : ""}
|
|
2807
3403
|
`
|
|
2808
3404
|
);
|
|
2809
|
-
process2.stdout.write(" type to chat \xB7 /next \xB7 /open <handle> \xB7 /quit\n");
|
|
3405
|
+
process2.stdout.write(" type to chat \xB7 /send <path> \xB7 /next \xB7 /open <handle> \xB7 /quit\n");
|
|
2810
3406
|
process2.stdout.write(" video chat: live A/V runs in the web app \u2014 run `vibedating open`\n\n");
|
|
2811
3407
|
const rl = readline.createInterface({ input: process2.stdin, terminal: false });
|
|
2812
3408
|
const stop = () => {
|
|
@@ -2829,6 +3425,28 @@ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
|
|
|
2829
3425
|
const handle2 = text.slice("/open ".length).trim();
|
|
2830
3426
|
if (pairing.open(handle2) === void 0) {
|
|
2831
3427
|
process2.stdout.write(` \xB7 no available peer "${handle2}"
|
|
3428
|
+
`);
|
|
3429
|
+
}
|
|
3430
|
+
continue;
|
|
3431
|
+
}
|
|
3432
|
+
const sendRes = parseSendCommand(text);
|
|
3433
|
+
if (sendRes !== null) {
|
|
3434
|
+
if ("error" in sendRes) {
|
|
3435
|
+
process2.stdout.write(" usage: /send <path>\n");
|
|
3436
|
+
continue;
|
|
3437
|
+
}
|
|
3438
|
+
const cur3 = pairing.current();
|
|
3439
|
+
if (cur3 === void 0) {
|
|
3440
|
+
process2.stdout.write(" \xB7 no peer yet \u2014 waiting for a match\u2026\n");
|
|
3441
|
+
continue;
|
|
3442
|
+
}
|
|
3443
|
+
try {
|
|
3444
|
+
process2.stdout.write(` \u{1F4CE} sending ${sendRes.path}\u2026
|
|
3445
|
+
`);
|
|
3446
|
+
await cur3.sendMedia(sendRes.path);
|
|
3447
|
+
process2.stdout.write(" \u2713 sent\n");
|
|
3448
|
+
} catch (err) {
|
|
3449
|
+
process2.stdout.write(` \u2717 failed to send media: ${err instanceof Error ? err.message : String(err)}
|
|
2832
3450
|
`);
|
|
2833
3451
|
}
|
|
2834
3452
|
continue;
|
|
@@ -3303,5 +3921,6 @@ if (entryUrl !== void 0) {
|
|
|
3303
3921
|
export {
|
|
3304
3922
|
formatAgo,
|
|
3305
3923
|
parseArgs,
|
|
3924
|
+
parseSendCommand,
|
|
3306
3925
|
shouldKeepAlive
|
|
3307
3926
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibedate",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Dating by tokens — matched by usage league across agentic CLIs. Raw usage stays local; only your league is shared. CLI + local web app + MCP. Local-first.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|