Live video on the AT Protocol

Merge branch 'next' into v/chat-mod-lexicon

+5250 -1030
+6
.github/workflows/build.yaml
··· 38 38 fetch-depth: 0 39 39 ref: ${{ github.event.pull_request.head.sha }} 40 40 41 + - name: mise check 42 + if: matrix.name == 'linux-amd64' 43 + uses: jdx/mise-action@v2 44 + with: 45 + install: true 46 + 41 47 - name: Log in to the Container registry 42 48 uses: docker/login-action@v2 43 49 with:
+1 -1
.vscode/launch.json
··· 13 13 "type": "go", 14 14 "request": "launch", 15 15 "mode": "exec", 16 - "program": "${workspaceFolder}/build-darwin-amd64/libstreamplace" 16 + "program": "${workspaceFolder}/build-darwin-arm64/libstreamplace" 17 17 } 18 18 ] 19 19 }
+4 -1
Makefile
··· 170 170 -D "gst-plugins-good:multifile=enabled" \ 171 171 -D "gst-plugins-good:rtp=enabled" \ 172 172 -D "gst-plugins-bad:fdkaac=enabled" \ 173 + -D "gst-plugins-bad:rtmp2=enabled" \ 173 174 -D "gst-plugins-good:audioparsers=enabled" \ 174 175 -D "gst-plugins-good:isomp4=enabled" \ 175 176 -D "gst-plugins-good:png=enabled" \ 176 177 -D "gst-plugins-good:videobox=enabled" \ 177 178 -D "gst-plugins-good:jpeg=enabled" \ 178 179 -D "gst-plugins-good:audioparsers=enabled" \ 180 + -D "gst-plugins-good:flv=enabled" \ 179 181 -D "gst-plugins-bad:videoparsers=enabled" \ 180 182 -D "gst-plugins-bad:mpegtsmux=enabled" \ 181 183 -D "gst-plugins-bad:mpegtsdemux=enabled" \ ··· 185 187 -D "gst-plugins-ugly:gpl=enabled" \ 186 188 -D "x264:asm=enabled" \ 187 189 -D "gstreamer-full:gst-full=enabled" \ 188 - -D "gstreamer-full:gst-full-plugins=libgstopusparse.a;libgstcodectimestamper.a;libgstrtp.a;libgstaudioresample.a;libgstlibav.a;libgstmatroska.a;libgstmultifile.a;libgstjpeg.a;libgstaudiotestsrc.a;libgstaudioconvert.a;libgstaudioparsers.a;libgstfdkaac.a;libgstisomp4.a;libgstapp.a;libgstvideoconvertscale.a;libgstvideobox.a;libgstvideorate.a;libgstpng.a;libgstcompositor.a;libgstaudiorate.a;libgstx264.a;libgstopus.a;libgstvideotestsrc.a;libgstvideoparsersbad.a;libgstaudioparsers.a;libgstmpegtsmux.a;libgstmpegtsdemux.a;libgstplayback.a;libgsttypefindfunctions.a;libgstcoretracers.a;libgstcodec2json.a" \ 190 + -D "gstreamer-full:gst-full-plugins=libgstflv.a;libgstrtmp2.a;libgstopusparse.a;libgstcodectimestamper.a;libgstrtp.a;libgstaudioresample.a;libgstlibav.a;libgstmatroska.a;libgstmultifile.a;libgstjpeg.a;libgstaudiotestsrc.a;libgstaudioconvert.a;libgstaudioparsers.a;libgstfdkaac.a;libgstisomp4.a;libgstapp.a;libgstvideoconvertscale.a;libgstvideobox.a;libgstvideorate.a;libgstpng.a;libgstcompositor.a;libgstaudiorate.a;libgstx264.a;libgstopus.a;libgstvideotestsrc.a;libgstvideoparsersbad.a;libgstaudioparsers.a;libgstmpegtsmux.a;libgstmpegtsdemux.a;libgstplayback.a;libgsttypefindfunctions.a;libgstcoretracers.a;libgstcodec2json.a" \ 189 191 -D "gstreamer-full:gst-full-libraries=gstreamer-controller-1.0,gstreamer-plugins-base-1.0,gstreamer-pbutils-1.0" \ 190 192 -D "gstreamer-full:gst-full-elements=coreelements:concat,filesrc,filesink,queue,queue2,multiqueue,typefind,tee,capsfilter,fakesink,identity" \ 191 193 -D "gstreamer-full:bad=enabled" \ ··· 307 309 go install github.com/jstemmer/go-junit-report/v2@latest \ 308 310 && PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) \ 309 311 LD_LIBRARY_PATH=$(shell realpath $(BUILDDIR))/lib \ 312 + CGO_LDFLAGS="-lm" \ 310 313 bash -euo pipefail -c "go test -p 1 -timeout 300s ./pkg/... -v | tee /dev/stderr | go-junit-report -out test.xml" 311 314 312 315 .PHONY: iroh-test
+20 -16
go.mod
··· 17 17 github.com/NYTimes/gziphandler v1.1.1 18 18 github.com/ThalesGroup/crypto11 v0.0.0-00010101000000-000000000000 19 19 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d 20 + github.com/bluenviron/gortmplib v0.1.2 21 + github.com/bluenviron/gortsplib/v5 v5.2.1 22 + github.com/bluenviron/mediacommon/v2 v2.5.2 20 23 github.com/bluesky-social/indigo v0.0.0-20251206005924-d49b45419635 21 24 github.com/cenkalti/backoff v2.2.1+incompatible 22 25 github.com/cenkalti/backoff/v5 v5.0.2 ··· 48 51 github.com/patrickmn/go-cache v2.1.0+incompatible 49 52 github.com/peterbourgon/ff/v3 v3.4.0 50 53 github.com/pion/interceptor v0.1.37 51 - github.com/pion/rtcp v1.2.15 54 + github.com/pion/rtcp v1.2.16 52 55 github.com/pion/webrtc/v4 v4.0.11 53 56 github.com/piprate/json-gold v0.5.0 54 57 github.com/prometheus/client_golang v1.23.0 ··· 59 62 github.com/starttoaster/prometheus-exporter-scraper v0.0.1 60 63 github.com/streamplace/atproto-oauth-golang v0.0.0-20250619231223-a9c04fb888ac 61 64 github.com/streamplace/oatproxy v0.0.0-20251207230012-236e9bd6dae6 62 - github.com/stretchr/testify v1.10.0 65 + github.com/stretchr/testify v1.11.1 63 66 github.com/tdewolff/canvas v0.0.0-20250728095813-50d4cb1eee71 64 67 github.com/whyrusleeping/cbor-gen v0.3.1 65 68 github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 ··· 69 72 go.opentelemetry.io/otel/sdk v1.36.0 70 73 go.uber.org/goleak v1.3.0 71 74 golang.org/x/image v0.30.0 72 - golang.org/x/net v0.43.0 73 - golang.org/x/sync v0.16.0 74 - golang.org/x/term v0.34.0 75 + golang.org/x/net v0.47.0 76 + golang.org/x/sync v0.18.0 77 + golang.org/x/term v0.37.0 75 78 golang.org/x/time v0.12.0 76 - golang.org/x/tools v0.36.0 79 + golang.org/x/tools v0.38.0 77 80 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da 78 81 google.golang.org/api v0.228.0 79 82 gorm.io/datatypes v1.2.4 ··· 86 89 github.com/jinzhu/inflection v1.0.0 // indirect 87 90 github.com/jinzhu/now v1.1.5 // indirect 88 91 github.com/mattn/go-isatty v0.0.20 89 - golang.org/x/sys v0.35.0 // indirect 92 + golang.org/x/sys v0.38.0 // indirect 90 93 gorm.io/gorm v1.26.1 91 94 ) 92 95 ··· 137 140 github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b // indirect 138 141 github.com/StackExchange/wmi v1.2.1 // indirect 139 142 github.com/VictoriaMetrics/fastcache v1.13.0 // indirect 143 + github.com/abema/go-mp4 v1.4.1 // indirect 140 144 github.com/agnivade/levenshtein v1.2.0 // indirect 141 145 github.com/alecthomas/chroma/v2 v2.17.2 // indirect 142 146 github.com/alecthomas/go-check-sumtype v0.3.1 // indirect ··· 149 153 github.com/ashanbrown/forbidigo v1.6.0 // indirect 150 154 github.com/ashanbrown/makezero v1.2.0 // indirect 151 155 github.com/asticode/go-astikit v0.30.0 // indirect 152 - github.com/asticode/go-astits v1.13.0 // indirect 156 + github.com/asticode/go-astits v1.14.0 // indirect 153 157 github.com/aws/aws-sdk-go v1.44.273 // indirect 154 158 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 155 159 github.com/benoitkugler/textlayout v0.3.1 // indirect ··· 159 163 github.com/bkielbasa/cyclop v1.2.3 // indirect 160 164 github.com/blizzy78/varnamelen v0.8.0 // indirect 161 165 github.com/bluenviron/gortsplib/v4 v4.12.3 // indirect 162 - github.com/bluenviron/mediacommon/v2 v2.4.0 // indirect 163 166 github.com/bombsimon/wsl/v4 v4.7.0 // indirect 164 167 github.com/breml/bidichk v0.3.3 // indirect 165 168 github.com/breml/errchkjson v0.4.1 // indirect ··· 398 401 github.com/pion/logging v0.2.4 // indirect 399 402 github.com/pion/mdns/v2 v2.0.7 // indirect 400 403 github.com/pion/randutil v0.1.0 // indirect 401 - github.com/pion/rtp v1.8.21 // indirect 404 + github.com/pion/rtp v1.8.26 // indirect 402 405 github.com/pion/sctp v1.8.37 // indirect 403 - github.com/pion/sdp/v3 v3.0.10 // indirect 404 - github.com/pion/srtp/v3 v3.0.4 // indirect 406 + github.com/pion/sdp/v3 v3.0.16 // indirect 407 + github.com/pion/srtp/v3 v3.0.9 // indirect 405 408 github.com/pion/stun/v3 v3.0.0 // indirect 406 - github.com/pion/transport/v3 v3.0.7 // indirect 409 + github.com/pion/transport/v3 v3.1.1 // indirect 407 410 github.com/pion/turn/v4 v4.0.0 // indirect 408 411 github.com/pjbgf/sha1cd v0.3.0 // indirect 409 412 github.com/pkg/errors v0.9.1 // indirect ··· 507 510 go.uber.org/automaxprocs v1.6.0 // indirect 508 511 go.uber.org/multierr v1.11.0 // indirect 509 512 go.uber.org/zap v1.27.0 // indirect 510 - golang.org/x/crypto v0.41.0 // indirect 513 + golang.org/x/crypto v0.44.0 // indirect 511 514 golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect 512 515 golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect 513 - golang.org/x/mod v0.27.0 // indirect 516 + golang.org/x/mod v0.29.0 // indirect 514 517 golang.org/x/oauth2 v0.30.0 // indirect 515 - golang.org/x/text v0.28.0 // indirect 518 + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect 519 + golang.org/x/text v0.31.0 // indirect 516 520 gonum.org/v1/plot v0.16.0 // indirect 517 521 google.golang.org/appengine/v2 v2.0.2 // indirect 518 522 google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
+47 -34
go.sum
··· 130 130 github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= 131 131 github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= 132 132 github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= 133 + github.com/abema/go-mp4 v1.4.1 h1:YoS4VRqd+pAmddRPLFf8vMk74kuGl6ULSjzhsIqwr6M= 134 + github.com/abema/go-mp4 v1.4.1/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws= 133 135 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= 134 136 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= 135 137 github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= ··· 182 184 github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4= 183 185 github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= 184 186 github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= 185 - github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= 186 - github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= 187 + github.com/asticode/go-astits v1.14.0 h1:zkgnZzipx2XX5mWycqsSBeEyDH58+i4HtyF4j2ROb00= 188 + github.com/asticode/go-astits v1.14.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= 187 189 github.com/aws/aws-sdk-go v1.44.273 h1:CX8O0gK+cGrgUyv7bgJ6QQP9mQg7u5mweHdNzULH47c= 188 190 github.com/aws/aws-sdk-go v1.44.273/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= 189 191 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= ··· 209 211 github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= 210 212 github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= 211 213 github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= 214 + github.com/bluenviron/gortmplib v0.1.2 h1:bJ+5pUxKnHjEoKPbNs7lDLyYy5R9Z1lISJ1n54DsjWE= 215 + github.com/bluenviron/gortmplib v0.1.2/go.mod h1:oVOWgfs7wsZNoKYLttyqLar7a71ZTohO1JOVXkNxtHg= 212 216 github.com/bluenviron/gortsplib/v4 v4.12.3 h1:3EzbyGb5+MIOJQYiWytRegFEP4EW5paiyTrscQj63WE= 213 217 github.com/bluenviron/gortsplib/v4 v4.12.3/go.mod h1:SkZPdaMNr+IvHt2PKRjUXxZN6FDutmSZn4eT0GmF0sk= 214 - github.com/bluenviron/mediacommon/v2 v2.4.0 h1:Ss1T7AMxTrICJ+a/N5urS/1lp1ZpsF+3iJq3B/RLDMw= 215 - github.com/bluenviron/mediacommon/v2 v2.4.0/go.mod h1:a6MbPmXtYda9mKibKVMZlW20GYLLrX2R7ZkUE+1pwV0= 218 + github.com/bluenviron/gortsplib/v5 v5.2.1 h1:VrFC5RU8npiyKiqLFKXZmdUFChSmbjO5eZBfOUgxwjo= 219 + github.com/bluenviron/gortsplib/v5 v5.2.1/go.mod h1:sK4+00XQaSpU2iPIKjmhj6Yye+sVbNWEU2IJWYEZI9U= 220 + github.com/bluenviron/mediacommon/v2 v2.5.2 h1:eq7LHJFksDAVtVdTrwOUl7dO7LE8eKwLgYKYi5MmYaY= 221 + github.com/bluenviron/mediacommon/v2 v2.5.2/go.mod h1:5V15TiOfeaNVmZPVuOqAwqQSWyvMV86/dijDKu5q9Zs= 216 222 github.com/bluesky-social/indigo v0.0.0-20251206005924-d49b45419635 h1:kNeRrgGJH2g5OvjLqtaQ744YXqduliZYpFkJ/ld47c0= 217 223 github.com/bluesky-social/indigo v0.0.0-20251206005924-d49b45419635/go.mod h1:Pm2I1+iDXn/hLbF7XCg/DsZi6uDCiOo7hZGWprSM7k0= 218 224 github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= ··· 299 305 github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= 300 306 github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= 301 307 github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= 308 + github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 302 309 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 303 310 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 304 311 github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= ··· 860 867 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 861 868 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 862 869 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 870 + github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 863 871 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 864 872 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 865 873 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= ··· 1053 1061 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 1054 1062 github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU= 1055 1063 github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA= 1064 + github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw= 1065 + github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= 1056 1066 github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= 1057 1067 github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= 1058 1068 github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= ··· 1094 1104 github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= 1095 1105 github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 1096 1106 github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 1097 - github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= 1098 - github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= 1099 - github.com/pion/rtp v1.8.21 h1:3yrOwmZFyUpcIosNcWRpQaU+UXIJ6yxLuJ8Bx0mw37Y= 1100 - github.com/pion/rtp v1.8.21/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= 1107 + github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= 1108 + github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= 1109 + github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc= 1110 + github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= 1101 1111 github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs= 1102 1112 github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= 1103 - github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA= 1104 - github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= 1105 - github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= 1106 - github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= 1113 + github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo= 1114 + github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= 1115 + github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY= 1116 + github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8= 1107 1117 github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= 1108 1118 github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= 1109 1119 github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= ··· 1112 1122 github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= 1113 1123 github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= 1114 1124 github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= 1115 - github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= 1116 - github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= 1125 + github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= 1126 + github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= 1117 1127 github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= 1118 1128 github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= 1119 1129 github.com/pion/webrtc/v4 v4.0.11 h1:0i7BNFH2n8LVp08q/dqM5iyZBXW4TITbD1+RwNqk/iY= ··· 1307 1317 github.com/streamplace/atproto-oauth-golang v0.0.0-20250619231223-a9c04fb888ac/go.mod h1:9LlKkqciiO5lRfbX0n4Wn5KNY9nvFb4R3by8FdW2TWc= 1308 1318 github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4 h1:L1fS4HJSaAyNnkwfuZubgfeZy8rkWmA0cMtH5Z0HqNc= 1309 1319 github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4/go.mod h1:bGUXY9Wd4mnd+XUrOYZr358J2f6z9QO/dLhL1SsiD+0= 1310 - github.com/streamplace/oatproxy v0.0.0-20251201231246-9e9aa13c659d h1:xVIF467970izRZBXBC+/XWyCB6zBtxwZ1KGytEp1rTc= 1311 - github.com/streamplace/oatproxy v0.0.0-20251201231246-9e9aa13c659d/go.mod h1:pXi24hA7xBHj8eEywX6wGqJOR9FaEYlGwQ/72rN6okw= 1312 1320 github.com/streamplace/oatproxy v0.0.0-20251207230012-236e9bd6dae6 h1:Y81F18H+qQGWk58Vqangsw75XQ6G1shJOsUEqgKQdYI= 1313 1321 github.com/streamplace/oatproxy v0.0.0-20251207230012-236e9bd6dae6/go.mod h1:pXi24hA7xBHj8eEywX6wGqJOR9FaEYlGwQ/72rN6okw= 1314 1322 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= ··· 1328 1336 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 1329 1337 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 1330 1338 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 1331 - github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 1332 - github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 1339 + github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 1340 + github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 1333 1341 github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= 1334 1342 github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= 1335 1343 github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 1344 + github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I= 1336 1345 github.com/supranational/blst v0.3.15 h1:rd9viN6tfARE5wv3KZJ9H8e1cg0jXW8syFCcsbHa76o= 1337 1346 github.com/supranational/blst v0.3.15/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= 1338 1347 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= ··· 1520 1529 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 1521 1530 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 1522 1531 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 1523 - golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 1524 - golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 1532 + golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= 1533 + golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= 1525 1534 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 1526 1535 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 1527 1536 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= ··· 1573 1582 golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 1574 1583 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 1575 1584 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 1576 - golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= 1577 - golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= 1585 + golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= 1586 + golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 1578 1587 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 1579 1588 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 1580 1589 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= ··· 1627 1636 golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 1628 1637 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 1629 1638 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 1630 - golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= 1631 - golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 1639 + golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 1640 + golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 1632 1641 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 1633 1642 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 1634 1643 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= ··· 1658 1667 golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 1659 1668 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 1660 1669 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 1661 - golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 1662 - golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 1670 + golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 1671 + golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 1663 1672 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 1664 1673 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 1665 1674 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= ··· 1722 1731 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 1723 1732 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 1724 1733 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 1725 - golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 1726 - golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 1734 + golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 1735 + golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 1727 1736 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 1737 + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= 1738 + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= 1728 1739 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 1729 1740 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 1730 1741 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= ··· 1736 1747 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 1737 1748 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 1738 1749 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 1739 - golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= 1740 - golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= 1750 + golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 1751 + golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 1741 1752 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 1742 1753 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 1743 1754 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= ··· 1754 1765 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 1755 1766 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 1756 1767 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 1757 - golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 1758 - golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 1768 + golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 1769 + golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 1759 1770 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 1760 1771 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 1761 1772 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= ··· 1823 1834 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 1824 1835 golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 1825 1836 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 1826 - golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= 1827 - golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 1837 + golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= 1838 + golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 1828 1839 golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= 1829 1840 golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= 1830 1841 golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= ··· 1946 1957 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 1947 1958 gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 1948 1959 gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 1960 + gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= 1961 + gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= 1949 1962 gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 1950 1963 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 1951 1964 gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
+1
js/app/components/follow-button.tsx
··· 119 119 onPress={isFollowing ? handleUnfollow : handleFollow} 120 120 variant={isFollowing ? "secondary" : "primary"} 121 121 size="pill" 122 + width="min" 122 123 disabled={isFollowing === null} 123 124 loading={isFollowing === null} 124 125 leftIcon={!isFollowing && <Icon icon={Plus} size="sm" />}
+95 -120
js/app/components/mobile/desktop-ui.tsx
··· 18 18 useSharedValue, 19 19 withTiming, 20 20 } from "react-native-reanimated"; 21 + import { useSafeAreaInsets } from "react-native-safe-area-context"; 21 22 import { 22 23 BottomControlBar, 23 24 MuteOverlay, ··· 59 60 const { width, height } = usePlayerDimensions(); 60 61 const { shouldShowFloatingMetrics } = useResponsiveLayout(); 61 62 63 + const originalSafeAreaInsets = useSafeAreaInsets(); 64 + 62 65 const offline = useOffline(); 63 66 const showMetrics = usePlayerStore((state) => state.showDebugInfo); 64 67 const pipAction = usePlayerStore((state) => state.pipAction); 65 68 const videoRef = usePlayerStore((state) => state.videoRef); 66 69 const embedded = usePlayerStore((state) => state.embedded); 70 + 71 + const safeAreaInsets = embedded 72 + ? { ...originalSafeAreaInsets, top: 0 } 73 + : originalSafeAreaInsets; 67 74 68 75 const segment = useSegment(); 69 76 ··· 152 159 if (pipAction) pipAction(); 153 160 }, [pipAction]); 154 161 155 - // Live timer for offline overlay 156 - const [timeSinceLastSeen, setTimeSinceLastSeen] = useState("Unknown"); 157 - 158 - useEffect(() => { 159 - if (!offline || !segment?.startTime) { 160 - setTimeSinceLastSeen("Unknown"); 161 - return; 162 - } 163 - 164 - const updateTimer = () => { 165 - const now = new Date(); 166 - const lastSeen = new Date(segment.startTime); 167 - const diffMs = now.getTime() - lastSeen.getTime(); 168 - const diffMinutes = Math.floor(diffMs / 60000); 169 - const diffSeconds = Math.floor((diffMs % 60000) / 1000); 170 - 171 - if (diffMinutes > 0) { 172 - setTimeSinceLastSeen(`${diffMinutes}m ${diffSeconds}s ago`); 173 - } else { 174 - setTimeSinceLastSeen(`${diffSeconds}s ago`); 175 - } 176 - }; 177 - 178 - // Update immediately 179 - updateTimer(); 180 - 181 - // Update every second while offline 182 - const interval = setInterval(updateTimer, 1000); 183 - 184 - return () => clearInterval(interval); 185 - }, [offline, segment?.startTime]); 186 - 187 162 const hover = Gesture.Hover().onChange((_) => runOnJS(onPlayerHover)()); 188 163 189 164 return ( 190 165 <GestureDetector gesture={hover}> 191 - <> 192 - <View 193 - style={[layout.position.absolute, h.percent[100], w.percent[100]]} 166 + <View 167 + style={[layout.position.absolute, h.percent[100], w.percent[100]]} 168 + collapsable={false} 169 + > 170 + <MuteOverlay /> 171 + <PlayerUI.AutoplayButton /> 172 + <PlayerUI.ViewerLoadingOverlay /> 173 + <Animated.View 174 + style={[ 175 + layout.position.absolute, 176 + w.percent[100], 177 + { 178 + top: safeAreaInsets.top, 179 + paddingHorizontal: 16, 180 + paddingVertical: 16, 181 + }, 182 + animatedFadeStyle, 183 + ]} 194 184 > 195 - <MuteOverlay /> 196 - <PlayerUI.AutoplayButton /> 197 - <PlayerUI.ViewerLoadingOverlay /> 198 - <Animated.View 185 + <TopControlBar 186 + offline={offline} 187 + isActivelyLive={isActivelyLive} 188 + ingest={ingest} 189 + isChatOpen={isChatOpen || false} 190 + onToggleChat={toggleChat} 191 + embedded={embedded} 192 + /> 193 + </Animated.View> 194 + 195 + {isActivelyLive && isControlsVisible && ( 196 + <View 199 197 style={[ 200 198 layout.position.absolute, 201 - w.percent[100], 202 199 { 203 - paddingHorizontal: 16, 204 - paddingVertical: 16, 200 + transform: [{ translateX: -100 }, { translateY: -25 }], 205 201 }, 206 - animatedFadeStyle, 207 202 ]} 208 203 > 209 - <TopControlBar 210 - offline={offline} 211 - isActivelyLive={isActivelyLive} 212 - ingest={ingest} 213 - isChatOpen={isChatOpen || false} 214 - onToggleChat={toggleChat} 215 - embedded={embedded} 216 - /> 217 - </Animated.View> 218 - 219 - {isActivelyLive && isControlsVisible && ( 220 - <View 204 + <Animated.View 221 205 style={[ 222 - layout.position.absolute, 223 206 { 224 - transform: [{ translateX: -100 }, { translateY: -25 }], 207 + padding: 12, 208 + backgroundColor: "rgba(0, 0, 0, 0.5)", 225 209 }, 210 + r[3], 211 + animatedFadeStyle, 226 212 ]} 227 213 > 228 - <Animated.View 229 - style={[ 230 - { 231 - padding: 12, 232 - backgroundColor: "rgba(0, 0, 0, 0.5)", 233 - }, 234 - r[3], 235 - animatedFadeStyle, 236 - ]} 237 - > 238 - <PlayerUI.MetricsPanel showMetrics={isActivelyLive} /> 239 - </Animated.View> 240 - </View> 241 - )} 214 + <PlayerUI.MetricsPanel showMetrics={isActivelyLive} /> 215 + </Animated.View> 216 + </View> 217 + )} 242 218 243 - <Animated.View 244 - style={[ 245 - layout.position.absolute, 246 - position.bottom[0], 247 - w.percent[100], 248 - { 249 - backgroundColor: "rgba(0, 0, 0, 0.6)", 250 - paddingHorizontal: 16, 251 - paddingVertical: 2, 252 - paddingBottom: 2, 253 - }, 254 - animatedFadeStyle, 255 - ]} 256 - > 257 - <BottomControlBar 258 - ingest={ingest} 259 - pipSupported={pipSupported} 260 - pipActive={pipActive} 261 - onHandlePip={handlePip} 262 - dropdownPortalContainer={dropdownPortalContainer} 263 - showChat={isChatOpen || false} 264 - setShowChat={setIsChatOpen || (() => {})} 265 - /> 266 - </Animated.View> 219 + <Animated.View 220 + style={[ 221 + layout.position.absolute, 222 + position.bottom[0], 223 + w.percent[100], 224 + { 225 + backgroundColor: "rgba(0, 0, 0, 0.6)", 226 + paddingHorizontal: 16, 227 + paddingVertical: 2, 228 + paddingBottom: 2, 229 + }, 230 + animatedFadeStyle, 231 + ]} 232 + > 233 + <BottomControlBar 234 + ingest={ingest} 235 + pipSupported={pipSupported} 236 + pipActive={pipActive} 237 + onHandlePip={handlePip} 238 + dropdownPortalContainer={dropdownPortalContainer} 239 + showChat={isChatOpen || false} 240 + setShowChat={setIsChatOpen || undefined} 241 + /> 242 + </Animated.View> 267 243 268 - {isSelfAndNotLive && ( 269 - <PlayerUI.InputPanel 270 - title={title} 271 - setTitle={setTitle} 272 - ingestStarting={ingestStarting} 273 - toggleGoLive={toggleGoLive} 274 - /> 275 - )} 244 + {isSelfAndNotLive && ( 245 + <PlayerUI.InputPanel 246 + title={title} 247 + setTitle={setTitle} 248 + ingestStarting={ingestStarting} 249 + toggleGoLive={toggleGoLive} 250 + /> 251 + )} 276 252 277 - <PlayerUI.CountdownOverlay 278 - visible={showCountdown} 279 - width={width} 280 - height={height} 281 - onDone={() => { 282 - setShowCountdown(false); 283 - }} 284 - /> 253 + <PlayerUI.CountdownOverlay 254 + visible={showCountdown} 255 + width={width} 256 + height={height} 257 + onDone={() => { 258 + setShowCountdown(false); 259 + }} 260 + /> 285 261 286 - <Toast 287 - open={recordSubmitted} 288 - onOpenChange={setRecordSubmitted} 289 - title="You're live!" 290 - description="We're notifying your followers that you just went live." 291 - duration={5} 292 - /> 293 - </View> 262 + <Toast 263 + open={recordSubmitted} 264 + onOpenChange={setRecordSubmitted} 265 + title="You're live!" 266 + description="We're notifying your followers that you just went live." 267 + duration={5} 268 + /> 294 269 {showMetrics && ( 295 270 <View 296 271 style={[ ··· 310 285 <PlayerUI.MetricsPanel showMetrics={showMetrics} /> 311 286 </View> 312 287 )} 313 - </> 288 + </View> 314 289 </GestureDetector> 315 290 ); 316 291 }
+2 -2
js/app/components/mobile/desktop-ui/bottom-controls.tsx
··· 30 30 onHandlePip: () => void; 31 31 dropdownPortalContainer?: any; 32 32 showChat: boolean; 33 - setShowChat: (show: boolean) => void; 33 + setShowChat?: (show: boolean) => void; 34 34 } 35 35 36 36 export function BottomControlBar({ ··· 103 103 /> 104 104 )} 105 105 {/* if not web, then add the collapse chat controls here */} 106 - {Platform.OS !== "web" && ( 106 + {Platform.OS !== "web" && setShowChat && ( 107 107 <Button 108 108 variant="outline" 109 109 size="sm"
+3 -5
js/app/components/mobile/desktop-ui/live-bubble.tsx
··· 29 29 ]} 30 30 > 31 31 <Code 32 + size="xs" 32 33 style={[ 33 34 text.white, 34 35 { 35 - fontSize: 12, 36 - lineHeight: 8, 37 36 fontWeight: "600", 38 - letterSpacing: 1.5, 37 + letterSpacing: 2, 39 38 }, 40 39 ]} 41 40 > ··· 59 58 > 60 59 <View style={[h[2], w[2], bg.white, { borderRadius: 999 }]} /> 61 60 <Code 61 + size="xs" 62 62 style={[ 63 63 text.white, 64 64 { 65 - fontSize: 12, 66 - lineHeight: 8, 67 65 fontWeight: "600", 68 66 letterSpacing: 2, 69 67 },
+2 -2
js/app/components/mobile/desktop-ui/top-controls.tsx
··· 62 62 ]} 63 63 > 64 64 <View style={[layout.flex.row, layout.flex.alignCenter, gap.all[3]]}> 65 - {Platform.OS !== "web" && ( 65 + {Platform.OS !== "web" && !embedded && ( 66 66 <Pressable 67 67 onPress={() => { 68 68 navigation.canGoBack() ··· 97 97 ]} 98 98 /> 99 99 100 - <View style={[layout.flex.column, gap.all[2]]}> 100 + <View style={[layout.flex.column, gap.all[1]]}> 101 101 <Text 102 102 style={[text.white, { fontSize: 16, fontWeight: "600" }]} 103 103 >
+102 -12
js/app/components/mobile/player.tsx
··· 9 9 PlayerUI, 10 10 RotationProvider, 11 11 Text, 12 + useLivestreamStore, 12 13 usePlayerDimensions, 13 14 usePlayerStore, 15 + useSegment, 14 16 View, 15 17 } from "@streamplace/components"; 16 18 import { gap, h, pt, w } from "@streamplace/components/src/lib/theme/atoms"; ··· 18 20 import { useSidebarControl } from "hooks/useSidebarControl"; 19 21 import { ArrowLeft, ArrowRight } from "lucide-react-native"; 20 22 import { ComponentRef, useEffect, useRef, useState } from "react"; 21 - import { Animated, Platform, ScrollView, StatusBar } from "react-native"; 22 - import { SafeAreaView } from "react-native-safe-area-context"; 23 - import { useStore } from "store"; 23 + import { Platform, ScrollView, StatusBar } from "react-native"; 24 + import Reanimated, { 25 + useAnimatedStyle, 26 + useSharedValue, 27 + withTiming, 28 + } from "react-native-reanimated"; 24 29 import { useUserProfile } from "store/hooks"; 25 30 import { BottomMetadata } from "./bottom-metadata"; 26 31 import { DesktopChatPanel } from "./chat"; ··· 29 34 import { MobileUi } from "./ui"; 30 35 import { useResponsiveLayout } from "./useResponsiveLayout"; 31 36 37 + import { useSafeAreaInsets } from "react-native-safe-area-context"; 38 + import { useStore } from "store"; 39 + import { UserOffline } from "./user-offline"; 40 + 41 + const SEGMENT_TIMEOUT = 500; // half a sec 42 + 32 43 export function Player( 33 44 props: Partial<PlayerProps> & { 34 45 setFullscreen?: (fullscreen: boolean) => void; ··· 38 49 const { shouldShowChatSidePanel, chatPanelWidth } = useResponsiveLayout(); 39 50 const chatVisible = shouldShowChatSidePanel && showChat; 40 51 52 + const websocketConnected = useLivestreamStore((x) => x.websocketConnected); 53 + const hasReceivedSegment = useLivestreamStore((x) => x.hasReceivedSegment); 54 + const [showUnavailable, setShowUnavailable] = useState(false); 55 + const segs = useSegment(); 56 + 57 + // periodically check if segment has become stale 58 + const [now, setNow] = useState(Date.now()); 59 + useEffect(() => { 60 + const interval = setInterval(() => { 61 + setNow(Date.now()); 62 + }, 15000); // check every 15 seconds 63 + return () => clearInterval(interval); 64 + }, []); 65 + 66 + useEffect(() => { 67 + if (!websocketConnected) { 68 + setShowUnavailable(false); 69 + return; 70 + } 71 + 72 + const then = new Date(segs?.startTime || 0).getTime(); 73 + const segmentIsStale = segs?.startTime ? then < now - 300_000 : true; 74 + 75 + if (!segmentIsStale) { 76 + setShowUnavailable(false); 77 + return; 78 + } 79 + 80 + const timer = setTimeout(() => { 81 + setShowUnavailable(true); 82 + }, SEGMENT_TIMEOUT); 83 + return () => clearTimeout(timer); 84 + }, [websocketConnected, hasReceivedSegment, segs, now]); 85 + 41 86 const [isStreamingElsewhere, setIsStreamingElsewhere] = useState< 42 87 boolean | null 43 88 >(null); ··· 137 182 {...props} 138 183 showChat={showChat} 139 184 setShowChat={setShowChat} 185 + showUnavailable={showUnavailable} 140 186 /> 141 187 {shouldShowChatSidePanel ? ( 142 188 <DesktopChatPanel ··· 144 190 chatPanelWidth={chatPanelWidth} 145 191 /> 146 192 ) : ( 147 - <MobileUi /> 193 + !showUnavailable && <MobileUi /> 148 194 )} 149 195 </View> 150 196 </PlayerProvider> ··· 157 203 props: Partial<PlayerProps> & { 158 204 showChat: boolean; 159 205 setShowChat: (show: boolean) => void; 206 + showUnavailable: boolean; 160 207 }, 161 208 ) { 162 209 let sb = useSidebarControl(); ··· 174 221 showChatSidePanelOnLandscape: props.showChat, 175 222 }); 176 223 224 + const safeAreaInsets = useSafeAreaInsets(); 177 225 const setSidebarHidden = useStore((state) => state.setSidebarHidden); 178 226 const setSidebarUnhidden = useStore((state) => state.setSidebarUnhidden); 179 227 228 + // auto-collapse chat once when going offline 229 + const hasCollapsedChat = useRef(false); 230 + useEffect(() => { 231 + if ( 232 + props.showUnavailable && 233 + shouldShowChatSidePanel && 234 + !hasCollapsedChat.current 235 + ) { 236 + props.setShowChat(false); 237 + hasCollapsedChat.current = true; 238 + } 239 + if (!props.showUnavailable) { 240 + hasCollapsedChat.current = false; 241 + } 242 + }, [props.showUnavailable, shouldShowChatSidePanel]); 243 + 244 + // animated height for offline state 245 + const heightMultiplier = useSharedValue(1); 246 + 247 + useEffect(() => { 248 + if (props.showUnavailable) { 249 + heightMultiplier.value = withTiming(0.65, { duration: 500 }); 250 + } else { 251 + heightMultiplier.value = withTiming(1, { duration: 500 }); 252 + } 253 + }, [props.showUnavailable]); 254 + 180 255 // content info 181 256 const { width, height } = usePlayerDimensions(); 182 257 ··· 213 288 const showFullDesktopMode = aspectRatio > 1 && screenWidth > 1200; 214 289 const isLandscape = aspectRatio > 1; 215 290 291 + const isPlayerRatioGreater = aspectRatio >= 16 / 9; 292 + 293 + // animated style for offline height transition 294 + const animatedHeightStyle = useAnimatedStyle(() => { 295 + return { 296 + height: showFullDesktopMode 297 + ? calculatedHeight * heightMultiplier.value 298 + : undefined, 299 + }; 300 + }); 301 + 216 302 return ( 217 303 <ScrollView 218 304 style={{ ··· 234 320 bounces={false} 235 321 showsVerticalScrollIndicator={false} 236 322 > 237 - <Animated.View 323 + <Reanimated.View 238 324 style={[ 239 325 showFullDesktopMode 240 326 ? { 241 327 width: calculatedWidth, 242 - height: calculatedHeight, 243 328 } 244 329 : { 245 330 flex: 1, 246 331 maxHeight: "auto", 247 332 }, 248 333 { 249 - // paddingTop: 250 - // isPlayerRatioGreater && !isLandscape ? safeAreaInsets.top : 0, 334 + paddingTop: 335 + isPlayerRatioGreater && !isLandscape && !props.showUnavailable 336 + ? safeAreaInsets.top 337 + : 0, 251 338 }, 339 + animatedHeightStyle, 252 340 ]} 253 341 > 254 - <SafeAreaView edges={["left", "top"]} style={{ flex: 1 }}> 342 + {props.showUnavailable ? ( 343 + <UserOffline /> 344 + ) : ( 255 345 <PlayerInnerInner {...props}> 256 346 {showFullDesktopMode || fullscreen ? ( 257 347 <DesktopUi dropdownPortalContainer={dropdownPortalRef.current} /> ··· 264 354 ) 265 355 )} 266 356 <PlayerUI.ViewerLoadingOverlay /> 267 - <OfflineCounter isMobile={true} /> 357 + {!props.showUnavailable && <OfflineCounter isMobile={true} />} 268 358 <View 269 359 ref={dropdownPortalRef} 270 360 style={{ ··· 277 367 }} 278 368 /> 279 369 </PlayerInnerInner> 280 - </SafeAreaView> 281 - </Animated.View> 370 + )} 371 + </Reanimated.View> 282 372 {showFullDesktopMode && ( 283 373 <BottomMetadata 284 374 setShowChat={props.setShowChat}
+4
js/app/components/mobile/ui.tsx
··· 50 50 export function MobileUi({ 51 51 setShowChat, 52 52 showChat, 53 + hideMobileChat, 54 + embed = false, 53 55 }: { 54 56 setShowChat?: (show: boolean) => void; 55 57 showChat?: boolean; 58 + hideMobileChat?: boolean; 59 + embed?: boolean; 56 60 }) { 57 61 const { theme } = useTheme(); 58 62 const navigation = useNavigation();
+464
js/app/components/mobile/user-offline.tsx
··· 1 + import { useNavigation } from "@react-navigation/native"; 2 + import { 3 + LivestreamProvider, 4 + Player, 5 + PlayerProvider, 6 + Text, 7 + Trans, 8 + useAvatars, 9 + useLivestreamStore, 10 + useTranslation, 11 + View, 12 + zero, 13 + } from "@streamplace/components"; 14 + import { overflow } from "@streamplace/components/src/lib/theme/atoms"; 15 + import { ChevronLeft } from "lucide-react-native"; 16 + import { memo, useEffect, useMemo, useState } from "react"; 17 + import { Image, Platform, Pressable, useWindowDimensions } from "react-native"; 18 + import { useStore } from "../../store"; 19 + import FollowButton from "../follow-button"; 20 + import { DesktopUi } from "./desktop-ui"; 21 + 22 + const { bg, borders, flex, gap, h, layout, mt, position, px, py, r, text, w } = 23 + zero; 24 + 25 + interface SourceType { 26 + did: string; 27 + source: string; 28 + } 29 + 30 + export const UserOffline = memo(() => { 31 + console.log("rendering offline"); 32 + const { t } = useTranslation("common"); 33 + const navigation = useNavigation(); 34 + const profile = useLivestreamStore((x) => x.profile); 35 + const { width, height } = useWindowDimensions(); 36 + 37 + const { isSmallScreen, isLandscape, useCompactLayout } = useMemo(() => { 38 + const isSmall = width < 1250; 39 + const isLand = width > height; 40 + return { 41 + isSmallScreen: isSmall, 42 + isLandscape: isLand, 43 + useCompactLayout: isSmall && !isLand, 44 + }; 45 + }, [width, height]); 46 + 47 + const [recommendedSource, setRecommendedSource] = useState<SourceType | null>( 48 + null, 49 + ); 50 + const [isLoadingRecommendation, setIsLoadingRecommendation] = useState(false); 51 + const getRecommendations = useStore((state) => state.getRecommendations); 52 + 53 + const pfp = useAvatars(profile ? [profile?.did] : []); 54 + 55 + // use the detailed profile from useAvatars 56 + const detailedProfile = profile ? pfp[profile?.did] : null; 57 + 58 + useEffect(() => { 59 + if (!profile?.did) return; 60 + 61 + let mounted = true; 62 + 63 + const fetchRecommendation = async () => { 64 + setIsLoadingRecommendation(true); 65 + try { 66 + console.log("fetching recommendations for", profile.did); 67 + const result = await getRecommendations(profile.did); 68 + if (!mounted) return; 69 + if (result.recommendations && result.recommendations.length > 0) { 70 + // Get the first livestream recommendation 71 + const firstLivestream = result.recommendations.find( 72 + (rec) => 73 + rec.$type === 74 + "place.stream.live.getRecommendations#livestreamRecommendation", 75 + ); 76 + if (firstLivestream?.did) { 77 + setRecommendedSource({ 78 + did: firstLivestream.did, 79 + source: firstLivestream.source || "default", 80 + }); 81 + } 82 + } 83 + } catch (err) { 84 + console.error("failed to get recommendations", err); 85 + } finally { 86 + if (mounted) setIsLoadingRecommendation(false); 87 + } 88 + }; 89 + 90 + fetchRecommendation(); 91 + return () => { 92 + mounted = false; 93 + }; 94 + }, [profile?.did, getRecommendations]); 95 + 96 + if (!profile) { 97 + return ( 98 + <View style={[flex.values[1], bg.gray[900], layout.flex.center]}> 99 + <Text size="2xl" color="muted"> 100 + {t("user-offline")} 101 + </Text> 102 + </View> 103 + ); 104 + } 105 + 106 + if (!isLoadingRecommendation && !recommendedSource) { 107 + return ( 108 + <View 109 + style={[ 110 + flex.values[1], 111 + useCompactLayout ? layout.flex.alignCenter : layout.flex.center, 112 + useCompactLayout ? mt[12] : mt[4], 113 + ]} 114 + > 115 + {/* Back Button and Profile */} 116 + {Platform.OS !== "web" && ( 117 + <View 118 + style={[ 119 + { 120 + padding: 3, 121 + paddingRight: 8, 122 + backgroundColor: "rgba(90,90,90, 0.25)", 123 + borderRadius: 12, 124 + alignSelf: "flex-start", 125 + zIndex: 100, 126 + }, 127 + r.lg, 128 + layout.position.absolute, 129 + position.left[4], 130 + useCompactLayout ? position.top[4] : position.top[0], 131 + ]} 132 + > 133 + <View style={[layout.flex.row, layout.flex.center, gap.all[2]]}> 134 + <Pressable 135 + onPress={() => { 136 + if (navigation.canGoBack()) { 137 + navigation.goBack(); 138 + } else { 139 + navigation.reset({ 140 + index: 0, 141 + routes: [ 142 + { name: "Home", params: { screen: "StreamList" } }, 143 + ], 144 + }); 145 + } 146 + }} 147 + > 148 + <ChevronLeft color="white" /> 149 + </Pressable> 150 + <Image 151 + source={ 152 + profile?.did 153 + ? { uri: detailedProfile?.avatar } 154 + : require("assets/images/goose.png") 155 + } 156 + style={[ 157 + { 158 + width: 36, 159 + height: 36, 160 + backgroundColor: "green", 161 + }, 162 + { borderRadius: 999 }, 163 + borders.width.thin, 164 + borders.color.gray[700], 165 + ]} 166 + /> 167 + <Text>{profile?.handle}</Text> 168 + </View> 169 + </View> 170 + )} 171 + {/* Banner Background */} 172 + {detailedProfile?.banner && ( 173 + <Image 174 + blurRadius={10} 175 + source={{ uri: detailedProfile.banner }} 176 + style={[ 177 + { 178 + position: "absolute", 179 + top: -50, 180 + left: 0, 181 + right: 0, 182 + bottom: 0, 183 + width: "100%", 184 + height: "110%", 185 + opacity: 0.15, 186 + }, 187 + ]} 188 + /> 189 + )} 190 + 191 + <View 192 + style={[ 193 + useCompactLayout ? mt[20] : layout.flex.row, 194 + gap.all[isLandscape && isSmallScreen ? 3 : 6], 195 + layout.flex.center, 196 + px[4], 197 + ]} 198 + > 199 + <View 200 + style={[ 201 + isLandscape && isSmallScreen ? { width: 280 } : w.percent[100], 202 + useCompactLayout ? h.auto : h.percent[100], 203 + useCompactLayout 204 + ? { maxWidth: "100%" } 205 + : isLandscape && isSmallScreen 206 + ? { maxWidth: 300 } 207 + : { maxWidth: 450 }, 208 + isLandscape && isSmallScreen ? px[4] : px[8], 209 + isLandscape && isSmallScreen ? py[3] : py[6], 210 + bg.neutral[900], 211 + r.lg, 212 + borders.color.neutral[800], 213 + borders.width.thin, 214 + gap.row[isLandscape && isSmallScreen ? 1 : 2], 215 + layout.flex.justify.center, 216 + ]} 217 + > 218 + <Text size={isLandscape && isSmallScreen ? "base" : "xl"}> 219 + <Trans 220 + i18nKey="user-offline-no-recommendations" 221 + ns="common" 222 + values={{ handle: profile.handle }} 223 + components={{ 224 + 1: ( 225 + <Text 226 + size={isLandscape && isSmallScreen ? "base" : "xl"} 227 + style={[text.gray[400]]} 228 + /> 229 + ), 230 + br: <Text />, 231 + }} 232 + /> 233 + </Text> 234 + </View> 235 + </View> 236 + </View> 237 + ); 238 + } 239 + 240 + return ( 241 + <View 242 + style={[ 243 + flex.values[1], 244 + useCompactLayout ? layout.flex.alignCenter : layout.flex.center, 245 + useCompactLayout ? mt[12] : mt[4], 246 + ]} 247 + > 248 + {/* Back Button and Profile */} 249 + {Platform.OS !== "web" && ( 250 + <View 251 + style={[ 252 + { 253 + padding: 3, 254 + paddingRight: 8, 255 + backgroundColor: "rgba(90,90,90, 0.25)", 256 + borderRadius: 12, 257 + alignSelf: "flex-start", 258 + zIndex: 100, 259 + }, 260 + r.lg, 261 + layout.position.absolute, 262 + position.left[4], 263 + useCompactLayout ? position.top[4] : position.top[0], 264 + ]} 265 + > 266 + <View style={[layout.flex.row, layout.flex.center, gap.all[2]]}> 267 + <Pressable 268 + onPress={() => { 269 + if (navigation.canGoBack()) { 270 + navigation.goBack(); 271 + } else { 272 + navigation.reset({ 273 + index: 0, 274 + routes: [ 275 + { name: "Home", params: { screen: "StreamList" } }, 276 + ], 277 + }); 278 + } 279 + }} 280 + > 281 + <ChevronLeft color="white" /> 282 + </Pressable> 283 + <Image 284 + source={ 285 + profile?.did 286 + ? { uri: detailedProfile?.avatar } 287 + : require("assets/images/goose.png") 288 + } 289 + style={[ 290 + { 291 + width: 36, 292 + height: 36, 293 + backgroundColor: "green", 294 + }, 295 + { borderRadius: 999 }, 296 + borders.width.thin, 297 + borders.color.gray[700], 298 + ]} 299 + /> 300 + <Text>{profile?.handle}</Text> 301 + </View> 302 + </View> 303 + )} 304 + {/* Banner Background */} 305 + {detailedProfile?.banner && ( 306 + <Image 307 + blurRadius={10} 308 + source={{ uri: detailedProfile.banner }} 309 + style={[ 310 + { 311 + position: "absolute", 312 + top: -50, 313 + left: 0, 314 + right: 0, 315 + bottom: 0, 316 + width: "100%", 317 + height: "110%", 318 + opacity: 0.15, 319 + }, 320 + ]} 321 + /> 322 + )} 323 + 324 + {recommendedSource && ( 325 + <LivestreamProvider src={recommendedSource.did} ignoreOuterContext> 326 + <View 327 + style={[ 328 + useCompactLayout ? mt[20] : layout.flex.row, 329 + gap.all[isLandscape && isSmallScreen ? 3 : 6], 330 + layout.flex.center, 331 + px[4], 332 + ]} 333 + > 334 + <View 335 + style={[ 336 + isLandscape && isSmallScreen ? { width: 280 } : w.percent[100], 337 + useCompactLayout ? h.auto : h.percent[100], 338 + useCompactLayout 339 + ? { maxWidth: "100%" } 340 + : isLandscape && isSmallScreen 341 + ? { maxWidth: 280 } 342 + : { maxWidth: 400 }, 343 + isLandscape && isSmallScreen ? px[4] : px[8], 344 + isLandscape && isSmallScreen ? py[3] : py[6], 345 + bg.neutral[900], 346 + r.lg, 347 + borders.color.neutral[800], 348 + borders.width.thin, 349 + gap.row[isLandscape && isSmallScreen ? 1 : 2], 350 + layout.flex.justify.center, 351 + ]} 352 + > 353 + <Text size={isLandscape && isSmallScreen ? "base" : "xl"}> 354 + <Trans 355 + i18nKey="user-offline-message" 356 + ns="common" 357 + values={{ 358 + handle: profile.handle, 359 + source: recommendedSource.source, 360 + }} 361 + components={{ 362 + 1: ( 363 + <Text 364 + size={isLandscape && isSmallScreen ? "base" : "xl"} 365 + style={[text.gray[400]]} 366 + /> 367 + ), 368 + }} 369 + /> 370 + </Text> 371 + <View style={[gap.all[1]]}> 372 + {isLoadingRecommendation ? ( 373 + <Text style={[text.gray[300]]}>{t("loading")}</Text> 374 + ) : ( 375 + <RecommendedSourceInfo /> 376 + )} 377 + </View> 378 + </View> 379 + <View 380 + style={[ 381 + useCompactLayout 382 + ? [w.percent[100], { maxWidth: "100%", aspectRatio: 16 / 9 }] 383 + : [ 384 + flex.values[1], 385 + { 386 + aspectRatio: 16 / 9, 387 + ...(!(isLandscape && isSmallScreen) && { 388 + maxWidth: 650, 389 + minWidth: 650, 390 + }), 391 + }, 392 + ], 393 + overflow.hidden, 394 + r.lg, 395 + overflow.hidden, 396 + borders.color.neutral[800], 397 + borders.width.thin, 398 + bg.black, 399 + gap.row[2], 400 + ]} 401 + > 402 + {!isLoadingRecommendation && ( 403 + <PlayerProvider> 404 + <Player src={recommendedSource.did} embedded={true}> 405 + <DesktopUi setIsChatOpen={undefined} /> 406 + </Player> 407 + </PlayerProvider> 408 + )} 409 + </View> 410 + </View> 411 + </LivestreamProvider> 412 + )} 413 + </View> 414 + ); 415 + }); 416 + 417 + const RecommendedSourceInfo = memo(() => { 418 + const { t } = useTranslation("common"); 419 + const profile = useLivestreamStore((x) => x.profile); 420 + const viewers = useLivestreamStore((x) => x.viewers); 421 + const lsInfo = useLivestreamStore((x) => x.livestream); 422 + const currentUserDID = useStore((state) => state.oauthSession?.did); 423 + 424 + const pfp = useAvatars(profile?.did ? [profile.did] : []); 425 + const detailedProfile = profile?.did ? pfp[profile.did] : null; 426 + 427 + return ( 428 + <View 429 + style={[ 430 + layout.flex.column, 431 + layout.flex.justifyCenter, 432 + gap.all[4], 433 + w.percent[100], 434 + ]} 435 + > 436 + <View style={[layout.flex.column, gap.all[4]]}> 437 + <View style={[layout.flex.row, gap.all[4], layout.flex.alignCenter]}> 438 + <Image 439 + source={{ uri: detailedProfile?.avatar || profile?.avatar }} 440 + style={[ 441 + { width: 48, height: 48, borderRadius: 999 }, 442 + borders.width.thin, 443 + borders.color.gray[700], 444 + ]} 445 + /> 446 + <View style={[flex.values[1]]}> 447 + <Text weight="bold"> 448 + @{detailedProfile?.handle || profile?.handle} 449 + </Text> 450 + <Text style={[text.gray[300]]} size="base"> 451 + {t("viewer-count", { count: viewers || 0 })} 452 + </Text> 453 + </View> 454 + </View> 455 + </View> 456 + {profile?.did && ( 457 + <FollowButton 458 + streamerDID={profile.did} 459 + currentUserDID={currentUserDID} 460 + /> 461 + )} 462 + </View> 463 + ); 464 + });
+696
js/app/components/settings/recommendations-manager.tsx
··· 1 + import { 2 + Button, 3 + Input, 4 + MenuContainer, 5 + MenuGroup, 6 + MenuInfo, 7 + MenuItem, 8 + MenuSeparator, 9 + ResponsiveDialog, 10 + Text, 11 + useToast, 12 + zero, 13 + } from "@streamplace/components"; 14 + import { usePDSAgent } from "@streamplace/components/src/streamplace-store/xrpc"; 15 + import Loading from "components/loading/loading"; 16 + import { 17 + Check, 18 + GripVertical, 19 + Pencil, 20 + Plus, 21 + RefreshCw, 22 + Search, 23 + X, 24 + } from "lucide-react-native"; 25 + import { useCallback, useEffect, useState } from "react"; 26 + import { useTranslation } from "react-i18next"; 27 + import { Pressable, ScrollView, View } from "react-native"; 28 + import Sortable from "react-native-sortables"; 29 + import { SettingsRowItem } from "./components/settings-navigation-item"; 30 + 31 + const { text, mt, mb, px, py, w, layout, gap, r, p } = zero; 32 + 33 + interface ActorSearchResult { 34 + did: string; 35 + handle: string; 36 + } 37 + 38 + export default function RecommendationsManager() { 39 + const agent = usePDSAgent(); 40 + const { theme } = zero.useTheme(); 41 + const [streamers, setStreamers] = useState<string[]>([]); 42 + const [loading, setLoading] = useState(true); 43 + const [saving, setSaving] = useState(false); 44 + const [deleteDialog, setDeleteDialog] = useState<{ 45 + isVisible: boolean; 46 + index: number | null; 47 + }>({ isVisible: false, index: null }); 48 + const [errors, setErrors] = useState<Record<number, string>>({}); 49 + const [editingIndex, setEditingIndex] = useState<number | null>(null); 50 + const [editValue, setEditValue] = useState(""); 51 + 52 + const a = useToast(); 53 + 54 + const [activeDrag, setActiveDrag] = useState(""); 55 + 56 + // Search state 57 + const [searchQuery, setSearchQuery] = useState(""); 58 + const [searchResults, setSearchResults] = useState<ActorSearchResult[]>([]); 59 + const [searching, setSearching] = useState(false); 60 + const [searchDebounceTimeout, setSearchDebounceTimeout] = 61 + useState<NodeJS.Timeout | null>(null); 62 + 63 + const { t } = useTranslation("settings"); 64 + 65 + const loadRecommendations = async () => { 66 + if (!agent) return; 67 + 68 + try { 69 + setLoading(true); 70 + const userDID = agent.did; 71 + if (!userDID) { 72 + setStreamers([]); 73 + return; 74 + } 75 + 76 + // Get the record directly from the PDS for editing 77 + const response = await agent.com.atproto.repo.getRecord({ 78 + repo: userDID, 79 + collection: "place.stream.live.recommendations", 80 + rkey: "self", 81 + }); 82 + 83 + // todo: type this right 84 + let record = response.data.value as any; 85 + 86 + if (!response.success) { 87 + // Create a new empty record if not found 88 + const res = await agent.com.atproto.repo.createRecord({ 89 + repo: userDID, 90 + collection: "place.stream.live.recommendations", 91 + record: { 92 + streamers: [], 93 + createdAt: new Date().toISOString(), 94 + }, 95 + }); 96 + if (!res.success) { 97 + throw new Error("Failed to create recommendations record"); 98 + } 99 + record = res.data; 100 + } 101 + setStreamers(record.streamers || []); 102 + } catch (error: any) { 103 + console.error("Failed to load recommendations:", error); 104 + if (error.status !== 404) { 105 + a.show("Error", "Failed to load recommendations. Please try again."); 106 + } 107 + setStreamers([]); 108 + } finally { 109 + setLoading(false); 110 + } 111 + }; 112 + 113 + const saveRecommendations = async (newStreamers: string[]) => { 114 + if (!agent || saving) return; 115 + 116 + try { 117 + if (!agent.did) { 118 + throw new Error("User DID not found"); 119 + } 120 + setSaving(true); 121 + 122 + // Use putRecord to create or update the record 123 + await agent.com.atproto.repo.putRecord({ 124 + repo: agent.did, 125 + collection: "place.stream.live.recommendations", 126 + rkey: "self", 127 + record: { 128 + streamers: newStreamers, 129 + createdAt: new Date().toISOString(), 130 + }, 131 + }); 132 + 133 + setStreamers(newStreamers); 134 + } catch (error: any) { 135 + console.error("Failed to save recommendations:", error); 136 + a.show( 137 + "Error", 138 + error.message || "Failed to save recommendations. Please try again.", 139 + ); 140 + // Reload to get back to consistent state 141 + await loadRecommendations(); 142 + } finally { 143 + setSaving(false); 144 + } 145 + }; 146 + 147 + const searchActors = useCallback( 148 + async (query: string) => { 149 + if (!agent || !query.trim()) { 150 + setSearchResults([]); 151 + return; 152 + } 153 + 154 + try { 155 + setSearching(true); 156 + const response = await agent.place.stream.live.searchActorsTypeahead({ 157 + q: query, 158 + limit: 10, 159 + }); 160 + 161 + setSearchResults( 162 + response.data.actors.map((actor: any) => ({ 163 + did: actor.did, 164 + handle: actor.handle, 165 + })), 166 + ); 167 + } catch (error: any) { 168 + console.error("Failed to search actors:", error); 169 + setSearchResults([]); 170 + } finally { 171 + setSearching(false); 172 + } 173 + }, 174 + [agent], 175 + ); 176 + 177 + const handleSearchChange = (query: string) => { 178 + setSearchQuery(query); 179 + 180 + // Clear previous timeout 181 + if (searchDebounceTimeout) { 182 + clearTimeout(searchDebounceTimeout); 183 + } 184 + 185 + // Set new timeout for debounced search 186 + if (query.trim()) { 187 + const timeout = setTimeout(() => { 188 + searchActors(query); 189 + }, 300); 190 + setSearchDebounceTimeout(timeout); 191 + } else { 192 + setSearchResults([]); 193 + } 194 + }; 195 + 196 + const handleSelectActor = async (actor: ActorSearchResult) => { 197 + if (streamers.length >= 8) { 198 + a.show("Maximum Reached", "You can only add up to 8 recommendations."); 199 + return; 200 + } 201 + 202 + if (streamers.includes(actor.did)) { 203 + a.show( 204 + "Already Added", 205 + "This streamer is already in your recommendations.", 206 + ); 207 + return; 208 + } 209 + 210 + const newStreamers = [...streamers, actor.did]; 211 + await saveRecommendations(newStreamers); 212 + 213 + // Clear search 214 + setSearchQuery(""); 215 + setSearchResults([]); 216 + }; 217 + 218 + const validateDID = (did: string, index: number): boolean => { 219 + const trimmed = did.trim(); 220 + if (!trimmed) { 221 + setErrors((prev) => ({ ...prev, [index]: "DID is required" })); 222 + return false; 223 + } 224 + if (!trimmed.startsWith("did:")) { 225 + setErrors((prev) => ({ 226 + ...prev, 227 + [index]: "DID must start with 'did:'", 228 + })); 229 + return false; 230 + } 231 + setErrors((prev) => { 232 + const newErrors = { ...prev }; 233 + delete newErrors[index]; 234 + return newErrors; 235 + }); 236 + return true; 237 + }; 238 + 239 + const handleEdit = (index: number) => { 240 + setEditingIndex(index); 241 + setEditValue(streamers[index]); 242 + setErrors({}); 243 + }; 244 + 245 + const handleCancelEdit = () => { 246 + setEditingIndex(null); 247 + setEditValue(""); 248 + setErrors({}); 249 + }; 250 + 251 + const handleSaveEdit = async () => { 252 + if (editingIndex === null) return; 253 + 254 + const trimmed = editValue.trim(); 255 + if (!trimmed) { 256 + setErrors({ [editingIndex]: "DID is required" }); 257 + return; 258 + } 259 + if (!trimmed.startsWith("did:")) { 260 + setErrors({ [editingIndex]: "DID must start with 'did:'" }); 261 + return; 262 + } 263 + 264 + const newStreamers = [...streamers]; 265 + newStreamers[editingIndex] = trimmed; 266 + await saveRecommendations(newStreamers); 267 + setEditingIndex(null); 268 + setEditValue(""); 269 + setErrors({}); 270 + }; 271 + 272 + const handleAddRecommendation = () => { 273 + if (streamers.length >= 8) { 274 + a.show("Maximum Reached", "You can only add up to 8 recommendations."); 275 + return; 276 + } 277 + const newIndex = streamers.length; 278 + setStreamers([...streamers, ""]); 279 + setEditingIndex(newIndex); 280 + setEditValue(""); 281 + }; 282 + 283 + const handleDelete = (index: number) => { 284 + setDeleteDialog({ isVisible: true, index }); 285 + }; 286 + 287 + const confirmDelete = async () => { 288 + if (deleteDialog.index === null) return; 289 + 290 + const newStreamers = streamers.filter((_, i) => i !== deleteDialog.index); 291 + await saveRecommendations(newStreamers); 292 + setDeleteDialog({ isVisible: false, index: null }); 293 + }; 294 + 295 + useEffect(() => { 296 + if (!agent) return; 297 + loadRecommendations(); 298 + }, [agent]); 299 + 300 + // Cleanup timeout on unmount 301 + useEffect(() => { 302 + return () => { 303 + if (searchDebounceTimeout) { 304 + clearTimeout(searchDebounceTimeout); 305 + } 306 + }; 307 + }, [searchDebounceTimeout]); 308 + 309 + if (!agent) { 310 + return <Loading />; 311 + } 312 + 313 + return ( 314 + <> 315 + <ScrollView> 316 + <View style={[zero.layout.flex.align.center, zero.px[2], zero.py[2]]}> 317 + <View style={{ maxWidth: 800, width: "100%" }}> 318 + <MenuContainer> 319 + <View style={[mb[2]]}> 320 + <View 321 + style={[ 322 + layout.flex.row, 323 + layout.flex.justify.between, 324 + layout.flex.alignCenter, 325 + ]} 326 + > 327 + <Text size="xl">{t("recommendations-to-others")}</Text> 328 + <Button 329 + onPress={loadRecommendations} 330 + disabled={loading || saving} 331 + leftIcon={<RefreshCw size={16} color={theme.colors.text} />} 332 + size="pill" 333 + width="min" 334 + variant="secondary" 335 + > 336 + <Text size="sm">{t("refresh")}</Text> 337 + </Button> 338 + </View> 339 + </View> 340 + 341 + <MenuInfo description={t("recommendations-description")} /> 342 + </MenuContainer> 343 + 344 + {/* Search Bar */} 345 + {streamers.length < 8 && ( 346 + <MenuContainer> 347 + <MenuGroup> 348 + <View style={[px[3], py[2]]}> 349 + <View 350 + style={[ 351 + layout.flex.row, 352 + layout.flex.alignCenter, 353 + gap.all[2], 354 + ]} 355 + > 356 + <Search size={20} color={theme.colors.textMuted} /> 357 + <Input 358 + value={searchQuery} 359 + onChangeText={handleSearchChange} 360 + placeholder="Search for streamers..." 361 + /> 362 + </View> 363 + </View> 364 + 365 + {searching && ( 366 + <> 367 + <MenuSeparator /> 368 + <View style={[py[2], layout.flex.center]}> 369 + <Text 370 + size="sm" 371 + style={{ color: theme.colors.textMuted }} 372 + > 373 + Searching... 374 + </Text> 375 + </View> 376 + </> 377 + )} 378 + 379 + {!searching && searchResults.length > 0 && ( 380 + <> 381 + <MenuSeparator /> 382 + {searchResults.map((actor, index) => { 383 + const alreadyAdded = streamers.includes(actor.did); 384 + return ( 385 + <View key={actor.did}> 386 + {index > 0 && <MenuSeparator />} 387 + <Pressable 388 + onPress={() => 389 + !alreadyAdded && handleSelectActor(actor) 390 + } 391 + disabled={alreadyAdded} 392 + > 393 + {({ pressed }) => ( 394 + <View 395 + style={[ 396 + px[3], 397 + py[2], 398 + layout.flex.row, 399 + layout.flex.alignCenter, 400 + gap.all[2], 401 + r.md, 402 + { 403 + backgroundColor: 404 + pressed && !alreadyAdded 405 + ? "#ffffff08" 406 + : "transparent", 407 + opacity: alreadyAdded ? 0.5 : 1, 408 + }, 409 + ]} 410 + > 411 + <View style={{ flex: 1 }}> 412 + <Text>@{actor.handle}</Text> 413 + </View> 414 + {alreadyAdded && ( 415 + <Text 416 + size="xs" 417 + style={{ color: theme.colors.textMuted }} 418 + > 419 + Added 420 + </Text> 421 + )} 422 + </View> 423 + )} 424 + </Pressable> 425 + </View> 426 + ); 427 + })} 428 + </> 429 + )} 430 + 431 + {!searching && 432 + searchQuery.trim() && 433 + searchResults.length === 0 && ( 434 + <> 435 + <MenuSeparator /> 436 + <View style={[py[2], layout.flex.center]}> 437 + <Text 438 + size="sm" 439 + style={{ color: theme.colors.textMuted }} 440 + > 441 + No results found 442 + </Text> 443 + </View> 444 + </> 445 + )} 446 + </MenuGroup> 447 + 448 + {searchQuery.trim() === "" && ( 449 + <MenuInfo description="Search for streamers by handle or name, or enter DIDs manually below" /> 450 + )} 451 + </MenuContainer> 452 + )} 453 + 454 + {loading ? ( 455 + <Loading /> 456 + ) : ( 457 + <MenuContainer> 458 + <MenuGroup> 459 + {streamers.length === 0 ? ( 460 + <View style={[py[4], layout.flex.center]}> 461 + <Text size="sm" style={{ color: theme.colors.textMuted }}> 462 + {t("no-recommendations-yet")} 463 + </Text> 464 + </View> 465 + ) : ( 466 + <Sortable.Grid 467 + columns={1} 468 + activeItemOpacity={90} 469 + activeItemScale={1} 470 + onActiveItemDropped={() => { 471 + saveRecommendations(streamers); 472 + }} 473 + data={streamers} 474 + keyExtractor={(item: string) => `item-${item}`} 475 + overDrag="vertical" 476 + onDragStart={(e) => { 477 + console.log("dragging", e.key); 478 + setActiveDrag(e.key); 479 + }} 480 + onDragEnd={() => setActiveDrag("")} 481 + renderItem={(params: any) => { 482 + const streamer: string = params.item; 483 + const index: number = params.index ?? 0; 484 + const beforeSeparator = 485 + index > 0 && "item-" + params.item !== activeDrag ? ( 486 + <MenuSeparator key={`sep-${index}`} /> 487 + ) : null; 488 + 489 + return ( 490 + <> 491 + {beforeSeparator} 492 + <MenuItem key={`item-${index}`}> 493 + <GripVertical 494 + color={theme.colors.mutedForeground + "a0"} 495 + size={18} 496 + style={{ 497 + marginLeft: -4, 498 + marginRight: 4, 499 + }} 500 + /> 501 + {editingIndex === index ? ( 502 + <> 503 + <View style={{ flex: 1 }}> 504 + <Input 505 + value={editValue} 506 + onChangeText={setEditValue} 507 + placeholder="did:plc:..." 508 + autoFocus 509 + /> 510 + {errors[index] && ( 511 + <Text 512 + size="xs" 513 + style={{ 514 + color: theme.colors.destructive, 515 + marginTop: 4, 516 + }} 517 + > 518 + {errors[index]} 519 + </Text> 520 + )} 521 + </View> 522 + 523 + <Pressable 524 + onPress={handleSaveEdit} 525 + style={({ pressed }) => [ 526 + { 527 + padding: 8, 528 + borderRadius: 6, 529 + backgroundColor: pressed 530 + ? "#ffffff08" 531 + : "transparent", 532 + }, 533 + ]} 534 + > 535 + <Check 536 + size={18} 537 + color={theme.colors.text} 538 + /> 539 + </Pressable> 540 + 541 + <Pressable 542 + onPress={handleCancelEdit} 543 + style={({ pressed }) => [ 544 + { 545 + padding: 8, 546 + borderRadius: 6, 547 + backgroundColor: pressed 548 + ? "#ffffff08" 549 + : "transparent", 550 + }, 551 + ]} 552 + > 553 + <X 554 + size={18} 555 + color={theme.colors.textMuted} 556 + /> 557 + </Pressable> 558 + </> 559 + ) : ( 560 + <> 561 + <View style={{ flex: 1 }}> 562 + <Text 563 + numberOfLines={1} 564 + ellipsizeMode="middle" 565 + > 566 + {streamer || "(empty)"} 567 + </Text> 568 + </View> 569 + 570 + <Pressable 571 + onPress={() => handleEdit(index)} 572 + style={({ pressed }) => [ 573 + { 574 + padding: 8, 575 + borderRadius: 6, 576 + backgroundColor: pressed 577 + ? "#ffffff08" 578 + : "transparent", 579 + }, 580 + ]} 581 + > 582 + <Pencil 583 + size={18} 584 + color={theme.colors.textMuted} 585 + /> 586 + </Pressable> 587 + 588 + <Pressable 589 + onPress={() => handleDelete(index)} 590 + style={({ pressed }) => [ 591 + { 592 + padding: 8, 593 + borderRadius: 6, 594 + backgroundColor: pressed 595 + ? "#ffffff08" 596 + : "transparent", 597 + }, 598 + ]} 599 + > 600 + <X 601 + size={18} 602 + color={theme.colors.destructive} 603 + /> 604 + </Pressable> 605 + </> 606 + )} 607 + </MenuItem> 608 + </> 609 + ); 610 + }} 611 + onOrderChange={(params) => { 612 + console.log(params); 613 + // calculate new order from params 614 + // duplicate streamers array 615 + const newData = [...streamers]; 616 + const movedItem = newData.splice( 617 + params.fromIndex, 618 + 1, 619 + )[0]; 620 + newData.splice(params.toIndex, 0, movedItem); 621 + setStreamers(newData); 622 + }} 623 + rowGap={0} 624 + columnGap={0} 625 + /> 626 + )} 627 + 628 + {streamers.length > 0 && streamers.length < 8 && ( 629 + <MenuSeparator /> 630 + )} 631 + 632 + {streamers.length < 8 && ( 633 + <SettingsRowItem onPress={handleAddRecommendation}> 634 + <View 635 + style={[ 636 + layout.flex.row, 637 + layout.flex.alignCenter, 638 + gap.all[2], 639 + ]} 640 + > 641 + <Plus color={theme.colors.text} /> 642 + <Text>Add DID manually</Text> 643 + </View> 644 + </SettingsRowItem> 645 + )} 646 + 647 + {saving && ( 648 + <View style={[mt[2], layout.flex.center]}> 649 + <Text size="sm" style={{ color: theme.colors.textMuted }}> 650 + {t("saving")} 651 + </Text> 652 + </View> 653 + )} 654 + </MenuGroup> 655 + </MenuContainer> 656 + )} 657 + </View> 658 + </View> 659 + </ScrollView> 660 + 661 + <ResponsiveDialog 662 + open={deleteDialog.isVisible} 663 + onOpenChange={(open) => 664 + !open && setDeleteDialog({ isVisible: false, index: null }) 665 + } 666 + title={t("delete")} 667 + dismissible={true} 668 + size="sm" 669 + > 670 + <View style={[w.percent[100], gap.all[2], mb[4]]}> 671 + <Text size="2xl">{t("confirm-delete")}</Text> 672 + <Text>{t("action-cannot-be-undone")}</Text> 673 + </View> 674 + 675 + <View style={[layout.flex.row, layout.flex.justify.end, gap.all[3]]}> 676 + <Button 677 + variant="secondary" 678 + width="min" 679 + onPress={() => setDeleteDialog({ isVisible: false, index: null })} 680 + disabled={saving} 681 + > 682 + <Text>{t("cancel")}</Text> 683 + </Button> 684 + <Button 685 + variant="destructive" 686 + width="min" 687 + onPress={confirmDelete} 688 + disabled={saving} 689 + > 690 + <Text>{saving ? t("deleting") : t("delete")}</Text> 691 + </Button> 692 + </View> 693 + </ResponsiveDialog> 694 + </> 695 + ); 696 + }
+7 -1
js/app/components/settings/streaming-category-settings.tsx
··· 5 5 View, 6 6 zero, 7 7 } from "@streamplace/components"; 8 - import { Key, Webhook } from "lucide-react-native"; 8 + import { Heart, Key, Webhook } from "lucide-react-native"; 9 9 import { useTranslation } from "react-i18next"; 10 10 import { ScrollView } from "react-native"; 11 11 import { SettingsNavigationItem } from "./components/settings-navigation-item"; ··· 22 22 title={t("key-management")} 23 23 screen="KeyManagement" 24 24 icon={Key} 25 + /> 26 + <MenuSeparator /> 27 + <SettingsNavigationItem 28 + title={t("recommendations-to-others")} 29 + screen="RecommendationsSettings" 30 + icon={Heart} 25 31 /> 26 32 <MenuSeparator /> 27 33 <SettingsNavigationItem
+25 -3
js/app/components/sidebar/sidebar.tsx
··· 7 7 useNavigation, 8 8 } from "@react-navigation/native"; 9 9 import { Text, zero } from "@streamplace/components"; 10 + import { useAQLinkHref } from "components/aqlink"; 10 11 import React from "react"; 11 - import { Image, Platform, View } from "react-native"; 12 + import { Image, Platform, Pressable, View } from "react-native"; 12 13 import Animated, { 13 14 SharedValue, 14 15 useAnimatedStyle, ··· 52 53 }; 53 54 }); 54 55 56 + // home route 57 + const route = state.routes.find((r) => r.name === "Home"); 58 + const { href } = useAQLinkHref({ 59 + screen: route ? route.name : "Home", 60 + params: route?.params as any, 61 + }); 62 + 55 63 if (hidden) { 56 64 return <View />; 57 65 } ··· 65 73 zero.layout.flex.column, 66 74 ]} 67 75 > 68 - <View 76 + <Pressable 77 + // @ts-ignore This makes it render as <a> on web! 78 + href={route ? href : undefined} 69 79 style={[ 70 80 zero.layout.flex.row, 71 81 zero.layout.flex.alignCenter, ··· 84 94 style={{ width: 28, height: 30, resizeMode: "contain" }} 85 95 /> 86 96 {!collapsed && <Text size="2xl">Streamplace</Text>} 87 - </View> 97 + </Pressable> 88 98 89 99 {state.routes.map((route) => { 90 100 const descriptor = descriptors[route.key]; ··· 124 134 route={route} 125 135 onPress={(ev) => { 126 136 ev.preventDefault(); 137 + // bleh 127 138 if (route.name === "Home") { 128 139 // reset the stack (b/c streamlist is in the same stack as home) 129 140 navigation.dispatch( ··· 135 146 state: { 136 147 routes: [{ name: "StreamList" }], 137 148 }, 149 + }, 150 + ], 151 + }), 152 + ); 153 + } else if (route.name === "Settings") { 154 + navigation.dispatch( 155 + CommonActions.reset({ 156 + index: 0, 157 + routes: [ 158 + { 159 + name: "Settings", 138 160 }, 139 161 ], 140 162 }),
+3 -1
js/app/package.json
··· 1 1 { 2 2 "name": "@streamplace/app", 3 3 "main": "./src/entrypoint.tsx", 4 - "version": "0.8.15", 4 + "version": "0.8.18", 5 5 "runtimeVersion": "0.7.2", 6 6 "scripts": { 7 7 "start": "npx expo start -c --port 38081", ··· 92 92 "react-dom": "19.0.0", 93 93 "react-i18next": "^15.7.3", 94 94 "react-native": "0.79.3", 95 + "react-native-draggable-flatlist": "^4.0.3", 95 96 "react-native-edge-to-edge": "^1.6.2", 96 97 "react-native-gesture-handler": "~2.26.0", 97 98 "react-native-localize": "^3.5.2", ··· 100 101 "react-native-reanimated": "~3.18.0", 101 102 "react-native-safe-area-context": "5.4.1", 102 103 "react-native-screens": "~4.11.1", 104 + "react-native-sortables": "^1.9.4", 103 105 "react-native-svg": "15.12.0", 104 106 "react-native-web": "^0.20.0", 105 107 "react-native-webrtc": "git+https://github.com/streamplace/react-native-webrtc.git#6b8472a771ac47f89217d327058a8a4124a6ae56",
+12 -3
js/app/scripts/generate-build-info.js
··· 3 3 const { execSync } = require("child_process"); 4 4 const fs = require("fs"); 5 5 const path = require("path"); 6 + const pkg = require("../package.json"); 6 7 7 8 function getGitInfo() { 8 9 try { ··· 13 14 const branch = execSync("git rev-parse --abbrev-ref HEAD", { 14 15 encoding: "utf-8", 15 16 }).trim(); 16 - const tag = execSync("git describe --tags --always --dirty", { 17 - encoding: "utf-8", 18 - }).trim(); 17 + 18 + let tag; 19 + try { 20 + tag = execSync("git describe --tags --always --dirty", { 21 + encoding: "utf-8", 22 + }).trim(); 23 + } catch (error) { 24 + // git describe fails in shallow clones, use package.json version + short hash 25 + tag = `v${pkg.version}-${shortHash}`; 26 + } 27 + 19 28 const isDirty = tag.endsWith("-dirty"); 20 29 21 30 return {
+24 -1
js/app/src/router.tsx
··· 73 73 74 74 import { useUrl } from "@streamplace/components"; 75 75 import { LanguagesCategorySettings } from "components/settings/languages-category-settings"; 76 + import RecommendationsManager from "components/settings/recommendations-manager"; 76 77 import Constants from "expo-constants"; 77 78 import { useBlueskyNotifications } from "hooks/useBlueskyNotifications"; 78 79 import { SystemBars } from "react-native-edge-to-edge"; ··· 115 116 AccountCategory: undefined; 116 117 StreamingCategory: undefined; 117 118 WebhooksSettings: undefined; 119 + RecommendationsSettings: undefined; 118 120 PrivacyCategory: undefined; 119 121 DanmuCategory: undefined; 120 122 AdvancedCategory: undefined; ··· 171 173 AccountCategory: "settings/account", 172 174 StreamingCategory: "settings/streaming", 173 175 WebhooksSettings: "settings/streaming/webhooks", 176 + RecommendationsSettings: "settings/streaming/recommendations", 174 177 PrivacyCategory: "settings/privacy", 175 178 DanmuCategory: "settings/danmu", 176 179 AdvancedCategory: "settings/advanced", ··· 514 517 options={{ 515 518 drawerIcon: () => <Home color={foregroundColor} size={24} />, 516 519 drawerLabel: () => <Text variant="h5">Home</Text>, 517 - headerTitle: "Streamplace", 520 + headerTitle: isWeb ? "Home" : "Streamplace", 518 521 headerShown: isWeb, 519 522 title: "Streamplace", 520 523 }} ··· 567 570 drawerLabel: () => <Text variant="h5">Settings</Text>, 568 571 headerShown: false, 569 572 }} 573 + listeners={{ 574 + drawerItemPress: (e) => { 575 + e.preventDefault(); 576 + navigation.dispatch( 577 + CommonActions.reset({ 578 + index: 0, 579 + routes: [ 580 + { 581 + name: "Settings", 582 + }, 583 + ], 584 + }), 585 + ); 586 + }, 587 + }} 570 588 /> 571 589 572 590 <Drawer.Screen ··· 750 768 name="WebhooksSettings" 751 769 component={WebhookManager} 752 770 options={{ headerTitle: "Webhooks", title: "Webhooks" }} 771 + /> 772 + <Stack.Screen 773 + name="RecommendationsSettings" 774 + component={RecommendationsManager} 775 + options={{ headerTitle: "Recommendations", title: "Recommendations" }} 753 776 /> 754 777 <Stack.Screen 755 778 name="PrivacyCategory"
+56 -10
js/app/src/screens/mobile-stream.tsx
··· 1 - import { KeepAwake } from "@streamplace/components"; 1 + import { 2 + KeepAwake, 3 + LivestreamProvider, 4 + PlayerProvider, 5 + useLivestreamStore, 6 + } from "@streamplace/components"; 2 7 import { Player } from "components/mobile/player"; 3 8 import { PlayerProps } from "components/player/props"; 4 9 import { FullscreenProvider } from "contexts/FullscreenContext"; 5 10 import useTitle from "hooks/useTitle"; 6 - import { Platform } from "react-native"; 11 + import { Platform, Text, View } from "react-native"; 7 12 import { queryToProps } from "./util"; 8 13 9 14 const isWeb = Platform.OS === "web"; 10 15 16 + function StreamError({ message }: { message: string }) { 17 + return ( 18 + <View 19 + style={{ 20 + flex: 1, 21 + justifyContent: "center", 22 + alignItems: "center", 23 + backgroundColor: "#111", 24 + }} 25 + > 26 + <Text style={{ color: "#fff", fontSize: 18 }}>{message}</Text> 27 + </View> 28 + ); 29 + } 30 + 31 + function MobileStreamInner({ 32 + user, 33 + src, 34 + extraProps, 35 + }: { 36 + user: string; 37 + src: string; 38 + extraProps: Partial<PlayerProps>; 39 + }) { 40 + const problems = useLivestreamStore((x) => x.problems); 41 + 42 + const userNotFoundError = problems.find((p) => p.code === "user_not_found"); 43 + 44 + useTitle(user); 45 + 46 + if (userNotFoundError) { 47 + return <StreamError message={userNotFoundError.message} />; 48 + } 49 + 50 + return ( 51 + <> 52 + <KeepAwake /> 53 + <FullscreenProvider> 54 + <Player src={src} {...extraProps} /> 55 + </FullscreenProvider> 56 + </> 57 + ); 58 + } 59 + 11 60 export default function MobileStream({ route }) { 12 61 const { user, protocol, url } = route.params; 13 62 let extraProps: Partial<PlayerProps> = {}; ··· 19 68 src = url; 20 69 } 21 70 22 - useTitle(user); 23 - 24 71 return ( 25 - <> 26 - <KeepAwake /> 27 - <FullscreenProvider> 28 - <Player src={src} {...extraProps} /> 29 - </FullscreenProvider> 30 - </> 72 + <LivestreamProvider src={src}> 73 + <PlayerProvider> 74 + <MobileStreamInner user={user} src={src} extraProps={extraProps} /> 75 + </PlayerProvider> 76 + </LivestreamProvider> 31 77 ); 32 78 }
+1 -1
js/app/store/slices/blueskySlice.ts
··· 522 522 }, 523 523 facets: rt.facets, 524 524 createdAt: now.toISOString(), 525 + langs: ["en"], 525 526 }; 526 527 record.embed = { 527 528 $type: "app.bsky.embed.external", ··· 532 533 uri: linkUrl, 533 534 }, 534 535 }; 535 - console.log("golivePost record", record); 536 536 return await state.pdsAgent.post(record); 537 537 }, 538 538
+19
js/app/store/slices/streamplaceSlice.ts
··· 38 38 chatWarn: (warned: boolean) => void; 39 39 getIdentity: () => Promise<void>; 40 40 pollMySegments: () => Promise<void>; 41 + getRecommendations: (userDID: string) => Promise<{ 42 + recommendations: Array<{ 43 + $type: string; 44 + did?: string; 45 + source?: string; 46 + uri?: string; 47 + }>; 48 + userDID?: string; 49 + }>; 41 50 } 42 51 43 52 export const createStreamplaceSlice: StateCreator<StreamplaceSlice> = ( ··· 113 122 } catch (err) { 114 123 // silently fail 115 124 } 125 + }, 126 + getRecommendations: async (userDID: string) => { 127 + const state = get() as any; // need to access bluesky slice 128 + if (!state.pdsAgent) { 129 + throw new Error("no pdsAgent"); 130 + } 131 + const result = await state.pdsAgent.place.stream.live.getRecommendations({ 132 + userDID, 133 + }); 134 + return result.data; 116 135 }, 117 136 });
+21
js/components/i18next.config.js
··· 1 + import { defineConfig } from "i18next-cli"; 2 + // i18next-cli configuration 3 + // We use i18next-fluent, so variant handling (platform, count, etc.) 4 + // is done using Fluent's select expressions within messages. 5 + // Example: shortcut-key-search = { $platform -> [mac] Cmd+K [windows] Ctrl+K *[other] Ctrl+K } 6 + 7 + export default defineConfig({ 8 + locales: ["en-US", "es-ES", "fr-FR", "pt-BR", "zh-Hant"], 9 + extract: { 10 + input: [ 11 + "src/**/*.{js,jsx,ts,tsx}", 12 + "../app/src/**/*.{js,jsx,ts,tsx}", 13 + "../app/components/**/*.{js,jsx,ts,tsx}", 14 + ], 15 + contextSeparator: "|", 16 + pluralSeparator: "/", 17 + output: "public/locales/{{language}}/{{namespace}}.json", 18 + primaryLanguage: "en-US", 19 + defaultNS: "common", 20 + }, 21 + });
+16
js/components/locales/en-US/common.ftl
··· 44 44 [1] One notification 45 45 *[other] { $count } notifications 46 46 } 47 + 48 + ## Offline User 49 + user-offline = user is offline 50 + user-offline-message = { $source -> 51 + [streamer] Looks like <1>@{ $handle } is offline</1>, but they recommend checking out: 52 + *[default] Looks like <1>@{ $handle } is offline</1>, but we recommend checking out: 53 + } 54 + user-offline-no-recommendations = 55 + Looks like <1>@{ $handle } is offline</1> right now. 56 + Check back later. 57 + streaming-title = streaming { $title } 58 + viewer-count = { $count -> 59 + [0] 0 viewers 60 + [1] 1 viewer 61 + *[other] { $count } viewers 62 + }
+13
js/components/locales/en-US/settings.ftl
··· 80 80 *[other] { $count } keys 81 81 } 82 82 83 + ## Recommendations 84 + recommendations = Recommendations 85 + manage-recommendations = Manage Recommendations 86 + recommendations-to-others = Recommendations to Others 87 + recommendations-description = Share up to 8 streamers you recommend to your viewers 88 + no-recommendations-yet = No recommendations configured yet 89 + add-recommendation = Add Recommendation 90 + streamer-did = Streamer DID 91 + recommendations-count = { $count -> 92 + [one] { $count } recommendation 93 + *[other] { $count } recommendations 94 + } 95 + 83 96 ## Webhook Management 84 97 webhooks = Webhooks 85 98 webhook-integrations = Webhook Integrations
+16
js/components/locales/es-ES/common.ftl
··· 44 44 [1] Una notificación 45 45 *[other] { $count } notificaciones 46 46 } 47 + 48 + ## Offline User 49 + user-offline = usuario desconectado 50 + user-offline-message = { $source -> 51 + [streamer] Parece que <1>@{ $handle } está desconectado</1>, pero ellos recomiendan ver: 52 + *[default] Parece que <1>@{ $handle } está desconectado</1>, pero te recomendamos ver: 53 + } 54 + user-offline-no-recommendations = 55 + Parece que <1>@{ $handle } está desconectado</1> ahora mismo. 56 + Vuelve más tarde. 57 + streaming-title = transmitiendo { $title } 58 + viewer-count = { $count -> 59 + [0] 0 espectadores 60 + [1] 1 espectador 61 + *[other] { $count } espectadores 62 + }
+1 -1
js/components/locales/es-ES/settings.ftl
··· 113 113 manage-keys = Gestionar Claves 114 114 your-stream-pubkeys = Tus Claves Públicas de Transmisión 115 115 no-keys = No hay claves configuradas 116 - pubkey-description = Las claves públicas se emparejan con claves de transmisión (usadas en software de streaming) para firmar y verificar tu transmisión 116 + pubkey-description = Las claves públicas se emparejan con claves de transmisión (usadas en software de transmitiendo) para firmar y verificar tu transmisión 117 117 118 118 keys-count = { $count -> 119 119 [one] { $count } clave
+16
js/components/locales/fr-FR/common.ftl
··· 44 44 [1] Une notification 45 45 *[other] { $count } notifications 46 46 } 47 + 48 + ## Offline User 49 + user-offline = utilisateur hors ligne 50 + user-offline-message = { $source -> 51 + [streamer] On dirait que <1>@{ $handle } est hors ligne</1>, mais ils recommandent de regarder : 52 + *[default] On dirait que <1>@{ $handle } est hors ligne</1>, mais nous recommandons de regarder : 53 + } 54 + user-offline-no-recommendations = 55 + On dirait que <1>@{ $handle } est hors ligne</1> maintenant. 56 + Revenez plus tard. 57 + streaming-title = diffusion de { $title } 58 + viewer-count = { $count -> 59 + [0] 0 spectateurs 60 + [1] 1 spectateur 61 + *[other] { $count } spectateurs 62 + }
+16
js/components/locales/pt-BR/common.ftl
··· 44 44 [1] Uma notificação 45 45 *[other] { $count } notificações 46 46 } 47 + 48 + ## Offline User 49 + user-offline = usuário offline 50 + user-offline-message = { $source -> 51 + [streamer] Parece que <1>@{ $handle } está offline</1>, mas eles recomendam assistir: 52 + *[default] Parece que <1>@{ $handle } está offline</1>, mas recomendamos assistir: 53 + } 54 + user-offline-no-recommendations = 55 + Parece que <1>@{ $handle } está offline</1> agora. 56 + Volte mais tarde. 57 + streaming-title = transmitindo { $title } 58 + viewer-count = { $count -> 59 + [0] 0 espectadores 60 + [1] 1 espectador 61 + *[other] { $count } espectadores 62 + }
+1 -1
js/components/locales/pt-BR/settings.ftl
··· 111 111 manage-keys = Gerenciar Chaves 112 112 your-stream-pubkeys = Suas Chaves Públicas de Transmissão 113 113 no-keys = Nenhuma chave configurada 114 - pubkey-description = Chaves públicas são emparelhadas com chaves de transmissão (usadas em software de streaming) para assinar e verificar sua transmissão 114 + pubkey-description = Chaves públicas são emparelhadas com chaves de transmissão (usadas em software de transmitindo) para assinar e verificar sua transmissão 115 115 116 116 keys-count = { $count -> 117 117 [one] { $count } chave
+16
js/components/locales/zh-Hant/common.ftl
··· 44 44 [1] 一則通知 45 45 *[other] { $count } 則通知 46 46 } 47 + 48 + ## Offline User 49 + user-offline = 使用者離線 50 + user-offline-message = { $source -> 51 + [streamer] 看起來 <1>@{ $handle } 離線</1> 了,但他們推薦觀看: 52 + *[default] 看起來 <1>@{ $handle } 離線</1> 了,但我們推薦觀看: 53 + } 54 + user-offline-no-recommendations = 55 + 看起來 <1>@{ $handle } 離線</1> 了。 56 + 請稍後再來看看。 57 + streaming-title = 正在直播 { $title } 58 + viewer-count = { $count -> 59 + [0] 0 位觀眾 60 + [1] 1 位觀眾 61 + *[other] { $count } 位觀眾 62 + }
+4 -3
js/components/package.json
··· 1 1 { 2 2 "name": "@streamplace/components", 3 - "version": "0.8.14", 3 + "version": "0.8.18", 4 4 "description": "Streamplace React (Native) Components", 5 5 "main": "dist/index.js", 6 6 "types": "src/index.tsx", ··· 19 19 "devDependencies": { 20 20 "@fluent/syntax": "^0.19.0", 21 21 "@types/sdp-transform": "^2.15.0", 22 + "i18next-cli": "^1.32.0", 22 23 "nodemon": "^3.1.10", 23 24 "tsup": "^8.5.0" 24 25 }, ··· 74 75 "start": "tsc --watch --preserveWatchOutput", 75 76 "prepare": "node scripts/compile-translations.js && tsc", 76 77 "i18n:compile": "node scripts/compile-translations.js", 77 - "i18n:watch": "nodemon scripts/compile-translations.js --watch locales/**/*.ftl", 78 - "i18n:extract": "node scripts/extract-i18n.js" 78 + "i18n:watch": "nodemon --watch 'locales/**/*.ftl' --exec 'node scripts/compile-translations.js'", 79 + "i18n:extract": "i18next-cli extract && node scripts/migrate-i18n.js" 79 80 } 80 81 }
-336
js/components/scripts/extract-i18n.js
··· 1 - #!/usr/bin/env node 2 - 3 - /** 4 - * i18n key extraction script 5 - * 1. Scans the codebase for i18n keys (t('key'), Trans components, etc) 6 - * 2. Extracts keys into namespace JSON files (common.json, settings.json, etc) 7 - * 3. Migrates new keys to corresponding .ftl files for translation 8 - * 9 - * Usage: 10 - * node extract-i18n.js # Extract keys and report new ones 11 - * node extract-i18n.js --add-to=common # Add new keys to common.ftl 12 - * node extract-i18n.js --add-to=settings # Add new keys to settings.ftl 13 - */ 14 - 15 - const { execSync } = require("child_process"); 16 - const fs = require("fs"); 17 - const path = require("path"); 18 - 19 - // Parse command line arguments 20 - const args = process.argv.slice(2); 21 - const addToNamespace = args 22 - .find((arg) => arg.startsWith("--add-to=")) 23 - ?.split("=")[1]; 24 - 25 - // Paths 26 - const COMPONENTS_ROOT = path.join(__dirname, ".."); 27 - const APP_ROOT = path.join(__dirname, "..", "..", "app"); 28 - const MANIFEST_PATH = path.join(COMPONENTS_ROOT, "locales/manifest.json"); 29 - const LOCALES_FTL_DIR = path.join(COMPONENTS_ROOT, "locales"); 30 - const LOCALES_JSON_DIR = path.join(COMPONENTS_ROOT, "public/locales"); 31 - 32 - // Load manifest 33 - const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8")); 34 - 35 - // Configuration for i18next-parser 36 - const parserConfig = { 37 - contextSeparator: "_", 38 - createOldCatalogs: false, 39 - defaultNamespace: "messages", 40 - defaultValue: "", 41 - indentation: 2, 42 - keepRemoved: true, 43 - keySeparator: false, 44 - namespaceSeparator: false, 45 - 46 - lexers: { 47 - js: ["JavascriptLexer"], 48 - ts: ["JavascriptLexer"], 49 - jsx: ["JsxLexer"], 50 - tsx: ["JsxLexer"], 51 - html: false, 52 - htm: false, 53 - handlebars: false, 54 - hbs: false, 55 - }, 56 - 57 - locales: manifest.supportedLocales, 58 - output: path.join(LOCALES_JSON_DIR, "$LOCALE/$NAMESPACE.json"), 59 - input: [ 60 - path.join(COMPONENTS_ROOT, "src/**/*.{js,jsx,ts,tsx}"), 61 - path.join(APP_ROOT, "src/**/*.{js,jsx,ts,tsx}"), 62 - path.join(APP_ROOT, "components/**/*.{js,jsx,ts,tsx}"), 63 - "!**/node_modules/**", 64 - "!**/dist/**", 65 - "!**/*.test.{js,jsx,ts,tsx}", 66 - "!**/*.spec.{js,jsx,ts,tsx}", 67 - ], 68 - 69 - verbose: true, 70 - sort: true, 71 - failOnWarnings: false, 72 - failOnUpdate: false, 73 - }; 74 - 75 - /** 76 - * Extract keys from codebase using i18next-parser 77 - */ 78 - function extractKeys() { 79 - const configPath = path.join(__dirname, ".i18next-parser.config.js"); 80 - const configContent = `module.exports = ${JSON.stringify(parserConfig, null, 2)};`; 81 - 82 - try { 83 - fs.writeFileSync(configPath, configContent); 84 - console.log("🔍 Extracting i18n keys from codebase..."); 85 - 86 - execSync(`npx i18next-parser --config ${configPath}`, { 87 - stdio: "inherit", 88 - cwd: __dirname, 89 - }); 90 - 91 - console.log("✅ Keys extracted successfully!"); 92 - return true; 93 - } catch (error) { 94 - console.error("❌ Error extracting i18n keys:", error.message); 95 - return false; 96 - } finally { 97 - if (fs.existsSync(configPath)) { 98 - fs.unlinkSync(configPath); 99 - } 100 - } 101 - } 102 - 103 - /** 104 - * Read existing keys from .ftl files in a locale directory 105 - * Returns a map of namespace -> Set of keys 106 - */ 107 - function getExistingFtlKeys(localeDir) { 108 - const keysByNamespace = {}; 109 - 110 - if (!fs.existsSync(localeDir)) { 111 - return keysByNamespace; 112 - } 113 - 114 - const ftlFiles = fs 115 - .readdirSync(localeDir) 116 - .filter((file) => file.endsWith(".ftl")); 117 - 118 - for (const file of ftlFiles) { 119 - const namespace = path.basename(file, ".ftl"); 120 - const keys = new Set(); 121 - 122 - const content = fs.readFileSync(path.join(localeDir, file), "utf8"); 123 - const lines = content.split("\n"); 124 - 125 - for (const line of lines) { 126 - const trimmed = line.trim(); 127 - const keyMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9_-]*)\s*=/); 128 - if (keyMatch) { 129 - keys.add(keyMatch[1]); 130 - } 131 - } 132 - 133 - keysByNamespace[namespace] = keys; 134 - } 135 - 136 - return keysByNamespace; 137 - } 138 - 139 - /** 140 - * Get all namespaces (json files) in the locale directory 141 - */ 142 - function getNamespaces(localeJsonDir) { 143 - if (!fs.existsSync(localeJsonDir)) { 144 - return []; 145 - } 146 - 147 - return fs 148 - .readdirSync(localeJsonDir) 149 - .filter((file) => file.endsWith(".json")) 150 - .map((file) => path.basename(file, ".json")); 151 - } 152 - 153 - /** 154 - * Add new keys to a .ftl file 155 - */ 156 - function addKeysToFtlFile(localeDir, namespace, newKeys, locale) { 157 - const targetFile = path.join(localeDir, `${namespace}.ftl`); 158 - 159 - // Create file with header if it doesn't exist 160 - if (!fs.existsSync(localeDir)) { 161 - fs.mkdirSync(localeDir, { recursive: true }); 162 - } 163 - 164 - if (!fs.existsSync(targetFile)) { 165 - const languageName = manifest.languages[locale]?.name || locale; 166 - const namespaceName = 167 - namespace.charAt(0).toUpperCase() + namespace.slice(1); 168 - const header = `# ${namespaceName} translations - ${languageName}\n\n`; 169 - fs.writeFileSync(targetFile, header); 170 - } 171 - 172 - // Append new keys 173 - let content = fs.readFileSync(targetFile, "utf8"); 174 - 175 - if (!content.endsWith("\n")) { 176 - content += "\n"; 177 - } 178 - 179 - content += "\n# Newly extracted keys\n"; 180 - content += newKeys.map((key) => `${key} = ${key}`).join("\n") + "\n"; 181 - 182 - fs.writeFileSync(targetFile, content); 183 - 184 - return targetFile; 185 - } 186 - 187 - /** 188 - * Migrate extracted JSON keys to .ftl files 189 - */ 190 - function migrateKeysToFtl() { 191 - console.log("\n🔄 Analyzing extracted keys..."); 192 - 193 - const newKeysByLocaleAndNamespace = {}; // locale -> namespace -> [keys] 194 - 195 - // Process each locale 196 - for (const locale of manifest.supportedLocales) { 197 - const localeJsonDir = path.join(LOCALES_JSON_DIR, locale); 198 - const localeFtlDir = path.join(LOCALES_FTL_DIR, locale); 199 - 200 - if (!fs.existsSync(localeJsonDir)) { 201 - console.log(`⚠️ No JSON files found for ${locale}`); 202 - continue; 203 - } 204 - 205 - // Get all namespaces (json files) 206 - const namespaces = getNamespaces(localeJsonDir); 207 - 208 - if (namespaces.length === 0) { 209 - console.log(`⚠️ No namespace files found for ${locale}`); 210 - continue; 211 - } 212 - 213 - // Get existing keys from .ftl files 214 - const existingKeysByNamespace = getExistingFtlKeys(localeFtlDir); 215 - 216 - // Process each namespace 217 - for (const namespace of namespaces) { 218 - const jsonPath = path.join(localeJsonDir, `${namespace}.json`); 219 - const jsonContent = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 220 - const extractedKeys = Object.keys(jsonContent); 221 - 222 - // Get existing keys for this namespace 223 - const existingKeys = existingKeysByNamespace[namespace] || new Set(); 224 - 225 - // Find new keys 226 - const newKeys = extractedKeys.filter((key) => !existingKeys.has(key)); 227 - 228 - if (newKeys.length > 0) { 229 - if (!newKeysByLocaleAndNamespace[locale]) { 230 - newKeysByLocaleAndNamespace[locale] = {}; 231 - } 232 - newKeysByLocaleAndNamespace[locale][namespace] = newKeys; 233 - } 234 - } 235 - } 236 - 237 - // Check if there are any new keys 238 - const hasNewKeys = Object.keys(newKeysByLocaleAndNamespace).length > 0; 239 - 240 - if (!hasNewKeys) { 241 - console.log( 242 - "\n🎉 No new keys found. All extracted keys already exist in .ftl files.", 243 - ); 244 - return; 245 - } 246 - 247 - // Display found keys 248 - console.log("\n📊 New keys found:"); 249 - for (const locale of Object.keys(newKeysByLocaleAndNamespace)) { 250 - console.log(`\n${locale}:`); 251 - for (const namespace of Object.keys(newKeysByLocaleAndNamespace[locale])) { 252 - const keys = newKeysByLocaleAndNamespace[locale][namespace]; 253 - console.log(` 📝 ${namespace} (${keys.length} new keys):`); 254 - keys.forEach((key) => console.log(` - ${key}`)); 255 - } 256 - } 257 - 258 - // If --add-to flag is provided, add keys to that namespace 259 - if (addToNamespace) { 260 - console.log(`\n✍️ Adding new keys to ${addToNamespace}.ftl files...`); 261 - 262 - let totalAdded = 0; 263 - const processedFiles = []; 264 - 265 - for (const locale of Object.keys(newKeysByLocaleAndNamespace)) { 266 - const localeFtlDir = path.join(LOCALES_FTL_DIR, locale); 267 - const namespacesForLocale = newKeysByLocaleAndNamespace[locale]; 268 - 269 - // Collect all new keys across all namespaces for this locale 270 - const allNewKeys = []; 271 - for (const namespace of Object.keys(namespacesForLocale)) { 272 - allNewKeys.push(...namespacesForLocale[namespace]); 273 - } 274 - 275 - if (allNewKeys.length === 0) continue; 276 - 277 - // Add all keys to the specified namespace 278 - const targetFile = addKeysToFtlFile( 279 - localeFtlDir, 280 - addToNamespace, 281 - allNewKeys, 282 - locale, 283 - ); 284 - processedFiles.push(path.relative(process.cwd(), targetFile)); 285 - totalAdded += allNewKeys.length; 286 - 287 - console.log( 288 - `✅ ${locale}: Added ${allNewKeys.length} keys to ${addToNamespace}.ftl`, 289 - ); 290 - } 291 - 292 - console.log( 293 - `\n🎉 Migration complete! Added ${totalAdded} new keys to ${addToNamespace}.ftl files.`, 294 - ); 295 - console.log("\nModified files:"); 296 - processedFiles.forEach((file) => console.log(` 📄 ${file}`)); 297 - 298 - console.log("\n💡 Next steps:"); 299 - console.log(" 1. Review the new keys in your .ftl files"); 300 - console.log(" 2. Replace placeholder values with actual translations"); 301 - console.log(" 3. Run `pnpm i18n:compile` to update compiled JSON files"); 302 - } else { 303 - // Just report 304 - let totalNewKeys = 0; 305 - const namespaceSet = new Set(); 306 - 307 - for (const locale of Object.keys(newKeysByLocaleAndNamespace)) { 308 - for (const namespace of Object.keys( 309 - newKeysByLocaleAndNamespace[locale], 310 - )) { 311 - namespaceSet.add(namespace); 312 - totalNewKeys += newKeysByLocaleAndNamespace[locale][namespace].length; 313 - } 314 - } 315 - 316 - console.log( 317 - `\n💡 Found ${totalNewKeys} new keys across ${namespaceSet.size} namespace(s).`, 318 - ); 319 - console.log("\nTo add these keys to a specific namespace file, run:"); 320 - Array.from(namespaceSet).forEach((ns) => { 321 - console.log(` node extract-i18n.js --add-to=${ns}`); 322 - }); 323 - } 324 - } 325 - 326 - function main() { 327 - const success = extractKeys(); 328 - 329 - if (success) { 330 - migrateKeysToFtl(); 331 - } else { 332 - process.exit(1); 333 - } 334 - } 335 - 336 - main();
+413
js/components/scripts/migrate-i18n.js
··· 1 + #!/usr/bin/env node 2 + 3 + /** 4 + * i18n migration script 5 + * Migrates extracted JSON keys to .ftl files for translation 6 + * 7 + * This script expects that i18next-cli has already extracted keys to JSON files. 8 + * It reads those JSON files, compares them to existing .ftl files, and adds any 9 + * new keys to the .ftl files. 10 + * 11 + * For keys with i18next context/plural suffixes (e.g., key_male, key_female, key_one, key_other), 12 + * it will convert them into Fluent select expressions. 13 + * 14 + * Usage: 15 + * node migrate-i18n.js # Report new keys 16 + * node migrate-i18n.js --add-to=common # Add new keys to common.ftl 17 + * node migrate-i18n.js --add-to=settings # Add new keys to settings.ftl 18 + */ 19 + 20 + const fs = require("fs"); 21 + const path = require("path"); 22 + 23 + // Parse command line arguments 24 + const args = process.argv.slice(2); 25 + const addToNamespace = args 26 + .find((arg) => arg.startsWith("--add-to=")) 27 + ?.split("=")[1]; 28 + 29 + // Paths 30 + const COMPONENTS_ROOT = path.join(__dirname, ".."); 31 + const MANIFEST_PATH = path.join(COMPONENTS_ROOT, "locales/manifest.json"); 32 + const LOCALES_FTL_DIR = path.join(COMPONENTS_ROOT, "locales"); 33 + const LOCALES_JSON_DIR = path.join(COMPONENTS_ROOT, "public/locales"); 34 + 35 + // Load manifest 36 + const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8")); 37 + 38 + // Plural forms that i18next uses 39 + const PLURAL_FORMS = ["zero", "one", "two", "few", "many", "other"]; 40 + 41 + // Separators used by i18next-cli (configured in i18next.config.js) 42 + const CONTEXT_SEPARATOR = "|"; 43 + const PLURAL_SEPARATOR = "/"; 44 + 45 + /** 46 + * Group keys by base name, detecting context and plural variants 47 + * Returns { baseKey: { base: true, variants: { context: Set, plurals: Set } } } 48 + */ 49 + function groupKeysByBase(keys) { 50 + const groups = {}; 51 + 52 + for (const key of keys) { 53 + if (!key.includes(CONTEXT_SEPARATOR) && !key.includes(PLURAL_SEPARATOR)) { 54 + // Simple key with no variants 55 + if (!groups[key]) { 56 + groups[key] = { 57 + base: true, 58 + variants: { contexts: new Set(), plurals: new Set() }, 59 + }; 60 + } 61 + groups[key].base = true; 62 + } else { 63 + // Key with variants 64 + // Format: base|context/plural or base/plural or base|context 65 + let baseKey = key; 66 + const detectedContexts = new Set(); 67 + const detectedPlurals = new Set(); 68 + 69 + // Split by context separator first 70 + if (key.includes(CONTEXT_SEPARATOR)) { 71 + const contextParts = key.split(CONTEXT_SEPARATOR); 72 + baseKey = contextParts[0]; 73 + 74 + // The remaining part might have plurals 75 + const contextAndPlural = contextParts[1]; 76 + 77 + if (contextAndPlural.includes(PLURAL_SEPARATOR)) { 78 + const pluralParts = contextAndPlural.split(PLURAL_SEPARATOR); 79 + detectedContexts.add(pluralParts[0]); 80 + pluralParts.slice(1).forEach((p) => { 81 + if (PLURAL_FORMS.includes(p)) { 82 + detectedPlurals.add(p); 83 + } 84 + }); 85 + } else { 86 + detectedContexts.add(contextAndPlural); 87 + } 88 + } else if (key.includes(PLURAL_SEPARATOR)) { 89 + // No context, just plural 90 + const pluralParts = key.split(PLURAL_SEPARATOR); 91 + baseKey = pluralParts[0]; 92 + pluralParts.slice(1).forEach((p) => { 93 + if (PLURAL_FORMS.includes(p)) { 94 + detectedPlurals.add(p); 95 + } 96 + }); 97 + } 98 + 99 + if (!groups[baseKey]) { 100 + groups[baseKey] = { 101 + base: false, 102 + variants: { contexts: new Set(), plurals: new Set() }, 103 + }; 104 + } 105 + 106 + detectedContexts.forEach((c) => groups[baseKey].variants.contexts.add(c)); 107 + detectedPlurals.forEach((p) => groups[baseKey].variants.plurals.add(p)); 108 + } 109 + } 110 + 111 + return groups; 112 + } 113 + 114 + /** 115 + * Convert a group of keys into Fluent format 116 + */ 117 + function convertToFluentFormat(baseKey, group) { 118 + const hasContexts = group.variants.contexts.size > 0; 119 + const hasPlurals = group.variants.plurals.size > 0; 120 + 121 + if (!hasContexts && !hasPlurals) { 122 + // Simple key 123 + return `${baseKey} = ${baseKey}`; 124 + } 125 + 126 + // Build Fluent select expression 127 + let selector = ""; 128 + let variants = []; 129 + 130 + if (hasContexts && hasPlurals) { 131 + // Both context and plural - outer selector is context, inner is plural 132 + selector = "$context"; 133 + const contextsList = Array.from(group.variants.contexts).sort(); 134 + const pluralsList = Array.from(group.variants.plurals).sort(); 135 + 136 + contextsList.forEach((context, idx) => { 137 + const isDefault = idx === contextsList.length - 1; 138 + const prefix = isDefault ? "*" : " "; 139 + 140 + // Build inner plural select 141 + const pluralVariants = pluralsList 142 + .map((p) => { 143 + const pluralPrefix = p === "other" ? "*" : ""; 144 + return `${pluralPrefix}[${p}] ${baseKey}`; 145 + }) 146 + .join(" "); 147 + 148 + variants.push( 149 + `\n ${prefix}[${context}] { $count -> ${pluralVariants} }`, 150 + ); 151 + }); 152 + } else if (hasContexts) { 153 + // Only context 154 + selector = "$context"; 155 + const contextsList = Array.from(group.variants.contexts).sort(); 156 + contextsList.forEach((context, idx) => { 157 + const isDefault = idx === contextsList.length - 1; 158 + const prefix = isDefault ? "*" : " "; 159 + variants.push(`\n ${prefix}[${context}] ${baseKey}`); 160 + }); 161 + } else if (hasPlurals) { 162 + // Only plural 163 + selector = "$count"; 164 + const pluralsList = Array.from(group.variants.plurals).sort(); 165 + pluralsList.forEach((plural) => { 166 + const isDefault = plural === "other"; 167 + const prefix = isDefault ? "*" : " "; 168 + variants.push(`\n ${prefix}[${plural}] ${baseKey}`); 169 + }); 170 + } 171 + 172 + return `# TODO: Convert to proper Fluent select expression\n${baseKey} = { ${selector} ->${variants.join("")}\n}`; 173 + } 174 + 175 + /** 176 + * Read existing keys from .ftl files in a locale directory 177 + * Returns a map of namespace -> Set of keys 178 + */ 179 + function getExistingFtlKeys(localeDir) { 180 + const keysByNamespace = {}; 181 + 182 + if (!fs.existsSync(localeDir)) { 183 + return keysByNamespace; 184 + } 185 + 186 + const ftlFiles = fs 187 + .readdirSync(localeDir) 188 + .filter((file) => file.endsWith(".ftl")); 189 + 190 + for (const file of ftlFiles) { 191 + const namespace = path.basename(file, ".ftl"); 192 + const keys = new Set(); 193 + 194 + const content = fs.readFileSync(path.join(localeDir, file), "utf8"); 195 + const lines = content.split("\n"); 196 + 197 + for (const line of lines) { 198 + const trimmed = line.trim(); 199 + const keyMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9_-]*)\s*=/); 200 + if (keyMatch) { 201 + keys.add(keyMatch[1]); 202 + } 203 + } 204 + 205 + keysByNamespace[namespace] = keys; 206 + } 207 + 208 + return keysByNamespace; 209 + } 210 + 211 + /** 212 + * Get all namespaces (json files) in the locale directory 213 + */ 214 + function getNamespaces(localeJsonDir) { 215 + if (!fs.existsSync(localeJsonDir)) { 216 + return []; 217 + } 218 + 219 + return fs 220 + .readdirSync(localeJsonDir) 221 + .filter((file) => file.endsWith(".json")) 222 + .map((file) => path.basename(file, ".json")); 223 + } 224 + 225 + /** 226 + * Add new keys to a .ftl file, converting context/plural keys to Fluent format 227 + */ 228 + function addKeysToFtlFile(localeDir, namespace, newKeys, locale) { 229 + const targetFile = path.join(localeDir, `${namespace}.ftl`); 230 + 231 + // Create file with header if it doesn't exist 232 + if (!fs.existsSync(localeDir)) { 233 + fs.mkdirSync(localeDir, { recursive: true }); 234 + } 235 + 236 + if (!fs.existsSync(targetFile)) { 237 + const languageName = manifest.languages[locale]?.name || locale; 238 + const namespaceName = 239 + namespace.charAt(0).toUpperCase() + namespace.slice(1); 240 + const header = `# ${namespaceName} translations - ${languageName}\n\n`; 241 + fs.writeFileSync(targetFile, header); 242 + } 243 + 244 + // Group keys by base to detect context/plural variants 245 + const keyGroups = groupKeysByBase(newKeys); 246 + 247 + // Build content 248 + const fluentEntries = []; 249 + for (const [baseKey, group] of Object.entries(keyGroups)) { 250 + fluentEntries.push(convertToFluentFormat(baseKey, group)); 251 + } 252 + 253 + // Append new keys 254 + let content = fs.readFileSync(targetFile, "utf8"); 255 + 256 + if (!content.endsWith("\n")) { 257 + content += "\n"; 258 + } 259 + 260 + content += "\n# Newly extracted keys\n"; 261 + content += fluentEntries.join("\n\n") + "\n"; 262 + 263 + fs.writeFileSync(targetFile, content); 264 + 265 + return targetFile; 266 + } 267 + 268 + /** 269 + * Migrate extracted JSON keys to .ftl files 270 + */ 271 + function migrateKeysToFtl() { 272 + console.log("🔄 Analyzing extracted keys..."); 273 + 274 + const newKeysByLocaleAndNamespace = {}; // locale -> namespace -> [keys] 275 + 276 + // Process each locale 277 + for (const locale of manifest.supportedLocales) { 278 + const localeJsonDir = path.join(LOCALES_JSON_DIR, locale); 279 + const localeFtlDir = path.join(LOCALES_FTL_DIR, locale); 280 + 281 + if (!fs.existsSync(localeJsonDir)) { 282 + console.log(`⚠️ No JSON files found for ${locale}`); 283 + continue; 284 + } 285 + 286 + // Get all namespaces (json files) 287 + const namespaces = getNamespaces(localeJsonDir); 288 + 289 + if (namespaces.length === 0) { 290 + console.log(`⚠️ No namespace files found for ${locale}`); 291 + continue; 292 + } 293 + 294 + // Get existing keys from .ftl files 295 + const existingKeysByNamespace = getExistingFtlKeys(localeFtlDir); 296 + 297 + // Process each namespace 298 + for (const namespace of namespaces) { 299 + const jsonPath = path.join(localeJsonDir, `${namespace}.json`); 300 + const jsonContent = JSON.parse(fs.readFileSync(jsonPath, "utf8")); 301 + const extractedKeys = Object.keys(jsonContent); 302 + 303 + // Get existing keys for this namespace 304 + const existingKeys = existingKeysByNamespace[namespace] || new Set(); 305 + 306 + // Find new keys 307 + const newKeys = extractedKeys.filter((key) => !existingKeys.has(key)); 308 + 309 + if (newKeys.length > 0) { 310 + if (!newKeysByLocaleAndNamespace[locale]) { 311 + newKeysByLocaleAndNamespace[locale] = {}; 312 + } 313 + newKeysByLocaleAndNamespace[locale][namespace] = newKeys; 314 + } 315 + } 316 + } 317 + 318 + // Check if there are any new keys 319 + const hasNewKeys = Object.keys(newKeysByLocaleAndNamespace).length > 0; 320 + 321 + if (!hasNewKeys) { 322 + console.log( 323 + "\n🎉 No new keys found. All extracted keys already exist in .ftl files.", 324 + ); 325 + return; 326 + } 327 + 328 + // Display found keys 329 + console.log("\n📊 New keys found:"); 330 + for (const locale of Object.keys(newKeysByLocaleAndNamespace)) { 331 + console.log(`\n${locale}:`); 332 + for (const namespace of Object.keys(newKeysByLocaleAndNamespace[locale])) { 333 + const keys = newKeysByLocaleAndNamespace[locale][namespace]; 334 + console.log(` 📝 ${namespace} (${keys.length} new keys):`); 335 + keys.forEach((key) => console.log(` - ${key}`)); 336 + } 337 + } 338 + 339 + // If --add-to flag is provided, add keys to that namespace 340 + if (addToNamespace) { 341 + console.log(`\n✍️ Adding new keys to ${addToNamespace}.ftl files...`); 342 + 343 + let totalAdded = 0; 344 + const processedFiles = []; 345 + 346 + for (const locale of Object.keys(newKeysByLocaleAndNamespace)) { 347 + const localeFtlDir = path.join(LOCALES_FTL_DIR, locale); 348 + const namespacesForLocale = newKeysByLocaleAndNamespace[locale]; 349 + 350 + // Collect all new keys across all namespaces for this locale 351 + const allNewKeys = []; 352 + for (const namespace of Object.keys(namespacesForLocale)) { 353 + allNewKeys.push(...namespacesForLocale[namespace]); 354 + } 355 + 356 + if (allNewKeys.length === 0) continue; 357 + 358 + // Add all keys to the specified namespace 359 + const targetFile = addKeysToFtlFile( 360 + localeFtlDir, 361 + addToNamespace, 362 + allNewKeys, 363 + locale, 364 + ); 365 + processedFiles.push(path.relative(process.cwd(), targetFile)); 366 + totalAdded += allNewKeys.length; 367 + 368 + console.log( 369 + `✅ ${locale}: Added ${allNewKeys.length} keys to ${addToNamespace}.ftl`, 370 + ); 371 + } 372 + 373 + console.log( 374 + `\n🎉 Migration complete! Added ${totalAdded} new keys to ${addToNamespace}.ftl files.`, 375 + ); 376 + console.log("\nModified files:"); 377 + processedFiles.forEach((file) => console.log(` 📄 ${file}`)); 378 + 379 + console.log("\n💡 Next steps:"); 380 + console.log(" 1. Review the new keys in your .ftl files"); 381 + console.log( 382 + " 2. Convert TODO placeholders to proper Fluent translations", 383 + ); 384 + console.log(" 3. Run `pnpm i18n:compile` to update compiled JSON files"); 385 + } else { 386 + // Just report 387 + let totalNewKeys = 0; 388 + const namespaceSet = new Set(); 389 + 390 + for (const locale of Object.keys(newKeysByLocaleAndNamespace)) { 391 + for (const namespace of Object.keys( 392 + newKeysByLocaleAndNamespace[locale], 393 + )) { 394 + namespaceSet.add(namespace); 395 + totalNewKeys += newKeysByLocaleAndNamespace[locale][namespace].length; 396 + } 397 + } 398 + 399 + console.log( 400 + `\n💡 Found ${totalNewKeys} new keys across ${namespaceSet.size} namespace(s).`, 401 + ); 402 + console.log("\nTo add these keys to a specific namespace file, run:"); 403 + Array.from(namespaceSet).forEach((ns) => { 404 + console.log(` node migrate-i18n.js --add-to=${ns}`); 405 + }); 406 + } 407 + } 408 + 409 + function main() { 410 + migrateKeysToFtl(); 411 + } 412 + 413 + main();
+2 -1
js/components/src/components/mobile-player/video-async.native.tsx
··· 414 414 variant="secondary" 415 415 > 416 416 <View style={[layout.flex.row, gap.all[1]]}> 417 - <Text>Open Settings</Text> <ArrowRight color="white" size="18" /> 417 + <Text>Open Settings</Text> 418 + <ArrowRight color="white" size="18" /> 418 419 </View> 419 420 </Button> 420 421 )}
+180 -3
js/components/src/components/ui/menu.tsx
··· 1 - import { forwardRef, ReactNode } from "react"; 2 - import { Platform, View, ViewStyle } from "react-native"; 1 + import { 2 + Children, 3 + cloneElement, 4 + forwardRef, 5 + isValidElement, 6 + ReactNode, 7 + } from "react"; 8 + import { Animated, Platform, View, ViewStyle } from "react-native"; 9 + import { Gesture, GestureDetector } from "react-native-gesture-handler"; 10 + import { 11 + runOnJS, 12 + useAnimatedStyle, 13 + useSharedValue, 14 + withSpring, 15 + } from "react-native-reanimated"; 3 16 import { 4 17 a, 5 18 borderRadius, ··· 64 77 disabled?: boolean; 65 78 style?: ViewStyle; 66 79 onPress?: () => void; 80 + draggable?: boolean; 81 + dragHandle?: ReactNode; 82 + _dragIndex?: number; 83 + _dragTotalItems?: number; 84 + _onDragMove?: (fromIndex: number, toIndex: number) => void; 85 + _onDragEnd?: (fromIndex: number, toIndex: number) => void; 67 86 } 68 87 69 88 export const MenuItem = forwardRef<View, MenuItemProps>( 70 - ({ children, disabled, style }, ref) => { 89 + ( 90 + { 91 + children, 92 + disabled, 93 + style, 94 + draggable, 95 + dragHandle, 96 + _dragIndex, 97 + _dragTotalItems, 98 + _onDragMove, 99 + _onDragEnd, 100 + }, 101 + ref, 102 + ) => { 71 103 const { theme } = useTheme(); 104 + 105 + if ( 106 + draggable && 107 + _dragIndex !== undefined && 108 + _dragTotalItems !== undefined && 109 + _onDragMove && 110 + _onDragEnd 111 + ) { 112 + const translateY = useSharedValue(0); 113 + const isDragging = useSharedValue(false); 114 + const ITEM_HEIGHT = 60; 115 + 116 + const panGesture = Gesture.Pan() 117 + .onStart(() => { 118 + isDragging.value = true; 119 + }) 120 + .onUpdate((event) => { 121 + translateY.value = event.translationY; 122 + 123 + const newIndex = Math.round( 124 + _dragIndex + translateY.value / ITEM_HEIGHT, 125 + ); 126 + const clampedIndex = Math.max( 127 + 0, 128 + Math.min(_dragTotalItems - 1, newIndex), 129 + ); 130 + 131 + if (clampedIndex !== _dragIndex) { 132 + runOnJS(_onDragMove)(_dragIndex, clampedIndex); 133 + } 134 + }) 135 + .onEnd(() => { 136 + const newIndex = Math.round( 137 + _dragIndex + translateY.value / ITEM_HEIGHT, 138 + ); 139 + const clampedIndex = Math.max( 140 + 0, 141 + Math.min(_dragTotalItems - 1, newIndex), 142 + ); 143 + 144 + runOnJS(_onDragEnd)(_dragIndex, clampedIndex); 145 + 146 + translateY.value = withSpring(0); 147 + isDragging.value = false; 148 + }); 149 + 150 + const animatedStyle = useAnimatedStyle(() => ({ 151 + transform: [{ translateY: translateY.value }], 152 + zIndex: isDragging.value ? 100 : 1, 153 + opacity: isDragging.value ? 0.8 : 1, 154 + })); 155 + 156 + return ( 157 + <Animated.View style={animatedStyle}> 158 + <View 159 + ref={ref} 160 + style={[ 161 + a.layout.flex.row, 162 + a.layout.flex.alignCenter, 163 + a.radius.all.sm, 164 + py[1], 165 + pl[3], 166 + pr[2], 167 + disabled && { opacity: 0.5 }, 168 + style, 169 + ]} 170 + > 171 + {dragHandle && ( 172 + <GestureDetector gesture={panGesture}> 173 + <View style={{ marginRight: 8 }}>{dragHandle}</View> 174 + </GestureDetector> 175 + )} 176 + {typeof children === "string" ? ( 177 + <Text style={{ color: theme.colors.popoverForeground }}> 178 + {children} 179 + </Text> 180 + ) : ( 181 + children 182 + )} 183 + </View> 184 + </Animated.View> 185 + ); 186 + } 187 + 72 188 return ( 73 189 <View 74 190 ref={ref} ··· 169 285 ); 170 286 }, 171 287 ); 288 + 289 + export interface MenuDraggableGroupProps { 290 + children: ReactNode; 291 + onMove: (fromIndex: number, toIndex: number) => void; 292 + onDragEnd: (fromIndex: number, toIndex: number) => void; 293 + dragHandle?: ReactNode; 294 + style?: ViewStyle; 295 + } 296 + 297 + export const MenuDraggableGroup = forwardRef<View, MenuDraggableGroupProps>( 298 + ({ children, onMove, onDragEnd, dragHandle, style }, ref) => { 299 + const { theme } = useTheme(); 300 + 301 + const childrenArray = Children.toArray(children); 302 + const draggableItems = childrenArray.filter( 303 + (child) => 304 + isValidElement(child) && 305 + (child.type === MenuItem || child.type === MenuSeparator), 306 + ); 307 + 308 + let itemIndex = 0; 309 + const enhancedChildren = Children.map(children, (child) => { 310 + if (isValidElement(child)) { 311 + if (child.type === MenuItem) { 312 + const currentIndex = itemIndex; 313 + itemIndex++; 314 + 315 + return cloneElement(child, { 316 + draggable: true, 317 + dragHandle: dragHandle || child.props.dragHandle, 318 + _dragIndex: currentIndex, 319 + _dragTotalItems: draggableItems.filter( 320 + (c) => isValidElement(c) && c.type === MenuItem, 321 + ).length, 322 + _onDragMove: onMove, 323 + _onDragEnd: onDragEnd, 324 + } as any); 325 + } 326 + if (child.type === MenuSeparator) { 327 + return child; 328 + } 329 + } 330 + return child; 331 + }); 332 + 333 + return ( 334 + <View 335 + ref={ref} 336 + style={[ 337 + { backgroundColor: theme.colors.muted + "c0" }, 338 + Platform.OS === "web" ? [px[1], py[1]] : p[1], 339 + gap.all[1], 340 + { borderRadius: borderRadius.lg }, 341 + style, 342 + ]} 343 + > 344 + {enhancedChildren} 345 + </View> 346 + ); 347 + }, 348 + );
+1 -1
js/components/src/lib/theme/tokens.ts
··· 595 595 xs: { 596 596 fontSize: 12, 597 597 lineHeight: 16, 598 - marginBottom: -0.7, 598 + marginBottom: -0.3, 599 599 fontWeight: "400" as const, 600 600 fontFamily: "AtkinsonHyperlegibleNext-Regular", 601 601 },
+5 -1
js/components/src/livestream-provider/index.tsx
··· 5 5 export function LivestreamProvider({ 6 6 children, 7 7 src, 8 + ignoreOuterContext = false, 8 9 }: { 9 10 children: React.ReactNode; 10 11 src: string; 12 + ignoreOuterContext?: boolean; 11 13 }) { 12 14 const context = useContext(LivestreamContext); 13 15 const store = useRef(makeLivestreamStore()).current; ··· 15 17 // this is ok, there's use cases for having one in another 16 18 // like having a player component that's independently usable 17 19 // but can also be embedded within an entire livestream page 18 - return <>{children}</>; 20 + if (!ignoreOuterContext) { 21 + return <>{children}</>; 22 + } 19 23 } 20 24 (window as any).livestreamStore = store; 21 25 return (
+15 -1
js/components/src/livestream-provider/websocket.tsx
··· 12 12 13 13 const ref = useRef<any[]>([]); 14 14 const handle = useRef<NodeJS.Timeout | null>(null); 15 + const hasReceivedMessage = useRef(false); 16 + const hasErrored = useRef(false); 15 17 16 18 const { readyState } = useWebSocket(`${wsUrl}/api/websocket/${src}`, { 17 19 reconnectInterval: 1000, 18 - shouldReconnect: () => true, 20 + shouldReconnect: () => !hasErrored.current, 19 21 20 22 onOpen: () => { 21 23 ref.current = []; 24 + hasReceivedMessage.current = false; 22 25 }, 23 26 24 27 onError: (e) => { 25 28 console.log("onError", e); 29 + if (!hasReceivedMessage.current) { 30 + hasErrored.current = true; 31 + handleWebSocketMessages([ 32 + { 33 + $type: "place.stream.error", 34 + code: "user_not_found", 35 + message: "this stream doesn't exist or is unavailable", 36 + }, 37 + ]); 38 + } 26 39 }, 27 40 28 41 // spamming the redux store with messages causes a zillion re-renders, ··· 30 43 onMessage: (msg) => { 31 44 try { 32 45 const data = JSON.parse(msg.data); 46 + hasReceivedMessage.current = true; 33 47 ref.current.push(data); 34 48 if (handle.current) { 35 49 return;
+2
js/components/src/livestream-store/livestream-state.tsx
··· 21 21 replyToMessage: ChatMessageViewHydrated | null; 22 22 streamKey: string | null; 23 23 setStreamKey: (key: string | null) => void; 24 + websocketConnected: boolean; 25 + hasReceivedSegment: boolean; 24 26 } 25 27 26 28 export interface LivestreamProblem {
+5
js/components/src/livestream-store/livestream-store.tsx
··· 22 22 authors: {}, 23 23 recentSegments: [], 24 24 problems: [], 25 + websocketConnected: false, 26 + hasReceivedSegment: false, 25 27 })); 26 28 }; 27 29 ··· 58 60 export const useLivestream = () => useLivestreamStore((x) => x.livestream); 59 61 60 62 export const useSegment = () => useLivestreamStore((x) => x.segment); 63 + 64 + export const useRecentSegments = () => 65 + useLivestreamStore((x) => x.recentSegments); 61 66 62 67 export const useRenditions = () => useLivestreamStore((x) => x.renditions);
+95 -73
js/components/src/livestream-store/websocket-consumer.tsx
··· 21 21 messages: any[], 22 22 ): LivestreamState => { 23 23 for (let message of messages) { 24 - if (PlaceStreamLivestream.isLivestreamView(message)) { 25 - const newLivestream = message as LivestreamViewHydrated; 26 - const oldLivestream = state.livestream; 27 - 28 - // check if this is actually new 29 - if (!oldLivestream || oldLivestream.uri !== newLivestream.uri) { 30 - const streamTitle = newLivestream.record.title || "something cool!"; 31 - const systemMessage = SystemMessages.streamStart(streamTitle); 32 - // set proper times 33 - systemMessage.indexedAt = newLivestream.indexedAt; 34 - systemMessage.record.createdAt = newLivestream.record.createdAt; 35 - 36 - state = reduceChat(state, [systemMessage], []); 37 - } 38 - 39 - state = { 40 - ...state, 41 - livestream: newLivestream, 42 - }; 43 - } else if (PlaceStreamLivestream.isViewerCount(message)) { 44 - message = message as PlaceStreamLivestream.ViewerCount; 45 - state = { 46 - ...state, 47 - viewers: message.count, 48 - }; 49 - } else if (PlaceStreamChatDefs.isMessageView(message)) { 50 - message = message as PlaceStreamChatDefs.MessageView; 51 - // Explicitly map MessageView to MessageViewHydrated 52 - const hydrated: ChatMessageViewHydrated = { 53 - uri: message.uri, 54 - cid: message.cid, 55 - author: message.author, 56 - record: message.record as PlaceStreamChatMessage.Record, 57 - indexedAt: message.indexedAt, 58 - chatProfile: (message as any).chatProfile, 59 - replyTo: (message as any).replyTo, 60 - deleted: message.deleted, 61 - }; 62 - state = reduceChat(state, [hydrated], [], []); 63 - } else if (PlaceStreamSegment.isRecord(message)) { 64 - const newRecentSegments = [...state.recentSegments]; 65 - newRecentSegments.unshift(message); 66 - if (newRecentSegments.length > MAX_RECENT_SEGMENTS) { 67 - newRecentSegments.pop(); 68 - } 24 + if (message.$type === "place.stream.error") { 69 25 state = { 70 26 ...state, 71 - segment: message as PlaceStreamSegment.Record, 72 - recentSegments: newRecentSegments, 73 - problems: findProblems(newRecentSegments), 74 - }; 75 - } else if (PlaceStreamDefs.isBlockView(message)) { 76 - const block = message as PlaceStreamDefs.BlockView; 77 - state = reduceChat(state, [], [block], []); 78 - } else if (PlaceStreamDefs.isRenditions(message)) { 79 - message = message as PlaceStreamDefs.Renditions; 80 - state = { 81 - ...state, 82 - renditions: message.renditions, 83 - }; 84 - } else if (AppBskyActorDefs.isProfileViewBasic(message)) { 85 - state = { 86 - ...state, 87 - profile: message, 27 + problems: [ 28 + ...state.problems, 29 + { 30 + code: message.code, 31 + message: message.message, 32 + severity: "error", 33 + }, 34 + ], 88 35 }; 89 - } else if (PlaceStreamChatGate.isRecord(message)) { 90 - const hideRecord = message as PlaceStreamChatGate.Record; 91 - const hiddenMessageUri = hideRecord.hiddenMessage; 92 - const newPendingHides = [...state.pendingHides]; 93 - if (!newPendingHides.includes(hiddenMessageUri)) { 94 - newPendingHides.push(hiddenMessageUri); 36 + } else { 37 + if (!state.websocketConnected) { 38 + state = { 39 + ...state, 40 + websocketConnected: true, 41 + }; 95 42 } 96 43 97 - state = { 98 - ...state, 99 - pendingHides: newPendingHides, 100 - }; 101 - state = reduceChat(state, [], [], [hiddenMessageUri]); 44 + if (PlaceStreamLivestream.isLivestreamView(message)) { 45 + const newLivestream = message as LivestreamViewHydrated; 46 + const oldLivestream = state.livestream; 47 + 48 + // check if this is actually new 49 + if (!oldLivestream || oldLivestream.uri !== newLivestream.uri) { 50 + const streamTitle = newLivestream.record.title || "something cool!"; 51 + const systemMessage = SystemMessages.streamStart(streamTitle); 52 + // set proper times 53 + systemMessage.indexedAt = newLivestream.indexedAt; 54 + systemMessage.record.createdAt = newLivestream.record.createdAt; 55 + 56 + state = reduceChat(state, [systemMessage], []); 57 + } 58 + 59 + state = { 60 + ...state, 61 + livestream: newLivestream, 62 + }; 63 + } else if (PlaceStreamLivestream.isViewerCount(message)) { 64 + message = message as PlaceStreamLivestream.ViewerCount; 65 + state = { 66 + ...state, 67 + viewers: message.count, 68 + }; 69 + } else if (PlaceStreamChatDefs.isMessageView(message)) { 70 + message = message as PlaceStreamChatDefs.MessageView; 71 + // Explicitly map MessageView to MessageViewHydrated 72 + const hydrated: ChatMessageViewHydrated = { 73 + uri: message.uri, 74 + cid: message.cid, 75 + author: message.author, 76 + record: message.record as PlaceStreamChatMessage.Record, 77 + indexedAt: message.indexedAt, 78 + chatProfile: (message as any).chatProfile, 79 + replyTo: (message as any).replyTo, 80 + deleted: message.deleted, 81 + }; 82 + state = reduceChat(state, [hydrated], [], []); 83 + } else if (PlaceStreamSegment.isRecord(message)) { 84 + const newRecentSegments = [...state.recentSegments]; 85 + newRecentSegments.unshift(message); 86 + if (newRecentSegments.length > MAX_RECENT_SEGMENTS) { 87 + newRecentSegments.pop(); 88 + } 89 + state = { 90 + ...state, 91 + segment: message as PlaceStreamSegment.Record, 92 + recentSegments: newRecentSegments, 93 + problems: findProblems(newRecentSegments), 94 + hasReceivedSegment: true, 95 + }; 96 + } else if (PlaceStreamDefs.isBlockView(message)) { 97 + const block = message as PlaceStreamDefs.BlockView; 98 + state = reduceChat(state, [], [block], []); 99 + } else if (PlaceStreamDefs.isRenditions(message)) { 100 + message = message as PlaceStreamDefs.Renditions; 101 + state = { 102 + ...state, 103 + renditions: message.renditions, 104 + }; 105 + } else if (AppBskyActorDefs.isProfileViewBasic(message)) { 106 + state = { 107 + ...state, 108 + profile: message, 109 + }; 110 + } else if (PlaceStreamChatGate.isRecord(message)) { 111 + const hideRecord = message as PlaceStreamChatGate.Record; 112 + const hiddenMessageUri = hideRecord.hiddenMessage; 113 + const newPendingHides = [...state.pendingHides]; 114 + if (!newPendingHides.includes(hiddenMessageUri)) { 115 + newPendingHides.push(hiddenMessageUri); 116 + } 117 + 118 + state = { 119 + ...state, 120 + pendingHides: newPendingHides, 121 + }; 122 + state = reduceChat(state, [], [], [hiddenMessageUri]); 123 + } 102 124 } 103 125 } 104 126 return reduceChat(state, [], [], []);
+1
js/components/src/streamplace-store/stream.tsx
··· 108 108 }, 109 109 facets: rt.facets, 110 110 createdAt: now.toISOString(), 111 + langs: ["en"], 111 112 }; 112 113 record.embed = { 113 114 $type: "app.bsky.embed.external",
+1 -1
js/desktop/package.json
··· 1 1 { 2 2 "name": "streamplace-desktop", 3 3 "productName": "streamplace-desktop", 4 - "version": "0.8.0", 4 + "version": "0.8.17", 5 5 "description": "Streamplace Desktop Application", 6 6 "main": ".webpack/main", 7 7 "scripts": {
+1
js/desktop/src/tests/test-runner.ts
··· 69 69 testEnv.env = { 70 70 SP_HTTP_ADDR: `127.0.0.1:${randomPort()}`, 71 71 SP_HTTP_INTERNAL_ADDR: `127.0.0.1:${randomPort()}`, 72 + SP_RTMP_ADDR: `127.0.0.1:${randomPort()}`, 72 73 SP_DATA_DIR: tmpDir, 73 74 }; 74 75 }
+1 -1
js/docs/package.json
··· 1 1 { 2 2 "name": "streamplace-docs", 3 3 "type": "module", 4 - "version": "0.8.15", 4 + "version": "0.8.18", 5 5 "scripts": { 6 6 "dev": "astro dev --host 0.0.0.0 --port 38082", 7 7 "start": "astro dev --host 0.0.0.0 --port 38082",
+8 -6
js/docs/src/content/docs/guides/start-contributing/streamplace-dev-setup.md
··· 10 10 11 11 ## Prerequisites 12 12 13 - - [Node.js](https://nodejs.org/) 13 + Except for the C/C++ compilers, we'd highly recommend using 14 + [mise](https://mise.jdx.dev/) to get your workspace set up for development. 15 + 16 + - [Node.js](https://nodejs.org/) (version 22 [important!]) 14 17 - [pnpm](https://pnpm.io/) 15 - - A way to install it is with `npm install -g pnpm` if corepack is not enabled 16 - in your node install. 17 18 - Go (version 1.24) 18 - - If you use `mise`, you can install latest Go 1.24 with 19 - `mise install go@prefix:1.24` 19 + - Rust 20 20 - Meson 21 21 - Ninja 22 22 - pkg-config 23 - - Rust 24 23 - Working C and C++ compilers: `gcc` on Linux or `clang` (via Xcode) on macOS. 24 + - On most unix-like systems, a c/++ compiler is included with the distro's 25 + version of `build-essential`/`base-devel` (`xcode-select –-install` on 26 + macOS) 25 27 26 28 ## Get Started 27 29
+115
js/docs/src/content/docs/lex-reference/live/place-stream-live-getrecommendations.md
··· 1 + --- 2 + title: place.stream.live.getRecommendations 3 + description: Reference for the place.stream.live.getRecommendations lexicon 4 + --- 5 + 6 + **Lexicon Version:** 1 7 + 8 + ## Definitions 9 + 10 + <a name="main"></a> 11 + 12 + ### `main` 13 + 14 + **Type:** `query` 15 + 16 + Get the list of streamers recommended by a user 17 + 18 + **Parameters:** 19 + 20 + | Name | Type | Req'd | Description | Constraints | 21 + | --------- | -------- | ----- | -------------------------------------------------- | ------------- | 22 + | `userDID` | `string` | ✅ | The DID of the user whose recommendations to fetch | Format: `did` | 23 + 24 + **Output:** 25 + 26 + - **Encoding:** `application/json` 27 + - **Schema:** 28 + 29 + **Schema Type:** `object` 30 + 31 + | Name | Type | Req'd | Description | Constraints | 32 + | ----------------- | ------------------------------------------------------------------------------------------- | ----- | --------------------------------------- | ------------- | 33 + | `recommendations` | Array of Union of:<br/>&nbsp;&nbsp;[`#livestreamRecommendation`](#livestreamrecommendation) | ✅ | Ordered list of recommendations | | 34 + | `userDID` | `string` | ❌ | The user DID this recommendation is for | Format: `did` | 35 + 36 + --- 37 + 38 + <a name="livestreamrecommendation"></a> 39 + 40 + ### `livestreamRecommendation` 41 + 42 + **Type:** `object` 43 + 44 + **Properties:** 45 + 46 + | Name | Type | Req'd | Description | Constraints | 47 + | -------- | -------- | ----- | ----------------------------------- | ----------------------------------- | 48 + | `did` | `string` | ✅ | The DID of the recommended streamer | Format: `did` | 49 + | `source` | `string` | ✅ | Source of the recommendation | Enum: `streamer`, `follows`, `host` | 50 + 51 + --- 52 + 53 + ## Lexicon Source 54 + 55 + ```json 56 + { 57 + "lexicon": 1, 58 + "id": "place.stream.live.getRecommendations", 59 + "defs": { 60 + "main": { 61 + "type": "query", 62 + "description": "Get the list of streamers recommended by a user", 63 + "parameters": { 64 + "type": "params", 65 + "required": ["userDID"], 66 + "properties": { 67 + "userDID": { 68 + "type": "string", 69 + "format": "did", 70 + "description": "The DID of the user whose recommendations to fetch" 71 + } 72 + } 73 + }, 74 + "output": { 75 + "encoding": "application/json", 76 + "schema": { 77 + "type": "object", 78 + "required": ["recommendations"], 79 + "properties": { 80 + "recommendations": { 81 + "type": "array", 82 + "description": "Ordered list of recommendations", 83 + "items": { 84 + "type": "union", 85 + "refs": ["#livestreamRecommendation"] 86 + } 87 + }, 88 + "userDID": { 89 + "type": "string", 90 + "format": "did", 91 + "description": "The user DID this recommendation is for" 92 + } 93 + } 94 + } 95 + } 96 + }, 97 + "livestreamRecommendation": { 98 + "type": "object", 99 + "required": ["did", "source"], 100 + "properties": { 101 + "did": { 102 + "type": "string", 103 + "format": "did", 104 + "description": "The DID of the recommended streamer" 105 + }, 106 + "source": { 107 + "type": "string", 108 + "enum": ["streamer", "follows", "host"], 109 + "description": "Source of the recommendation" 110 + } 111 + } 112 + } 113 + } 114 + } 115 + ```
+64
js/docs/src/content/docs/lex-reference/live/place-stream-live-recommendations.md
··· 1 + --- 2 + title: place.stream.live.recommendations 3 + description: Reference for the place.stream.live.recommendations lexicon 4 + --- 5 + 6 + **Lexicon Version:** 1 7 + 8 + ## Definitions 9 + 10 + <a name="main"></a> 11 + 12 + ### `main` 13 + 14 + **Type:** `record` 15 + 16 + A list of recommended streamers, in order of preference 17 + 18 + **Record Key:** `literal:self` 19 + 20 + **Record Properties:** 21 + 22 + | Name | Type | Req'd | Description | Constraints | 23 + | ----------- | ----------------- | ----- | ----------------------------------------------------- | ----------------------------- | 24 + | `streamers` | Array of `string` | ✅ | Ordered list of recommended streamer DIDs | Min Items: 0<br/>Max Items: 8 | 25 + | `createdAt` | `string` | ✅ | Client-declared timestamp when this list was created. | Format: `datetime` | 26 + 27 + --- 28 + 29 + ## Lexicon Source 30 + 31 + ```json 32 + { 33 + "lexicon": 1, 34 + "id": "place.stream.live.recommendations", 35 + "defs": { 36 + "main": { 37 + "type": "record", 38 + "description": "A list of recommended streamers, in order of preference", 39 + "key": "literal:self", 40 + "record": { 41 + "type": "object", 42 + "required": ["streamers", "createdAt"], 43 + "properties": { 44 + "streamers": { 45 + "type": "array", 46 + "description": "Ordered list of recommended streamer DIDs", 47 + "items": { 48 + "type": "string", 49 + "format": "did" 50 + }, 51 + "maxLength": 8, 52 + "minLength": 0 53 + }, 54 + "createdAt": { 55 + "type": "string", 56 + "format": "datetime", 57 + "description": "Client-declared timestamp when this list was created." 58 + } 59 + } 60 + } 61 + } 62 + } 63 + } 64 + ```
+114
js/docs/src/content/docs/lex-reference/live/place-stream-live-searchactorstypeahead.md
··· 1 + --- 2 + title: place.stream.live.searchActorsTypeahead 3 + description: Reference for the place.stream.live.searchActorsTypeahead lexicon 4 + --- 5 + 6 + **Lexicon Version:** 1 7 + 8 + ## Definitions 9 + 10 + <a name="main"></a> 11 + 12 + ### `main` 13 + 14 + **Type:** `query` 15 + 16 + Find actor suggestions for a prefix search term. Expected use is for 17 + auto-completion during text field entry. 18 + 19 + **Parameters:** 20 + 21 + | Name | Type | Req'd | Description | Constraints | 22 + | ------- | --------- | ----- | --------------------------------------------- | ------------------------------------- | 23 + | `q` | `string` | ❌ | Search query prefix; not a full query string. | | 24 + | `limit` | `integer` | ❌ | | Min: 1<br/>Max: 100<br/>Default: `10` | 25 + 26 + **Output:** 27 + 28 + - **Encoding:** `application/json` 29 + - **Schema:** 30 + 31 + **Schema Type:** `object` 32 + 33 + | Name | Type | Req'd | Description | Constraints | 34 + | -------- | --------------------------- | ----- | ----------- | ----------- | 35 + | `actors` | Array of [`#actor`](#actor) | ✅ | | | 36 + 37 + --- 38 + 39 + <a name="actor"></a> 40 + 41 + ### `actor` 42 + 43 + **Type:** `object` 44 + 45 + **Properties:** 46 + 47 + | Name | Type | Req'd | Description | Constraints | 48 + | -------- | -------- | ----- | ------------------ | ---------------- | 49 + | `did` | `string` | ✅ | The actor's DID | Format: `did` | 50 + | `handle` | `string` | ✅ | The actor's handle | Format: `handle` | 51 + 52 + --- 53 + 54 + ## Lexicon Source 55 + 56 + ```json 57 + { 58 + "lexicon": 1, 59 + "id": "place.stream.live.searchActorsTypeahead", 60 + "defs": { 61 + "main": { 62 + "type": "query", 63 + "description": "Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry.", 64 + "parameters": { 65 + "type": "params", 66 + "properties": { 67 + "q": { 68 + "type": "string", 69 + "description": "Search query prefix; not a full query string." 70 + }, 71 + "limit": { 72 + "type": "integer", 73 + "minimum": 1, 74 + "maximum": 100, 75 + "default": 10 76 + } 77 + } 78 + }, 79 + "output": { 80 + "encoding": "application/json", 81 + "schema": { 82 + "type": "object", 83 + "required": ["actors"], 84 + "properties": { 85 + "actors": { 86 + "type": "array", 87 + "items": { 88 + "type": "ref", 89 + "ref": "#actor" 90 + } 91 + } 92 + } 93 + } 94 + } 95 + }, 96 + "actor": { 97 + "type": "object", 98 + "required": ["did", "handle"], 99 + "properties": { 100 + "did": { 101 + "type": "string", 102 + "format": "did", 103 + "description": "The actor's DID" 104 + }, 105 + "handle": { 106 + "type": "string", 107 + "format": "handle", 108 + "description": "The actor's handle" 109 + } 110 + } 111 + } 112 + } 113 + } 114 + ```
+134
js/docs/src/content/docs/lex-reference/openapi.json
··· 1033 1033 ] 1034 1034 } 1035 1035 }, 1036 + "/xrpc/place.stream.live.getRecommendations": { 1037 + "get": { 1038 + "summary": "Get the list of streamers recommended by a user", 1039 + "operationId": "place.stream.live.getRecommendations", 1040 + "tags": ["place.stream.live"], 1041 + "responses": { 1042 + "200": { 1043 + "description": "Success", 1044 + "content": { 1045 + "application/json": { 1046 + "schema": { 1047 + "type": "object", 1048 + "properties": { 1049 + "recommendations": { 1050 + "type": "array", 1051 + "description": "Ordered list of recommendations", 1052 + "items": { 1053 + "oneOf": [ 1054 + { 1055 + "$ref": "#/components/schemas/place.stream.live.getRecommendations_livestreamRecommendation" 1056 + } 1057 + ] 1058 + } 1059 + }, 1060 + "userDID": { 1061 + "type": "string", 1062 + "description": "The user DID this recommendation is for", 1063 + "format": "did" 1064 + } 1065 + }, 1066 + "required": ["recommendations"] 1067 + } 1068 + } 1069 + } 1070 + } 1071 + }, 1072 + "parameters": [ 1073 + { 1074 + "name": "userDID", 1075 + "in": "query", 1076 + "required": true, 1077 + "description": "The DID of the user whose recommendations to fetch", 1078 + "schema": { 1079 + "type": "string", 1080 + "description": "The DID of the user whose recommendations to fetch", 1081 + "format": "did" 1082 + } 1083 + } 1084 + ] 1085 + } 1086 + }, 1036 1087 "/xrpc/place.stream.live.getSegments": { 1037 1088 "get": { 1038 1089 "summary": "Get a list of livestream segments for a user", ··· 1088 1139 "schema": { 1089 1140 "type": "string", 1090 1141 "format": "date-time" 1142 + } 1143 + } 1144 + ] 1145 + } 1146 + }, 1147 + "/xrpc/place.stream.live.searchActorsTypeahead": { 1148 + "get": { 1149 + "summary": "Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry.", 1150 + "operationId": "place.stream.live.searchActorsTypeahead", 1151 + "tags": ["place.stream.live"], 1152 + "responses": { 1153 + "200": { 1154 + "description": "Success", 1155 + "content": { 1156 + "application/json": { 1157 + "schema": { 1158 + "type": "object", 1159 + "properties": { 1160 + "actors": { 1161 + "type": "array", 1162 + "items": { 1163 + "$ref": "#/components/schemas/place.stream.live.searchActorsTypeahead_actor" 1164 + } 1165 + } 1166 + }, 1167 + "required": ["actors"] 1168 + } 1169 + } 1170 + } 1171 + } 1172 + }, 1173 + "parameters": [ 1174 + { 1175 + "name": "q", 1176 + "in": "query", 1177 + "required": false, 1178 + "description": "Search query prefix; not a full query string.", 1179 + "schema": { 1180 + "type": "string", 1181 + "description": "Search query prefix; not a full query string." 1182 + } 1183 + }, 1184 + { 1185 + "name": "limit", 1186 + "in": "query", 1187 + "required": false, 1188 + "schema": { 1189 + "type": "integer", 1190 + "default": 10, 1191 + "minimum": 1, 1192 + "maximum": 100 1091 1193 } 1092 1194 } 1093 1195 ] ··· 2558 2660 }, 2559 2661 "required": ["count"] 2560 2662 }, 2663 + "place.stream.live.getRecommendations_livestreamRecommendation": { 2664 + "type": "object", 2665 + "properties": { 2666 + "did": { 2667 + "type": "string", 2668 + "description": "The DID of the recommended streamer", 2669 + "format": "did" 2670 + }, 2671 + "source": { 2672 + "type": "string", 2673 + "description": "Source of the recommendation", 2674 + "enum": ["streamer", "follows", "host"] 2675 + } 2676 + }, 2677 + "required": ["did", "source"] 2678 + }, 2561 2679 "place.stream.segment_segmentView": { 2562 2680 "type": "object", 2563 2681 "properties": { ··· 2568 2686 "record": {} 2569 2687 }, 2570 2688 "required": ["cid", "record"] 2689 + }, 2690 + "place.stream.live.searchActorsTypeahead_actor": { 2691 + "type": "object", 2692 + "properties": { 2693 + "did": { 2694 + "type": "string", 2695 + "description": "The actor's DID", 2696 + "format": "did" 2697 + }, 2698 + "handle": { 2699 + "type": "string", 2700 + "description": "The actor's handle", 2701 + "format": "handle" 2702 + } 2703 + }, 2704 + "required": ["did", "handle"] 2571 2705 }, 2572 2706 "place.stream.live.subscribeSegments_segment": { 2573 2707 "type": "string",
+37 -28
js/docs/src/content/docs/video-metadata/c2pa-integration.md
··· 17 17 18 18 ```json 19 19 { 20 - "active_manifest": "urn:c2pa:b23b55a7-bd34-4138-99d8-ce565fab3934", 20 + "active_manifest": "urn:c2pa:66130743-a4cd-4f73-a4c7-201079ef127a", 21 21 "manifests": { 22 - "urn:c2pa:b23b55a7-bd34-4138-99d8-ce565fab3934": { 22 + "urn:c2pa:66130743-a4cd-4f73-a4c7-201079ef127a": { 23 23 "claim_generator_info": [ 24 24 { 25 25 "name": "c2pa-rs", ··· 27 27 "org.contentauth.c2pa_rs": "0.58.0" 28 28 } 29 29 ], 30 - "title": "Livestream Segment at 2025-10-21T19:24:24.156Z", 31 - "instance_id": "xmp:iid:17f3177c-7cfe-4de2-9a23-019dcdb00559", 30 + "title": "Livestream Segment at 2025-11-13T20:41:30.499Z", 31 + "instance_id": "xmp:iid:a031a30d-e1eb-4548-a20f-6453e15c2ace", 32 32 "ingredients": [], 33 33 "assertions": [ 34 34 { ··· 36 36 "data": { 37 37 "actions": [ 38 38 { 39 - "action": "c2pa.created" 39 + "action": "c2pa.created", 40 + "when": "2025-11-13T20:41:30.499Z" 40 41 }, 41 42 { 42 - "action": "c2pa.published" 43 + "action": "c2pa.published", 44 + "when": "2025-11-13T20:41:30.499Z" 43 45 } 44 46 ], 45 47 "allActionsIncluded": false ··· 83 85 } 84 86 ], 85 87 "alg": "sha256", 86 - "hash": "HrLwGm+HdaZh9TkBiWhJH1Mo7QcvLgmhMThcG8f3qZc=", 88 + "hash": "ZHobn5CtL1NsTLAMMPT8D7koSrMr2Ijm5eJt73ED4HA=", 87 89 "name": "jumbf manifest" 88 90 } 89 91 }, 90 92 { 91 - "label": "place.stream.metadata", 93 + "label": "cawg.metadata", 92 94 "data": { 93 95 "@context": { 94 96 "photoshop": "http://ns.adobe.com/photoshop/1.0/", 95 - "dc": "http://purl.org/dc/elements/1.1/", 96 97 "xmpRights": "http://ns.adobe.com/xap/1.0/rights/", 97 - "Iptc4xmpExt": "http://iptc.org/std/Iptc4xmpExt/2008-02-29/" 98 + "Iptc4xmpExt": "http://iptc.org/std/Iptc4xmpExt/2008-02-29/", 99 + "dc": "http://purl.org/dc/elements/1.1/" 98 100 }, 99 - "dc:creator": "did:plc:y3lae7hmqiwyq7w2v3bcb2c2", 100 - "dc:title": ["🦎🦎"], 101 - "dc:date": ["2025-10-21T19:24:24.156Z"], 102 - "distributionPolicy": { 103 - "deleteAfter": "2025-10-21T19:29:24.000Z" 104 - }, 105 - "Iptc4xmpExt:LinkedEncRightsExpr": "http://creativecommons.org/publicdomain/zero/1.0/" 101 + "xmpRights:UsageTerms": "All rights reserved", 102 + "dc:creator": "did:plc:2j2ounbiyi3ftihronlw5qhj", 103 + "dc:date": "2025-11-13T20:41:30.499Z", 104 + "Iptc4xmpExt:ContentWarning": ["cwarn:flashingLights"], 105 + "dc:title": "Test" 106 106 }, 107 107 "kind": "Json" 108 108 }, ··· 111 111 "data": { 112 112 "$type": "place.stream.metadata.configuration", 113 113 "contentRights": { 114 - "license": "place.stream.metadata.contentRights#cc0_1__0" 114 + "license": "place.stream.metadata.contentRights#all-rights-reserved" 115 + }, 116 + "contentWarnings": { 117 + "warnings": ["cwarn:flashingLights"] 115 118 }, 116 119 "distributionPolicy": { 117 120 "deleteAfter": 300 ··· 121 124 { 122 125 "label": "place.stream.livestream", 123 126 "data": { 124 - "url": "https://picnic-labs-nicholas-not.trycloudflare.com", 127 + "url": "https://headphones-glad-thunder-guide.trycloudflare.com", 125 128 "post": { 126 - "cid": "bafyreicucf722xnyf74psia5ghd5usdnona4e7bkcgbqhdma2a6dokqh5m", 127 - "uri": "at://did:plc:y3lae7hmqiwyq7w2v3bcb2c2/app.bsky.feed.post/3lxyfybn55m2o" 129 + "cid": "bafyreigxivgp5rjgohv7y6z3r4wdjw6qbu56ozartttuvtydoth45app7e", 130 + "uri": "at://did:plc:2j2ounbiyi3ftihronlw5qhj/app.bsky.feed.post/3m2k26c2l4u2g" 128 131 }, 129 132 "$type": "place.stream.livestream", 133 + "agent": "@streamplace/components/0.7.35 (web, Firefox)", 130 134 "thumb": { 131 135 "ref": { 132 - "$link": "bafkreiauoc74hcintbaua7tvp233qbfl4iymiyocc5aclhyohkz3bdinty" 136 + "$link": "bafkreihzmf7rywllxelclvnweuefl2bpqkkazznflnhdh24aexnqzh4xum" 133 137 }, 134 - "size": 9231, 138 + "size": 60106, 135 139 "$type": "blob", 136 140 "mimeType": "image/jpeg" 137 141 }, 138 - "title": "🦎🦎", 139 - "createdAt": "2025-10-06T16:25:06.950Z" 142 + "title": "Test", 143 + "createdAt": "2025-10-06T16:35:03.719Z", 144 + "canonicalUrl": "https://headphones-glad-thunder-guide.trycloudflare.com/makeworld.space" 140 145 } 141 146 } 142 147 ], 143 148 "signature_info": { 144 149 "issuer": "Streamplace", 145 - "common_name": "did:key:zQ3shfmFgwDstMiGaAkS4HhMJ7p3pTVhyLTHz9ABbhd4v4KJn", 146 - "cert_serial_number": "54472225560857906834076190516168844896" 150 + "common_name": "did:key:zQ3shiYS17LRhT7x6mfd6HfsHHzz1aD9DpGJUP3aT5f2ghdAy", 151 + "cert_serial_number": "34409281865771434870888029768053492928" 147 152 }, 148 - "label": "urn:c2pa:b23b55a7-bd34-4138-99d8-ce565fab3934" 153 + "label": "urn:c2pa:66130743-a4cd-4f73-a4c7-201079ef127a" 149 154 } 150 155 } 151 156 } ··· 171 176 segments, such as content warnings. However, not everything Streamplace does 172 177 fits neatly into C2PA-compliant metadata, so the primary source of truth for 173 178 metadata on a Streamplace segment remains the `place.stream` assertions. 179 + 180 + You can see existing metadata standards being used in the `cawg.metadata` 181 + assertion, which is an assertion defined outside of the C2PA spec for extra 182 + metadata, by the [Creator Assertions Working Group](https://cawg.io/). 174 183 175 184 ## Code paths 176 185
+14 -14
js/docs/src/content/docs/video-metadata/metadata-record.md
··· 11 11 This record is created by users through the Streamplace frontend and contains 12 12 three main components: 13 13 14 - 1. **Content Warnings** (`place.stream.metadata.contentWarnings`): Users can 15 - select content warnings to indicate to node operators and viewers what types 16 - of warnings have been disclosed. The system supports ten predefined warning 17 - categories including _violence_, _nudity_, _flashing lights_, _language_, 18 - _drug use_, _death_, _sexuality_, _suffering_, _fantasy violence_, and 19 - _personally identifiable information (PII)_. These categories are based on 20 - the 14 + 1. **Content Warnings** (`place.stream.metadata.configuration.contentWarnings`): 15 + Users can select content warnings to indicate to node operators and viewers 16 + what types of warnings have been disclosed. The system supports ten 17 + predefined warning categories including _violence_, _nudity_, _flashing 18 + lights_, _language_, _drug use_, _death_, _sexuality_, _suffering_, _fantasy 19 + violence_, and _personally identifiable information (PII)_. These categories 20 + are based on the 21 21 [IPTC controlled vocabulary for content warnings](https://cv.iptc.org/newscodes/contentwarning/). 22 22 Each warning provides descriptions to help creators properly categorize their 23 23 content. Streamplace node operators may also configure their nodes to exclude 24 24 certain types of content. 25 - 2. **Content Rights** (`place.stream.metadata.contentRights`): This section 26 - captures copyright and attribution information, including the creator’s name, 27 - copyright notice, publication year, license type, and credit line. The system 28 - supports various pre-defined licensing options from several Creative Commons 29 - licenses (CC0, CC-BY, CC-BY-SA, CC-BY-NC, CC-BY-NC-SA, CC-BY-ND, and 30 - CC-BY-NC-ND) to “All Rights Reserved”, as well as the option to input custom 31 - licensing terms. 25 + 2. **Content Rights** (`place.stream.metadata.configuration.contentRights`): 26 + This section captures copyright and attribution information, including the 27 + creator’s name, copyright notice, publication year, license type, and credit 28 + line. The system supports various pre-defined licensing options from several 29 + Creative Commons licenses (CC0, CC-BY, CC-BY-SA, CC-BY-NC, CC-BY-NC-SA, 30 + CC-BY-ND, and CC-BY-NC-ND) to “All Rights Reserved”, as well as the option to 31 + input custom licensing terms. 32 32 3. **Distribution Policy** (`place.stream.metadata.distributionPolicy`): This 33 33 section currently allows creators to specify a `deleteAfter` property, which 34 34 is meant to indicate the time after which the user no longer wants the stream
+1 -1
lerna.json
··· 1 1 { 2 2 "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 - "version": "0.8.15", 3 + "version": "0.8.18", 4 4 "npmClient": "pnpm" 5 5 }
+59
lexicons/place/stream/live/getRecommendations.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.stream.live.getRecommendations", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get the list of streamers recommended by a user", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["userDID"], 11 + "properties": { 12 + "userDID": { 13 + "type": "string", 14 + "format": "did", 15 + "description": "The DID of the user whose recommendations to fetch" 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "required": ["recommendations"], 24 + "properties": { 25 + "recommendations": { 26 + "type": "array", 27 + "description": "Ordered list of recommendations", 28 + "items": { 29 + "type": "union", 30 + "refs": ["#livestreamRecommendation"] 31 + } 32 + }, 33 + "userDID": { 34 + "type": "string", 35 + "format": "did", 36 + "description": "The user DID this recommendation is for" 37 + } 38 + } 39 + } 40 + } 41 + }, 42 + "livestreamRecommendation": { 43 + "type": "object", 44 + "required": ["did", "source"], 45 + "properties": { 46 + "did": { 47 + "type": "string", 48 + "format": "did", 49 + "description": "The DID of the recommended streamer" 50 + }, 51 + "source": { 52 + "type": "string", 53 + "enum": ["streamer", "follows", "host"], 54 + "description": "Source of the recommendation" 55 + } 56 + } 57 + } 58 + } 59 + }
+32
lexicons/place/stream/live/recommendations.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.stream.live.recommendations", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A list of recommended streamers, in order of preference", 8 + "key": "literal:self", 9 + "record": { 10 + "type": "object", 11 + "required": ["streamers", "createdAt"], 12 + "properties": { 13 + "streamers": { 14 + "type": "array", 15 + "description": "Ordered list of recommended streamer DIDs", 16 + "items": { 17 + "type": "string", 18 + "format": "did" 19 + }, 20 + "maxLength": 8, 21 + "minLength": 0 22 + }, 23 + "createdAt": { 24 + "type": "string", 25 + "format": "datetime", 26 + "description": "Client-declared timestamp when this list was created." 27 + } 28 + } 29 + } 30 + } 31 + } 32 + }
+57
lexicons/place/stream/live/searchActorsTypeahead.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.stream.live.searchActorsTypeahead", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "q": { 12 + "type": "string", 13 + "description": "Search query prefix; not a full query string." 14 + }, 15 + "limit": { 16 + "type": "integer", 17 + "minimum": 1, 18 + "maximum": 100, 19 + "default": 10 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": ["actors"], 28 + "properties": { 29 + "actors": { 30 + "type": "array", 31 + "items": { 32 + "type": "ref", 33 + "ref": "#actor" 34 + } 35 + } 36 + } 37 + } 38 + } 39 + }, 40 + "actor": { 41 + "type": "object", 42 + "required": ["did", "handle"], 43 + "properties": { 44 + "did": { 45 + "type": "string", 46 + "format": "did", 47 + "description": "The actor's DID" 48 + }, 49 + "handle": { 50 + "type": "string", 51 + "format": "handle", 52 + "description": "The actor's handle" 53 + } 54 + } 55 + } 56 + } 57 + }
+7
mise.toml
··· 1 + [tools] 2 + go = "latest" 3 + rust = "latest" 4 + node = "22" 5 + meson = "latest" 6 + ninja = "latest" 7 + pnpm = "latest"
+6
pkg/api/api.go
··· 79 79 HTTPRedirectTLSPort *int 80 80 sessions map[string]map[string]time.Time 81 81 sessionsLock sync.RWMutex 82 + 83 + rtmpSessions map[string]*media.RTMPSession 84 + rtmpSessionsLock sync.Mutex 85 + rtmpInternalPlaybackAddr string 82 86 } 83 87 84 88 type WebsocketTracker struct { ··· 109 113 op: op, 110 114 sessions: make(map[string]map[string]time.Time), 111 115 sessionsLock: sync.RWMutex{}, 116 + rtmpSessions: make(map[string]*media.RTMPSession), 117 + rtmpSessionsLock: sync.Mutex{}, 112 118 } 113 119 a.Mimes, err = updater.GetMimes() 114 120 if err != nil {
+346
pkg/api/rtmp_server.go
··· 1 + // Package main contains an example. 2 + package api 3 + 4 + import ( 5 + "context" 6 + "crypto/tls" 7 + "fmt" 8 + "net" 9 + "strings" 10 + "time" 11 + 12 + "github.com/bluenviron/gortmplib" 13 + "github.com/bluenviron/gortsplib/v5/pkg/format" 14 + "golang.org/x/sync/errgroup" 15 + "stream.place/streamplace/pkg/config" 16 + "stream.place/streamplace/pkg/log" 17 + "stream.place/streamplace/pkg/media" 18 + ) 19 + 20 + // This example shows how to: 21 + // 1. create a RTMP server 22 + // 2. accept a stream from a reader. 23 + // 3. broadcast the stream to readers. 24 + 25 + var RTMPTimeout = 10 * time.Second 26 + 27 + const RTMPPrefix = "/live/" 28 + 29 + func (a *StreamplaceAPI) HandleRTMPPublisher(ctx context.Context, sc *gortmplib.ServerConn) error { 30 + err := sc.RW.(net.Conn).SetReadDeadline(time.Now().Add(RTMPTimeout)) 31 + if err != nil { 32 + return err 33 + } 34 + 35 + if !strings.HasPrefix(sc.URL.Path, RTMPPrefix) { 36 + return fmt.Errorf("RTMP publisher is not allowed to publish to %s (must start with %s)", sc.URL.String(), RTMPPrefix) 37 + } 38 + streamKey := strings.TrimPrefix(sc.URL.Path, RTMPPrefix) 39 + mediaSigner, err := a.MakeMediaSigner(ctx, streamKey) 40 + if err != nil { 41 + return fmt.Errorf("failed to make media signer: %w", err) 42 + } 43 + 44 + streamer := mediaSigner.Streamer() 45 + ctx = log.WithLogValues(ctx, "streamer", streamer) 46 + session := &media.RTMPSession{ 47 + EventChan: make(chan any, 1024), 48 + MediaSigner: mediaSigner, 49 + } 50 + a.rtmpSessionsLock.Lock() 51 + a.rtmpSessions[streamer] = session 52 + a.rtmpSessionsLock.Unlock() 53 + 54 + defer func() { 55 + a.rtmpSessionsLock.Lock() 56 + delete(a.rtmpSessions, streamer) 57 + a.rtmpSessionsLock.Unlock() 58 + close(session.EventChan) 59 + }() 60 + 61 + r := &gortmplib.Reader{ 62 + Conn: sc, 63 + } 64 + err = r.Initialize() 65 + if err != nil { 66 + return err 67 + } 68 + 69 + for _, track := range r.Tracks() { 70 + log.Log(ctx, "get track", "track", track) 71 + 72 + switch track := track.(type) { 73 + case *format.H264: 74 + session.VideoTrack = track 75 + r.OnDataH264(track, func(pts time.Duration, dts time.Duration, au [][]byte) { 76 + // log.Log(ctx, "got H264", "len", len(au), "pts", pts, "dts", dts) 77 + session.EventChan <- &media.RTMPH264Data{ 78 + AU: au, 79 + PTS: pts, 80 + DTS: dts, 81 + } 82 + }) 83 + 84 + case *format.MPEG4Audio: 85 + session.AudioTrack = track 86 + r.OnDataMPEG4Audio(track, func(pts time.Duration, au []byte) { 87 + // log.Log(ctx, "got MPEG4Au", "len", len(au), "pts", pts) 88 + session.EventChan <- &media.RTMPAACData{ 89 + AU: au, 90 + PTS: pts, 91 + } 92 + }) 93 + 94 + default: 95 + return fmt.Errorf("unsupported track type: %T", track) 96 + } 97 + } 98 + 99 + g, ctx := errgroup.WithContext(ctx) 100 + g.Go(func() error { 101 + for { 102 + if ctx.Err() != nil { 103 + return ctx.Err() 104 + } 105 + err = sc.RW.(net.Conn).SetReadDeadline(time.Now().Add(RTMPTimeout)) 106 + if err != nil { 107 + return err 108 + } 109 + err = r.Read() 110 + if err != nil { 111 + return err 112 + } 113 + } 114 + }) 115 + 116 + g.Go(func() error { 117 + return a.MediaManager.RTMPIngest(ctx, fmt.Sprintf("rtmp://%s/live/%s", a.rtmpInternalPlaybackAddr, streamer), mediaSigner) 118 + }) 119 + 120 + return g.Wait() 121 + } 122 + 123 + func (a *StreamplaceAPI) HandleRTMPPlayback(ctx context.Context, sc *gortmplib.ServerConn) error { 124 + if !strings.HasPrefix(sc.URL.Path, RTMPPrefix) { 125 + return fmt.Errorf("RTMP publisher is not allowed to publish to %s (must start with %s)", sc.URL.String(), RTMPPrefix) 126 + } 127 + streamer := strings.TrimPrefix(sc.URL.Path, RTMPPrefix) 128 + a.rtmpSessionsLock.Lock() 129 + session, ok := a.rtmpSessions[streamer] 130 + a.rtmpSessionsLock.Unlock() 131 + if !ok { 132 + return fmt.Errorf("RTMP session not found for streamer %s", streamer) 133 + } 134 + 135 + w := &gortmplib.Writer{ 136 + Conn: sc, 137 + Tracks: []format.Format{session.VideoTrack, session.AudioTrack}, 138 + } 139 + err := w.Initialize() 140 + if err != nil { 141 + return err 142 + } 143 + for { 144 + select { 145 + case <-ctx.Done(): 146 + return ctx.Err() 147 + case event := <-session.EventChan: 148 + if event == nil { 149 + return fmt.Errorf("RTMP session closed") 150 + } 151 + switch event := event.(type) { 152 + case *media.RTMPH264Data: 153 + err := w.WriteH264(session.VideoTrack, event.PTS, event.DTS, event.AU) 154 + if err != nil { 155 + return fmt.Errorf("error writing H264: %w", err) 156 + } 157 + case *media.RTMPAACData: 158 + err := w.WriteMPEG4Audio(session.AudioTrack, event.PTS, event.AU) 159 + if err != nil { 160 + return fmt.Errorf("error writing MPEG4Audio: %w", err) 161 + } 162 + default: 163 + return fmt.Errorf("unsupported event type: %T", event) 164 + } 165 + } 166 + } 167 + } 168 + 169 + func (a *StreamplaceAPI) HandleRTMPPublishConn(ctx context.Context, conn net.Conn) error { 170 + err := conn.SetReadDeadline(time.Now().Add(RTMPTimeout)) 171 + if err != nil { 172 + return err 173 + } 174 + 175 + sc := &gortmplib.ServerConn{ 176 + RW: conn, 177 + } 178 + err = sc.Initialize() 179 + if err != nil { 180 + return err 181 + } 182 + 183 + err = sc.Accept() 184 + if err != nil { 185 + return err 186 + } 187 + 188 + if sc.Publish { 189 + return a.HandleRTMPPublisher(ctx, sc) 190 + } 191 + return fmt.Errorf("RTMP playback is not allowed") 192 + } 193 + 194 + func (a *StreamplaceAPI) HandleRTMPPlaybackConn(ctx context.Context, conn net.Conn) error { 195 + err := conn.SetReadDeadline(time.Now().Add(RTMPTimeout)) 196 + if err != nil { 197 + return err 198 + } 199 + 200 + sc := &gortmplib.ServerConn{ 201 + RW: conn, 202 + } 203 + err = sc.Initialize() 204 + if err != nil { 205 + return err 206 + } 207 + 208 + err = sc.Accept() 209 + if err != nil { 210 + return err 211 + } 212 + 213 + if !sc.Publish { 214 + return a.HandleRTMPPlayback(ctx, sc) 215 + } 216 + return fmt.Errorf("RTMP playback is not allowed") 217 + } 218 + 219 + func (a *StreamplaceAPI) ServeRTMP(ctx context.Context) error { 220 + ln, err := net.Listen("tcp", a.CLI.RTMPAddr) 221 + if err != nil { 222 + return fmt.Errorf("failed to listen: %w", err) 223 + } 224 + defer ln.Close() 225 + 226 + go func() { 227 + <-ctx.Done() 228 + ln.Close() 229 + }() 230 + 231 + log.Log(ctx, "rtmp server starting", "addr", a.CLI.RTMPAddr) 232 + 233 + g, ctx := errgroup.WithContext(ctx) 234 + g.Go(func() error { 235 + return a.ServeRTMPInternalPlayback(ctx) 236 + }) 237 + g.Go(func() error { 238 + for { 239 + if ctx.Err() != nil { 240 + return ctx.Err() 241 + } 242 + conn, err := ln.Accept() 243 + if err != nil { 244 + return fmt.Errorf("error accepting RTMP connection: %w", err) 245 + } 246 + go func() { 247 + err := a.HandleRTMPPublishConn(ctx, conn) 248 + if err != nil { 249 + log.Error(ctx, "error handling RTMP publish connection", "error", err) 250 + } 251 + }() 252 + } 253 + }) 254 + 255 + return g.Wait() 256 + } 257 + 258 + // Serve RTMP internal playback server for gstreamer to pull from 259 + func (a *StreamplaceAPI) ServeRTMPInternalPlayback(ctx context.Context) error { 260 + ln, err := net.Listen("tcp", "127.0.0.1:0") 261 + if err != nil { 262 + return fmt.Errorf("failed to listen: %w", err) 263 + } 264 + addr := ln.Addr().String() 265 + defer ln.Close() 266 + 267 + _, port, err := net.SplitHostPort(addr) 268 + if err != nil { 269 + return fmt.Errorf("failed to split host and port: %w", err) 270 + } 271 + 272 + go func() { 273 + <-ctx.Done() 274 + ln.Close() 275 + }() 276 + 277 + a.rtmpInternalPlaybackAddr = fmt.Sprintf("127.0.0.1:%s", port) 278 + 279 + log.Log(ctx, "rtmp internal playback server starting", "addr", a.rtmpInternalPlaybackAddr) 280 + 281 + // Accept loop in a goroutine so we can select on context.Done 282 + for { 283 + if ctx.Err() != nil { 284 + return ctx.Err() 285 + } 286 + conn, err := ln.Accept() 287 + if err != nil { 288 + return fmt.Errorf("error accepting RTMP connection: %w", err) 289 + } 290 + 291 + go func() { 292 + err := a.HandleRTMPPlaybackConn(ctx, conn) 293 + if err != nil { 294 + log.Error(ctx, "error handling RTMP internal playback connection", "error", err) 295 + } 296 + }() 297 + } 298 + } 299 + 300 + func (a *StreamplaceAPI) ServeRTMPS(ctx context.Context, cli *config.CLI) error { 301 + cert, err := tls.LoadX509KeyPair(cli.TLSCertPath, cli.TLSKeyPath) 302 + if err != nil { 303 + return fmt.Errorf("failed to load TLS certificate: %w", err) 304 + } 305 + 306 + tlsConfig := &tls.Config{ 307 + Certificates: []tls.Certificate{cert}, 308 + MinVersion: tls.VersionTLS12, 309 + } 310 + 311 + ln, err := tls.Listen("tcp", cli.RTMPSAddr, tlsConfig) 312 + if err != nil { 313 + return fmt.Errorf("failed to create RTMPS listener: %w", err) 314 + } 315 + 316 + log.Log(ctx, "rtmps server starting", "addr", cli.RTMPAddr) 317 + 318 + go func() { 319 + <-ctx.Done() 320 + ln.Close() 321 + }() 322 + 323 + g, ctx := errgroup.WithContext(ctx) 324 + g.Go(func() error { 325 + return a.ServeRTMPInternalPlayback(ctx) 326 + }) 327 + g.Go(func() error { 328 + for { 329 + if ctx.Err() != nil { 330 + return ctx.Err() 331 + } 332 + conn, err := ln.Accept() 333 + if err != nil { 334 + return fmt.Errorf("error accepting RTMP connection: %w", err) 335 + } 336 + go func() { 337 + err := a.HandleRTMPPublishConn(ctx, conn) 338 + if err != nil { 339 + log.Error(ctx, "error handling RTMP publish connection", "error", err) 340 + } 341 + }() 342 + } 343 + }) 344 + 345 + return g.Wait() 346 + }
+1
pkg/atproto/firehose.go
··· 161 161 constants.APP_BSKY_GRAPH_FOLLOW, 162 162 constants.APP_BSKY_FEED_POST, 163 163 constants.APP_BSKY_GRAPH_BLOCK, 164 + constants.PLACE_STREAM_LIVE_RECOMMENDATIONS, 164 165 } 165 166 166 167 func (atsync *ATProtoSynchronizer) handleCommitEventOps(ctx context.Context, evt *comatproto.SyncSubscribeRepos_Commit) {
+33
pkg/atproto/sync.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "encoding/json" 5 6 "errors" 6 7 "fmt" 7 8 "reflect" ··· 447 448 // Publish moderation permission view to WebSocket bus for real-time updates 448 449 // This allows moderators to see their permissions instantly without page refresh 449 450 go atsync.Bus.Publish(userDID, view) 451 + 452 + case *streamplace.LiveRecommendations: 453 + log.Debug(ctx, "creating recommendations", "userDID", userDID, "count", len(rec.Streamers)) 454 + 455 + // Validate max 8 streamers 456 + if len(rec.Streamers) > 8 { 457 + log.Warn(ctx, "recommendations exceed maximum of 8", "count", len(rec.Streamers)) 458 + return fmt.Errorf("maximum 8 recommendations allowed, got %d", len(rec.Streamers)) 459 + } 460 + 461 + // Marshal streamers to JSON 462 + streamersJSON, err := json.Marshal(rec.Streamers) 463 + if err != nil { 464 + return fmt.Errorf("failed to marshal streamers: %w", err) 465 + } 466 + 467 + // Parse createdAt timestamp 468 + createdAt, err := time.Parse(time.RFC3339, rec.CreatedAt) 469 + if err != nil { 470 + return fmt.Errorf("failed to parse createdAt: %w", err) 471 + } 472 + 473 + recommendation := &model.Recommendation{ 474 + UserDID: userDID, 475 + Streamers: json.RawMessage(streamersJSON), 476 + CreatedAt: createdAt, 477 + } 478 + 479 + err = atsync.Model.UpsertRecommendation(recommendation) 480 + if err != nil { 481 + return fmt.Errorf("failed to upsert recommendation: %w", err) 482 + } 450 483 451 484 default: 452 485 log.Debug(ctx, "unhandled record type", "type", reflect.TypeOf(rec))
+8 -1
pkg/cmd/streamplace.go
··· 411 411 }) 412 412 if cli.RTMPServerAddon != "" { 413 413 group.Go(func() error { 414 - return rtmps.ServeRTMPS(ctx, &cli) 414 + return rtmps.ServeRTMPSAddon(ctx, &cli) 415 + }) 416 + } else { 417 + group.Go(func() error { 418 + return a.ServeRTMPS(ctx, &cli) 415 419 }) 416 420 } 417 421 } else { 418 422 group.Go(func() error { 419 423 return a.ServeHTTP(ctx) 424 + }) 425 + group.Go(func() error { 426 + return a.ServeRTMP(ctx) 420 427 }) 421 428 } 422 429
+90 -85
pkg/config/config.go
··· 52 52 } 53 53 54 54 type CLI struct { 55 - AdminAccount string 56 - Build *BuildFlags 57 - DataDir string 58 - DBURL string 59 - EthAccountAddr string 60 - EthKeystorePath string 61 - EthPassword string 62 - FirebaseServiceAccount string 63 - FirebaseServiceAccountFile string 64 - GitLabURL string 65 - HTTPAddr string 66 - HTTPInternalAddr string 67 - HTTPSAddr string 68 - RtmpsAddr string 69 - Secure bool 70 - NoMist bool 71 - MistAdminPort int 72 - MistHTTPPort int 73 - MistRTMPPort int 74 - SigningKeyPath string 75 - TAURL string 76 - TLSCertPath string 77 - TLSKeyPath string 78 - PKCS11ModulePath string 79 - PKCS11Pin string 80 - PKCS11TokenSlot string 81 - PKCS11TokenLabel string 82 - PKCS11TokenSerial string 83 - PKCS11KeypairLabel string 84 - PKCS11KeypairID string 85 - StreamerName string 86 - RelayHost string 87 - Debug map[string]map[string]int 88 - AllowedStreams []string 89 - WideOpen bool 90 - Peers []string 91 - Redirects []string 92 - TestStream bool 93 - FrontendProxy string 94 - PublicOAuth bool 95 - AppBundleID string 96 - NoFirehose bool 97 - PrintChat bool 98 - Color string 99 - LivepeerGatewayURL string 100 - LivepeerGateway bool 101 - WHIPTest string 102 - Thumbnail bool 103 - SmearAudio bool 104 - ExternalSigning bool 105 - RTMPServerAddon string 106 - TracingEndpoint string 107 - BroadcasterHost string 108 - XXDeprecatedPublicHost string 109 - ServerHost string 110 - RateLimitPerSecond int 111 - RateLimitBurst int 112 - RateLimitWebsocket int 113 - JWK jwk.Key 114 - AccessJWK jwk.Key 115 - dataDirFlags []*string 116 - DiscordWebhooks []*discordtypes.Webhook 117 - NewWebRTCPlayback bool 118 - AppleTeamID string 119 - AndroidCertFingerprint string 120 - Labelers []string 121 - AtprotoDID string 122 - LivepeerHelp bool 123 - PLCURL string 124 - ContentFilters *ContentFilters 125 - SQLLogging bool 126 - SentryDSN string 127 - LivepeerDebug bool 128 - Tickets []string 129 - IrohTopic string 130 - DID string 131 - DisableIrohRelay bool 132 - DevAccountCreds map[string]string 133 - StreamSessionTimeout time.Duration 134 - Replicators []string 135 - WebsocketURL string 136 - BehindHTTPSProxy bool 137 - SegmentDebugDir string 138 - Syndicate []string 55 + AdminAccount string 56 + Build *BuildFlags 57 + DataDir string 58 + DBURL string 59 + EthAccountAddr string 60 + EthKeystorePath string 61 + EthPassword string 62 + FirebaseServiceAccount string 63 + FirebaseServiceAccountFile string 64 + GitLabURL string 65 + HTTPAddr string 66 + HTTPInternalAddr string 67 + HTTPSAddr string 68 + RTMPAddr string 69 + RTMPSAddr string 70 + Secure bool 71 + NoMist bool 72 + MistAdminPort int 73 + MistHTTPPort int 74 + MistRTMPPort int 75 + SigningKeyPath string 76 + TAURL string 77 + TLSCertPath string 78 + TLSKeyPath string 79 + PKCS11ModulePath string 80 + PKCS11Pin string 81 + PKCS11TokenSlot string 82 + PKCS11TokenLabel string 83 + PKCS11TokenSerial string 84 + PKCS11KeypairLabel string 85 + PKCS11KeypairID string 86 + StreamerName string 87 + RelayHost string 88 + Debug map[string]map[string]int 89 + AllowedStreams []string 90 + WideOpen bool 91 + Peers []string 92 + Redirects []string 93 + TestStream bool 94 + FrontendProxy string 95 + PublicOAuth bool 96 + AppBundleID string 97 + NoFirehose bool 98 + PrintChat bool 99 + Color string 100 + LivepeerGatewayURL string 101 + LivepeerGateway bool 102 + WHIPTest string 103 + Thumbnail bool 104 + SmearAudio bool 105 + ExternalSigning bool 106 + RTMPServerAddon string 107 + TracingEndpoint string 108 + BroadcasterHost string 109 + XXDeprecatedPublicHost string 110 + ServerHost string 111 + RateLimitPerSecond int 112 + RateLimitBurst int 113 + RateLimitWebsocket int 114 + JWK jwk.Key 115 + AccessJWK jwk.Key 116 + dataDirFlags []*string 117 + DiscordWebhooks []*discordtypes.Webhook 118 + NewWebRTCPlayback bool 119 + AppleTeamID string 120 + AndroidCertFingerprint string 121 + Labelers []string 122 + AtprotoDID string 123 + LivepeerHelp bool 124 + PLCURL string 125 + ContentFilters *ContentFilters 126 + DefaultRecommendedStreamers []string 127 + SQLLogging bool 128 + SentryDSN string 129 + LivepeerDebug bool 130 + Tickets []string 131 + IrohTopic string 132 + DID string 133 + DisableIrohRelay bool 134 + DevAccountCreds map[string]string 135 + StreamSessionTimeout time.Duration 136 + Replicators []string 137 + WebsocketURL string 138 + BehindHTTPSProxy bool 139 + SegmentDebugDir string 140 + Syndicate []string 139 141 } 140 142 141 143 // ContentFilters represents the content filtering configuration ··· 203 205 fs.StringVar(&cli.ServerHost, "server-host", "", "public host for this particular physical streamplace node. defaults to broadcaster-host and only must be set for multi-node broadcasters") 204 206 fs.BoolVar(&cli.Thumbnail, "thumbnail", true, "enable thumbnail generation") 205 207 fs.BoolVar(&cli.SmearAudio, "smear-audio", false, "enable audio smearing to create 'perfect' segment timestamps") 208 + 206 209 fs.StringVar(&cli.TracingEndpoint, "tracing-endpoint", "", "gRPC endpoint to send traces to") 207 210 fs.IntVar(&cli.RateLimitPerSecond, "rate-limit-per-second", 0, "rate limit for requests per second per ip") 208 211 fs.IntVar(&cli.RateLimitBurst, "rate-limit-burst", 0, "rate limit burst for requests per ip") 209 212 fs.IntVar(&cli.RateLimitWebsocket, "rate-limit-websocket", 10, "number of concurrent websocket connections allowed per ip") 210 213 fs.StringVar(&cli.RTMPServerAddon, "rtmp-server-addon", "", "address of external RTMP server to forward streams to") 211 - fs.StringVar(&cli.RtmpsAddr, "rtmps-addr", ":1935", "address to listen for RTMPS connections") 214 + fs.StringVar(&cli.RTMPSAddr, "rtmps-addr", ":1935", "address to listen for RTMPS connections (when --secure=true)") 215 + fs.StringVar(&cli.RTMPAddr, "rtmp-addr", ":1935", "address to listen for RTMP connections (when --secure=false)") 212 216 cli.JSONFlag(fs, &cli.DiscordWebhooks, "discord-webhooks", "[]", "JSON array of Discord webhooks to send notifications to") 213 217 fs.BoolVar(&cli.NewWebRTCPlayback, "new-webrtc-playback", true, "enable new webrtc playback") 214 218 fs.StringVar(&cli.AppleTeamID, "apple-team-id", "", "apple team id for deep linking") ··· 216 220 cli.StringSliceFlag(fs, &cli.Labelers, "labelers", []string{}, "did of labelers that this instance should subscribe to") 217 221 fs.StringVar(&cli.AtprotoDID, "atproto-did", "", "atproto did to respond to on /.well-known/atproto-did (default did:web:PUBLIC_HOST)") 218 222 cli.JSONFlag(fs, &cli.ContentFilters, "content-filters", "{}", "JSON content filtering rules") 223 + cli.StringSliceFlag(fs, &cli.DefaultRecommendedStreamers, "default-recommended-streamers", []string{}, "comma-separated list of streamer DIDs to recommend by default when no other recommendations are available") 219 224 fs.BoolVar(&cli.LivepeerHelp, "livepeer-help", false, "print help for livepeer flags and exit") 220 225 fs.StringVar(&cli.PLCURL, "plc-url", "https://plc.directory", "url of the plc directory") 221 226 fs.BoolVar(&cli.SQLLogging, "sql-logging", false, "enable sql logging")
+1 -3
pkg/constants/constants.go
··· 12 12 var APP_BSKY_GRAPH_BLOCK = "app.bsky.graph.block" //nolint:all 13 13 var PLACE_STREAM_CHAT_GATE = "place.stream.chat.gate" //nolint:all 14 14 var PLACE_STREAM_DEFAULT_METADATA = "place.stream.metadata.configuration" //nolint:all 15 + var PLACE_STREAM_LIVE_RECOMMENDATIONS = "place.stream.live.recommendations" //nolint:all 15 16 16 17 const DID_KEY_PREFIX = "did:key" //nolint:all 17 18 const ADDRESS_KEY_PREFIX = "0x" //nolint:all 18 - 19 - // Streamplace metadata constant 20 - const StreamplaceMetadata = "place.stream.metadata" //nolint:all 21 19 22 20 // Streamplace metadata license values 23 21 const (
+1
pkg/gen/gen.go
··· 33 33 streamplace.MetadataContentRights{}, 34 34 streamplace.MetadataContentWarnings{}, 35 35 streamplace.ModerationPermission{}, 36 + streamplace.LiveRecommendations{}, 36 37 ); err != nil { 37 38 panic(err) 38 39 }
+16 -26
pkg/media/manifest_builder.go
··· 48 48 func (mb *ManifestBuilder) BuildManifest(ctx context.Context, streamerName string, start int64) ([]byte, error) { 49 49 log.Debug(ctx, "🔍 BuildManifest ENTRY", "streamer", streamerName, "start", start) 50 50 // Start with base manifest 51 + startTime := aqtime.FromMillis(start).String() 51 52 mani := obj{ 52 - "title": fmt.Sprintf("Livestream Segment at %s", aqtime.FromMillis(start)), 53 + "title": fmt.Sprintf("Livestream Segment at %s", startTime), 53 54 "assertions": []obj{ 55 + // Required by spec, just basic info 54 56 { 55 57 "label": "c2pa.actions", 56 58 "data": obj{ 57 59 "actions": []obj{ 58 - {"action": "c2pa.created"}, 59 - {"action": "c2pa.published"}, 60 + { 61 + "action": "c2pa.created", 62 + "when": startTime, 63 + }, 64 + { 65 + "action": "c2pa.published", 66 + "when": startTime, 67 + }, 60 68 }, 61 69 }, 62 70 }, 71 + // Content metadata, with extra custom fields added later 63 72 { 64 - "label": constants.StreamplaceMetadata, 73 + "label": "cawg.metadata", 65 74 "data": obj{ 66 75 "@context": obj{ 67 76 "dc": "http://purl.org/dc/elements/1.1/", ··· 70 79 "xmpRights": "http://ns.adobe.com/xap/1.0/rights/", 71 80 }, 72 81 "dc:creator": streamerName, 73 - // TODO: Add the title of the livestream. This should come from the livestream record. 74 - "dc:title": []string{"livestream"}, 75 - "dc:date": []string{aqtime.FromMillis(start).String()}, 82 + "dc:title": "livestream", 83 + "dc:date": startTime, 76 84 }, 77 85 }, 78 86 }, ··· 134 142 } 135 143 136 144 // Update the manifest title with the retrieved livestream title 137 - mani["assertions"].([]obj)[1]["data"].(obj)["dc:title"] = []string{livestreamTitle} 145 + mani["assertions"].([]obj)[1]["data"].(obj)["dc:title"] = livestreamTitle 138 146 139 147 // Convert manifest to JSON bytes for use with Rust c2pa library 140 148 manifestBs, err := json.Marshal(mani) ··· 228 236 // Unknown warnings remain unchanged 229 237 } 230 238 mani["assertions"].([]obj)[1]["data"].(obj)["Iptc4xmpExt:ContentWarning"] = metadata.ContentWarnings.Warnings 231 - } 232 - 233 - if metadata.DistributionPolicy != nil { 234 - // Convert the distribution policy duration to an absolute expiry timestamp 235 - // deleteAfter is in seconds, startTimeMillis is in milliseconds 236 - if metadata.DistributionPolicy.DeleteAfter != nil { 237 - // Calculate expiry: start time (seconds) + duration (seconds) = expiry timestamp (seconds) 238 - startTimeSeconds := startTimeMillis / 1000 239 - expiresAtSeconds := startTimeSeconds + *metadata.DistributionPolicy.DeleteAfter 240 - 241 - // Convert to ISO 8601 datetime string for C2PA manifest 242 - // Note: In the manifest, we store this in "deleteAfter" field but with timestamp value instead of duration 243 - deleteAfterTimestamp := aqtime.FromMillis(expiresAtSeconds * 1000).String() 244 - 245 - mani["assertions"].([]obj)[1]["data"].(obj)["distributionPolicy"] = obj{ 246 - "deleteAfter": deleteAfterTimestamp, 247 - } 248 - } 249 239 } 250 240 251 241 return mani
+14 -30
pkg/media/media.go
··· 9 9 "fmt" 10 10 "io" 11 11 "sync" 12 - "time" 13 12 14 13 "github.com/google/uuid" 15 14 "github.com/pion/interceptor" ··· 37 36 const CertFile = "cert.pem" 38 37 const SegmentsDir = "segments" 39 38 40 - var StreamplaceMetadata = "place.stream.metadata" 39 + const StreamplaceMetadata = "cawg.metadata" 41 40 42 41 type MediaManager struct { 43 42 cli *config.CLI ··· 209 208 ass = &a 210 209 break 211 210 } 211 + if a.Label == "place.stream.metadata" { 212 + // backwards compatibility for old manifests 213 + ass = &a 214 + break 215 + } 212 216 } 213 217 if ass == nil { 214 218 return nil, ErrMissingMetadata ··· 372 376 373 377 // extractDistributionPolicy extracts distribution policy from the C2PA manifest 374 378 func extractDistributionPolicy(mani *c2patypes.Manifest, segmentStart aqtime.AQTime) *model.DistributionPolicy { 375 - ass := findAssertion(mani, StreamplaceMetadata) 376 - if ass == nil { 377 - return nil 378 - } 379 - 380 - data, ok := ass.Data.(map[string]interface{}) 381 - if !ok { 382 - return nil 383 - } 384 - 385 - policy, ok := data["distributionPolicy"] 386 - if !ok { 387 - return nil 388 - } 389 - 390 - policyMap, ok := policy.(map[string]interface{}) 391 - if !ok { 379 + metadataConfig := extractMetadataConfiguration(mani) 380 + if metadataConfig == nil { 392 381 return nil 393 382 } 394 383 395 - expiry, ok := policyMap["deleteAfter"] 396 - if !ok { 384 + if metadataConfig.DistributionPolicy == nil { 397 385 return nil 398 386 } 399 387 400 - // deleteAfter now contains a timestamp string (RFC3339/ISO 8601 format) 401 - expiryStr, ok := expiry.(string) 402 - if !ok { 388 + if metadataConfig.DistributionPolicy.DeleteAfter == nil { 403 389 return nil 404 390 } 405 391 406 - expiryTime, err := time.Parse(time.RFC3339, expiryStr) 407 - if err != nil { 408 - return nil 409 - } 392 + // deleteAfter contains an offset in seconds from creation time 393 + deleteAfterSeconds := *metadataConfig.DistributionPolicy.DeleteAfter 410 394 411 395 return &model.DistributionPolicy{ 412 - ExpiresAt: &expiryTime, 396 + DeleteAfterSeconds: &deleteAfterSeconds, 413 397 } 414 398 } 415 399 416 - // extractDistributionPolicy extracts the place.stream.metadata.configuration from the C2PA manifest 400 + // extractMetadataConfiguration extracts the place.stream.metadata.configuration from the C2PA manifest 417 401 func extractMetadataConfiguration(mani *c2patypes.Manifest) *streamplace.MetadataConfiguration { 418 402 ass := findAssertion(mani, "place.stream.metadata.configuration") 419 403 if ass == nil {
+13 -6
pkg/media/media_signer.go
··· 112 112 // Fallback to basic manifest without metadata 113 113 ctx, span = otel.Tracer("signer").Start(ctx, "SignMP4_BasicManifest") 114 114 title := "livestream" 115 + startTime := aqtime.FromMillis(start).String() 115 116 mani := obj{ 116 - "title": fmt.Sprintf("Livestream Segment at %s", aqtime.FromMillis(start)), 117 + "title": fmt.Sprintf("Livestream Segment at %s", startTime), 117 118 "assertions": []obj{ 118 119 { 119 120 "label": "c2pa.actions", 120 121 "data": obj{ 121 122 "actions": []obj{ 122 - {"action": "c2pa.created"}, 123 - {"action": "c2pa.published"}, 123 + { 124 + "action": "c2pa.created", 125 + "when": startTime, 126 + }, 127 + { 128 + "action": "c2pa.published", 129 + "when": startTime, 130 + }, 124 131 }, 125 132 }, 126 133 }, 127 134 { 128 - "label": StreamplaceMetadata, 135 + "label": "cawg.metadata", 129 136 "data": obj{ 130 137 "@context": obj{ 131 138 "dc": "http://purl.org/dc/elements/1.1/", 132 139 }, 133 140 "dc:creator": ms.StreamerName, 134 - "dc:title": []string{title}, 135 - "dc:date": []string{aqtime.FromMillis(start).String()}, 141 + "dc:title": title, 142 + "dc:date": startTime, 136 143 }, 137 144 }, 138 145 },
+6 -2
pkg/media/media_test.go
··· 12 12 "stream.place/streamplace/pkg/config" 13 13 ct "stream.place/streamplace/pkg/config/configtesting" 14 14 "stream.place/streamplace/pkg/model" 15 + "stream.place/streamplace/pkg/statedb" 15 16 ) 16 17 17 18 func getFixture(name string) string { ··· 30 31 } 31 32 cli := ct.CLI(t, &config.CLI{ 32 33 TAURL: "http://timestamp.digicert.com", 33 - AllowedStreams: []string{"did:key:zQ3shhoPCrDZWE8CryCEHYCrb1x8mCkr2byTkF5EGJT7dgazC"}, 34 + AllowedStreams: []string{"did:plc:2j2ounbiyi3ftihronlw5qhj"}, 35 + DBURL: ":memory:", 34 36 }) 37 + statedb, err := statedb.MakeDB(context.Background(), cli, nil, mod) 38 + require.NoError(t, err) 35 39 atsync := &atproto.ATProtoSynchronizer{ 36 40 CLI: cli, 37 41 Model: mod, 38 - StatefulDB: nil, // Test doesn't need StatefulDB for now 42 + StatefulDB: statedb, 39 43 Bus: bus.NewBus(), 40 44 } 41 45 mm, err := MakeMediaManager(context.Background(), cli, nil, mod, bus.NewBus(), atsync)
+8
pkg/media/rtcrec_test.go
··· 36 36 expectedSegmentsMin: 1, 37 37 expectedSegmentsMax: 10, 38 38 }, 39 + { 40 + name: "NekomimiPet", 41 + fixture: remote.RemoteFixture("91176de4b92fb4c8e84116bd2be0070e96f964fcb8e127da4bfa7020317f4195/nekomimi.pet.rtcrec.cbor"), 42 + fatalErrors: true, 43 + expectedSegmentsMin: 29, 44 + expectedSegmentsMax: 29, 45 + }, 39 46 } 40 47 41 48 func TestRTCRecording(t *testing.T) { ··· 58 65 err = cli.Parse(fs, []string{ 59 66 "--data-dir", dir, 60 67 "-wide-open=true", 68 + "--segment-debug-dir", "/home/iameli/testvids/nekomimi.pet", 61 69 }) 62 70 require.NoError(t, err) 63 71 mm, err := MakeMediaManager(context.Background(), cli, nil, nil, nil, nil)
+95
pkg/media/rtmp_ingest.go
··· 1 + package media 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "strings" 7 + "time" 8 + 9 + "github.com/bluenviron/gortsplib/v5/pkg/format" 10 + "github.com/go-gst/go-gst/gst" 11 + "stream.place/streamplace/pkg/log" 12 + ) 13 + 14 + type RTMPH264Data struct { 15 + AU [][]byte 16 + PTS time.Duration 17 + DTS time.Duration 18 + } 19 + 20 + type RTMPAACData struct { 21 + AU []byte 22 + PTS time.Duration 23 + } 24 + 25 + type RTMPSession struct { 26 + EventChan chan any 27 + VideoTrack *format.H264 28 + AudioTrack *format.MPEG4Audio 29 + MediaSigner MediaSigner 30 + } 31 + 32 + func (mm *MediaManager) RTMPIngest(ctx context.Context, rtmpURL string, ms MediaSigner) error { 33 + ctx, cancel := context.WithCancel(ctx) 34 + defer cancel() 35 + pipelineSlice := []string{ 36 + fmt.Sprintf("rtmp2src location=%s ! flvdemux name=demux", rtmpURL), 37 + "demux.audio ! queue ! fdkaacdec ! audioresample ! opusenc name=audioenc", 38 + "demux.video ! queue ! h264parse name=parse", 39 + } 40 + pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) 41 + if err != nil { 42 + return fmt.Errorf("error creating RTMPIngest pipeline: %w", err) 43 + } 44 + 45 + signer, err := mm.SegmentAndSignElem(ctx, ms) 46 + if err != nil { 47 + return err 48 + } 49 + 50 + parseEle, err := pipeline.GetElementByName("parse") 51 + if err != nil { 52 + return err 53 + } 54 + 55 + err = pipeline.Add(signer) 56 + if err != nil { 57 + return err 58 + } 59 + err = parseEle.Link(signer) 60 + if err != nil { 61 + return err 62 + } 63 + audioenc, err := pipeline.GetElementByName("audioenc") 64 + if err != nil { 65 + return err 66 + } 67 + err = audioenc.Link(signer) 68 + if err != nil { 69 + return err 70 + } 71 + 72 + busErr := make(chan error) 73 + go func() { 74 + err := HandleBusMessages(ctx, pipeline) 75 + busErr <- err 76 + }() 77 + 78 + go mm.HandleKeyRevocation(ctx, ms, pipeline) 79 + 80 + err = pipeline.SetState(gst.StatePlaying) 81 + if err != nil { 82 + return err 83 + } 84 + 85 + defer func() { 86 + err := pipeline.SetState(gst.StateNull) 87 + if err != nil { 88 + log.Error(ctx, "error setting pipeline to null state", "error", err) 89 + } 90 + }() 91 + 92 + err = <-busErr 93 + 94 + return err 95 + }
+5 -4
pkg/media/segment_roundtrip_test.go
··· 44 44 name string 45 45 fixture string 46 46 }{ 47 - { 48 - name: "OneMinute", 49 - fixture: remote.RemoteArchive("4563c7b48c0ca02c3fc87bbe6f1e63a743656e465a82bec0af75ef7eead04a23/1-minute-of-signed-segments.tar.gz"), 50 - }, 47 + // { 48 + // name: "OneMinute", 49 + // fixture: remote.RemoteArchive("4563c7b48c0ca02c3fc87bbe6f1e63a743656e465a82bec0af75ef7eead04a23/1-minute-of-signed-segments.tar.gz"), 50 + // }, 51 51 { 52 52 name: "ThreeSegs", 53 53 fixture: remote.RemoteArchive("c21e9352e72ca0729c66af2fcabec1b8997b509601241e8d38d5728f9687386b/threesegs.tar.gz"), ··· 57 57 t.Run(testCase.name, func(t *testing.T) { 58 58 withNoGSTLeaks(t, func() { 59 59 tempDir, err := os.MkdirTemp("", "ingredient_test") 60 + t.Logf("tempDir: %s", tempDir) 60 61 require.NoError(t, err) 61 62 getTestVids := func() []io.ReadSeeker { 62 63 testVids := []io.ReadSeeker{}
+3
pkg/media/segment_split.go
··· 126 126 SegmentMetadata: metadata, 127 127 }) 128 128 } 129 + if len(manifestList) == 0 { 130 + return fmt.Errorf("no manifests found") 131 + } 129 132 sort.Slice(manifestList, func(i, j int) bool { 130 133 m1 := manifestList[i] 131 134 m2 := manifestList[j]
+4 -2
pkg/media/segmenter.go
··· 127 127 } 128 128 resetTimer <- struct{}{} 129 129 convergeAndSign := func() error { 130 - bs, err := ConvergeSegment(ctx, cli, bs, now, streamer, doH264Parse) 130 + convergedBs, err := ConvergeSegment(ctx, cli, bs, now, streamer, doH264Parse) 131 131 if err != nil { 132 - return fmt.Errorf("error converging segment: %w", err) 132 + log.Error(ctx, "error converging segment", "error", err) 133 + } else { 134 + bs = convergedBs 133 135 } 134 136 log.Debug(ctx, "signing segment", "size", len(bs)) 135 137 err = cb(ctx, bs, now)
+8 -6
pkg/media/validate.go
··· 113 113 return err 114 114 } 115 115 var deleteAfter *time.Time 116 - if meta.DistributionPolicy != nil && meta.DistributionPolicy.ExpiresAt != nil { 117 - deleteAfter = meta.DistributionPolicy.ExpiresAt 116 + if meta.DistributionPolicy != nil && meta.DistributionPolicy.DeleteAfterSeconds != nil { 117 + expiryTime := meta.StartTime.Time().Add(time.Duration(*meta.DistributionPolicy.DeleteAfterSeconds) * time.Second) 118 + deleteAfter = &expiryTime 118 119 } 119 120 seg := &model.Segment{ 120 121 ID: *label, ··· 173 174 174 175 // Check distribution policy (if enabled) 175 176 if mm.cli.ContentFilters.DistributionPolicy.Enabled && meta.DistributionPolicy != nil { 176 - if meta.DistributionPolicy.ExpiresAt != nil { 177 - if time.Now().After(*meta.DistributionPolicy.ExpiresAt) { 178 - reason := fmt.Sprintf("distribution policy expired: segment expires at %s", meta.DistributionPolicy.ExpiresAt) 177 + if meta.DistributionPolicy.DeleteAfterSeconds != nil { 178 + expiresAt := meta.StartTime.Time().Add(time.Duration(*meta.DistributionPolicy.DeleteAfterSeconds) * time.Second) 179 + if time.Now().After(expiresAt) { 180 + reason := fmt.Sprintf("distribution policy expired: segment expires at %s", expiresAt) 179 181 log.Log(ctx, "content filtered", 180 182 "reason", reason, 181 183 "filter_type", "distribution_policy", 182 184 "creator", meta.Creator, 183 185 "start_time", meta.StartTime, 184 - "expires_at", *meta.DistributionPolicy.ExpiresAt) 186 + "expires_at", expiresAt) 185 187 return fmt.Errorf("content filtered: %s", reason) 186 188 } 187 189 }
+6
pkg/model/model.go
··· 32 32 MostRecentSegments() ([]Segment, error) 33 33 LatestSegmentForUser(user string) (*Segment, error) 34 34 LatestSegmentsForUser(user string, limit int, before *time.Time, after *time.Time) ([]Segment, error) 35 + FilterLiveRepoDIDs(repoDIDs []string) ([]string, error) 35 36 CreateThumbnail(thumb *Thumbnail) error 36 37 LatestThumbnailForUser(user string) (*Thumbnail, error) 37 38 GetSegment(id string) (*Segment, error) ··· 48 49 GetRepoByHandleOrDID(arg string) (*Repo, error) 49 50 GetRepoBySigningKey(signingKey string) (*Repo, error) 50 51 GetAllRepos() ([]Repo, error) 52 + SearchReposByHandle(query string, limit int) ([]Repo, error) 51 53 UpdateRepo(repo *Repo) error 52 54 53 55 UpdateSigningKey(key *SigningKey) error ··· 114 116 GetModerationDelegations(ctx context.Context, streamerDID, moderatorDID string) ([]*streamplace.ModerationDefs_PermissionView, error) 115 117 GetModeratorDelegations(ctx context.Context, moderatorDID string) ([]*streamplace.ModerationDefs_PermissionView, error) 116 118 GetStreamerModerators(ctx context.Context, streamerDID string) ([]*streamplace.ModerationDefs_PermissionView, error) 119 + 120 + GetRecommendation(userDID string) (*Recommendation, error) 121 + UpsertRecommendation(rec *Recommendation) error 117 122 } 118 123 119 124 var DBRevision = 2 ··· 183 188 BroadcastOrigin{}, 184 189 MetadataConfiguration{}, 185 190 ModerationDelegation{}, 191 + Recommendation{}, 186 192 } { 187 193 err = db.AutoMigrate(model) 188 194 if err != nil {
+76
pkg/model/recommendations.go
··· 1 + package model 2 + 3 + import ( 4 + "encoding/json" 5 + "errors" 6 + "fmt" 7 + "time" 8 + 9 + "gorm.io/gorm" 10 + ) 11 + 12 + type Recommendation struct { 13 + UserDID string `gorm:"column:user_did;primaryKey"` 14 + Streamers json.RawMessage `gorm:"column:streamers;type:json;not null"` 15 + CreatedAt time.Time `gorm:"column:created_at"` 16 + UpdatedAt time.Time `gorm:"column:updated_at"` 17 + } 18 + 19 + func (r *Recommendation) TableName() string { 20 + return "recommendations" 21 + } 22 + 23 + func (r *Recommendation) GetStreamersArray() ([]string, error) { 24 + var streamers []string 25 + if err := json.Unmarshal(r.Streamers, &streamers); err != nil { 26 + return nil, fmt.Errorf("failed to unmarshal streamers: %w", err) 27 + } 28 + return streamers, nil 29 + } 30 + 31 + // UpsertRecommendation creates or updates recommendations for a user 32 + func (m *DBModel) UpsertRecommendation(rec *Recommendation) error { 33 + if rec.UserDID == "" { 34 + return fmt.Errorf("user DID cannot be empty") 35 + } 36 + 37 + // Validate JSON contains array of max 8 DIDs 38 + var streamers []string 39 + if err := json.Unmarshal(rec.Streamers, &streamers); err != nil { 40 + return fmt.Errorf("invalid streamers JSON: %w", err) 41 + } 42 + if len(streamers) > 8 { 43 + return fmt.Errorf("maximum 8 recommendations allowed, got %d", len(streamers)) 44 + } 45 + 46 + now := time.Now() 47 + if rec.CreatedAt.IsZero() { 48 + rec.CreatedAt = now 49 + } 50 + rec.UpdatedAt = now 51 + 52 + // Use GORM's upsert (On Conflict Do Update) 53 + result := m.DB.Save(rec) 54 + if result.Error != nil { 55 + return fmt.Errorf("database upsert failed: %w", result.Error) 56 + } 57 + 58 + return nil 59 + } 60 + 61 + // GetRecommendation retrieves a valid recommendation from a user 62 + func (m *DBModel) GetRecommendation(userDID string) (*Recommendation, error) { 63 + if userDID == "" { 64 + return nil, fmt.Errorf("user DID cannot be empty") 65 + } 66 + 67 + var rec Recommendation 68 + err := m.DB.Where("user_did = ?", userDID).First(&rec).Error 69 + if err != nil { 70 + if errors.Is(err, gorm.ErrRecordNotFound) { 71 + return nil, err 72 + } 73 + return nil, fmt.Errorf("database query failed: %w", err) 74 + } 75 + return &rec, nil 76 + }
+11
pkg/model/repo.go
··· 77 77 func (m *DBModel) UpdateRepo(repo *Repo) error { 78 78 return m.DB.Save(repo).Error 79 79 } 80 + 81 + func (m *DBModel) SearchReposByHandle(query string, limit int) ([]Repo, error) { 82 + var repos []Repo 83 + // Search for repos where handle starts with the query (case-insensitive) 84 + // Use LIKE with LOWER for sqlite/postgres compatibility 85 + res := m.DB.Where("LOWER(handle) LIKE LOWER(?)", query+"%").Limit(limit).Find(&repos) 86 + if res.Error != nil { 87 + return nil, res.Error 88 + } 89 + return repos, nil 90 + }
+24 -7
pkg/model/segment.go
··· 85 85 86 86 // DistributionPolicy represents distribution policy information 87 87 type DistributionPolicy struct { 88 - ExpiresAt *time.Time `json:"expiresAt,omitempty"` 88 + DeleteAfterSeconds *int64 `json:"deleteAfterSeconds,omitempty"` 89 89 } 90 90 91 91 // Scan scan value into DistributionPolicy, implements sql.Scanner interface ··· 185 185 } 186 186 187 187 var distributionPolicy *streamplace.MetadataDistributionPolicy 188 - if s.DistributionPolicy != nil && s.DistributionPolicy.ExpiresAt != nil { 189 - // Convert the absolute timestamp back to a duration (in seconds) from segment start 190 - startTimeUnix := s.StartTime.Unix() 191 - expiresAtUnix := s.DistributionPolicy.ExpiresAt.Unix() 192 - deleteAfterSecs := expiresAtUnix - startTimeUnix 188 + if s.DistributionPolicy != nil && s.DistributionPolicy.DeleteAfterSeconds != nil { 193 189 distributionPolicy = &streamplace.MetadataDistributionPolicy{ 194 - DeleteAfter: &deleteAfterSecs, 190 + DeleteAfter: s.DistributionPolicy.DeleteAfterSeconds, 195 191 } 196 192 } 197 193 ··· 281 277 return nil, err 282 278 } 283 279 return &seg, nil 280 + } 281 + 282 + func (m *DBModel) FilterLiveRepoDIDs(repoDIDs []string) ([]string, error) { 283 + if len(repoDIDs) == 0 { 284 + return []string{}, nil 285 + } 286 + 287 + thirtySecondsAgo := time.Now().Add(-30 * time.Second) 288 + 289 + var liveDIDs []string 290 + 291 + err := m.DB.Table("segments"). 292 + Select("DISTINCT repo_did"). 293 + Where("repo_did IN ? AND start_time > ?", repoDIDs, thirtySecondsAgo.UTC()). 294 + Pluck("repo_did", &liveDIDs).Error 295 + 296 + if err != nil { 297 + return nil, err 298 + } 299 + 300 + return liveDIDs, nil 284 301 } 285 302 286 303 func (m *DBModel) LatestSegmentsForUser(user string, limit int, before *time.Time, after *time.Time) ([]Segment, error) {
+1
pkg/multitest/multitest_test.go
··· 116 116 env := map[string]string{ 117 117 "SP_HTTP_ADDR": fmt.Sprintf("127.0.0.1:%d", apiPort), 118 118 "SP_HTTP_INTERNAL_ADDR": fmt.Sprintf("127.0.0.1:%d", nextPort()), 119 + "SP_RTMP_ADDR": fmt.Sprintf("127.0.0.1:%d", nextPort()), 119 120 "SP_RELAY_HOST": strings.ReplaceAll(dev.PDSURL, "http://", "ws://"), 120 121 "SP_PLC_URL": dev.PLCURL, 121 122 "SP_DATA_DIR": dataDir,
+2 -1
pkg/notifications/firebase.go
··· 5 5 "encoding/json" 6 6 "fmt" 7 7 8 + "context" 9 + 8 10 firebase "firebase.google.com/go/v4" 9 11 "firebase.google.com/go/v4/messaging" 10 - "golang.org/x/net/context" 11 12 "google.golang.org/api/option" 12 13 "stream.place/streamplace/pkg/log" 13 14 )
+3 -3
pkg/rtmps/rtmps.go
··· 14 14 ) 15 15 16 16 // passthrough RTMPS TLS terminator to external RTMP server 17 - func ServeRTMPS(ctx context.Context, cli *config.CLI) error { 17 + func ServeRTMPSAddon(ctx context.Context, cli *config.CLI) error { 18 18 if cli.RTMPServerAddon == "" { 19 19 return fmt.Errorf("RTMP server address not configured") 20 20 } ··· 29 29 MinVersion: tls.VersionTLS12, 30 30 } 31 31 32 - listener, err := tls.Listen("tcp", cli.RtmpsAddr, tlsConfig) 32 + listener, err := tls.Listen("tcp", cli.RTMPAddr, tlsConfig) 33 33 if err != nil { 34 34 return fmt.Errorf("failed to create RTMPS listener: %w", err) 35 35 } 36 36 37 37 log.Log(ctx, "rtmps server starting", 38 - "addr", cli.RtmpsAddr, 38 + "addr", cli.RTMPAddr, 39 39 "forwarding_to", cli.RTMPServerAddon) 40 40 41 41 go func() {
+99
pkg/spxrpc/place_stream_live.go
··· 153 153 log.Debug(c.Request().Context(), "received message", "message", string(msg)) 154 154 } 155 155 } 156 + 157 + func (s *Server) handlePlaceStreamLiveGetRecommendations(ctx context.Context, userDID string) (*placestreamtypes.LiveGetRecommendations_Output, error) { 158 + if userDID == "" { 159 + return nil, echo.NewHTTPError(http.StatusBadRequest, "userDID is required") 160 + } 161 + 162 + // Try to get streamer's recommendation list 163 + rec, err := s.model.GetRecommendation(userDID) 164 + // If we have a recommendation list, filter for live streamers 165 + if err == nil { 166 + streamers, err := rec.GetStreamersArray() 167 + if err != nil { 168 + return nil, echo.NewHTTPError(http.StatusInternalServerError, "Failed to parse recommendations") 169 + } 170 + 171 + // Filter for only live streamers 172 + liveStreamers, err := s.model.FilterLiveRepoDIDs(streamers) 173 + if err != nil { 174 + return nil, echo.NewHTTPError(http.StatusInternalServerError, "Failed to filter live streamers") 175 + } 176 + 177 + if len(liveStreamers) > 0 { 178 + var recommendations []*placestreamtypes.LiveGetRecommendations_Output_Recommendations_Elem 179 + for _, did := range liveStreamers { 180 + recommendations = append(recommendations, &placestreamtypes.LiveGetRecommendations_Output_Recommendations_Elem{ 181 + LiveGetRecommendations_LivestreamRecommendation: &placestreamtypes.LiveGetRecommendations_LivestreamRecommendation{ 182 + Did: did, 183 + Source: "streamer", 184 + }, 185 + }) 186 + } 187 + return &placestreamtypes.LiveGetRecommendations_Output{ 188 + Recommendations: recommendations, 189 + UserDID: &userDID, 190 + }, nil 191 + } 192 + } else { 193 + // not a big issue but we should log anyways 194 + log.Log(ctx, "no recommendations found for user", "userDID", userDID) 195 + } 196 + 197 + // get user's follows and check which are live 198 + follows, err := s.model.GetUserFollowing(ctx, userDID) 199 + if err == nil && len(follows) > 0 { 200 + followDIDs := make([]string, len(follows)) 201 + for i, follow := range follows { 202 + followDIDs[i] = follow.SubjectDID 203 + } 204 + 205 + liveFollows, err := s.model.FilterLiveRepoDIDs(followDIDs) 206 + if err != nil { 207 + return nil, echo.NewHTTPError(http.StatusInternalServerError, "Failed to filter live follows") 208 + } 209 + 210 + if len(liveFollows) > 0 { 211 + var recommendations []*placestreamtypes.LiveGetRecommendations_Output_Recommendations_Elem 212 + for _, did := range liveFollows { 213 + recommendations = append(recommendations, &placestreamtypes.LiveGetRecommendations_Output_Recommendations_Elem{ 214 + LiveGetRecommendations_LivestreamRecommendation: &placestreamtypes.LiveGetRecommendations_LivestreamRecommendation{ 215 + Did: did, 216 + Source: "follows", 217 + }, 218 + }) 219 + } 220 + return &placestreamtypes.LiveGetRecommendations_Output{ 221 + Recommendations: recommendations, 222 + UserDID: &userDID, 223 + }, nil 224 + } 225 + } 226 + 227 + // Final fallback: use host's default recommendations 228 + defaultStreamers := s.cli.DefaultRecommendedStreamers 229 + if len(defaultStreamers) > 0 { 230 + liveDefaults, err := s.model.FilterLiveRepoDIDs(defaultStreamers) 231 + if err != nil { 232 + return nil, echo.NewHTTPError(http.StatusInternalServerError, "Failed to filter default streamers") 233 + } 234 + var recommendations []*placestreamtypes.LiveGetRecommendations_Output_Recommendations_Elem 235 + for _, did := range liveDefaults { 236 + recommendations = append(recommendations, &placestreamtypes.LiveGetRecommendations_Output_Recommendations_Elem{ 237 + LiveGetRecommendations_LivestreamRecommendation: &placestreamtypes.LiveGetRecommendations_LivestreamRecommendation{ 238 + Did: did, 239 + Source: "host", 240 + }, 241 + }) 242 + } 243 + return &placestreamtypes.LiveGetRecommendations_Output{ 244 + Recommendations: recommendations, 245 + UserDID: &userDID, 246 + }, nil 247 + } 248 + 249 + // No recommendations available 250 + return &placestreamtypes.LiveGetRecommendations_Output{ 251 + Recommendations: []*placestreamtypes.LiveGetRecommendations_Output_Recommendations_Elem{}, 252 + UserDID: &userDID, 253 + }, nil 254 + }
+45
pkg/spxrpc/place_stream_live_searchActorsTypeahead.go
··· 1 + package spxrpc 2 + 3 + import ( 4 + "context" 5 + "net/http" 6 + 7 + "github.com/labstack/echo/v4" 8 + placestreamtypes "stream.place/streamplace/pkg/streamplace" 9 + ) 10 + 11 + func (s *Server) handlePlaceStreamLiveSearchActorsTypeahead(ctx context.Context, limit int, q string) (*placestreamtypes.LiveSearchActorsTypeahead_Output, error) { 12 + if q == "" { 13 + return &placestreamtypes.LiveSearchActorsTypeahead_Output{ 14 + Actors: []*placestreamtypes.LiveSearchActorsTypeahead_Actor{}, 15 + }, nil 16 + } 17 + 18 + // Default limit to 10 if not specified 19 + searchLimit := 10 20 + if limit > 0 { 21 + searchLimit = limit 22 + if searchLimit > 100 { 23 + searchLimit = 100 24 + } 25 + } 26 + 27 + // Search repos by handle 28 + repos, err := s.model.SearchReposByHandle(q, searchLimit) 29 + if err != nil { 30 + return nil, echo.NewHTTPError(http.StatusInternalServerError, "Failed to search actors "+err.Error()) 31 + } 32 + 33 + // Convert to output format 34 + actors := make([]*placestreamtypes.LiveSearchActorsTypeahead_Actor, len(repos)) 35 + for i, repo := range repos { 36 + actors[i] = &placestreamtypes.LiveSearchActorsTypeahead_Actor{ 37 + Did: repo.DID, 38 + Handle: repo.Handle, 39 + } 40 + } 41 + 42 + return &placestreamtypes.LiveSearchActorsTypeahead_Output{ 43 + Actors: actors, 44 + }, nil 45 + }
+41
pkg/spxrpc/stubs.go
··· 266 266 e.GET("/xrpc/place.stream.graph.getFollowingUser", s.HandlePlaceStreamGraphGetFollowingUser) 267 267 e.GET("/xrpc/place.stream.live.getLiveUsers", s.HandlePlaceStreamLiveGetLiveUsers) 268 268 e.GET("/xrpc/place.stream.live.getProfileCard", s.HandlePlaceStreamLiveGetProfileCard) 269 + e.GET("/xrpc/place.stream.live.getRecommendations", s.HandlePlaceStreamLiveGetRecommendations) 269 270 e.GET("/xrpc/place.stream.live.getSegments", s.HandlePlaceStreamLiveGetSegments) 271 + e.GET("/xrpc/place.stream.live.searchActorsTypeahead", s.HandlePlaceStreamLiveSearchActorsTypeahead) 270 272 e.POST("/xrpc/place.stream.moderation.createBlock", s.HandlePlaceStreamModerationCreateBlock) 271 273 e.POST("/xrpc/place.stream.moderation.createGate", s.HandlePlaceStreamModerationCreateGate) 272 274 e.POST("/xrpc/place.stream.moderation.deleteBlock", s.HandlePlaceStreamModerationDeleteBlock) ··· 348 350 return c.Stream(200, "application/octet-stream", out) 349 351 } 350 352 353 + func (s *Server) HandlePlaceStreamLiveGetRecommendations(c echo.Context) error { 354 + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamLiveGetRecommendations") 355 + defer span.End() 356 + userDID := c.QueryParam("userDID") 357 + var out *placestream.LiveGetRecommendations_Output 358 + var handleErr error 359 + // func (s *Server) handlePlaceStreamLiveGetRecommendations(ctx context.Context,userDID string) (*placestream.LiveGetRecommendations_Output, error) 360 + out, handleErr = s.handlePlaceStreamLiveGetRecommendations(ctx, userDID) 361 + if handleErr != nil { 362 + return handleErr 363 + } 364 + return c.JSON(200, out) 365 + } 366 + 351 367 func (s *Server) HandlePlaceStreamLiveGetSegments(c echo.Context) error { 352 368 ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamLiveGetSegments") 353 369 defer span.End() ··· 368 384 var handleErr error 369 385 // func (s *Server) handlePlaceStreamLiveGetSegments(ctx context.Context,before string,limit int,userDID string) (*placestream.LiveGetSegments_Output, error) 370 386 out, handleErr = s.handlePlaceStreamLiveGetSegments(ctx, before, limit, userDID) 387 + if handleErr != nil { 388 + return handleErr 389 + } 390 + return c.JSON(200, out) 391 + } 392 + 393 + func (s *Server) HandlePlaceStreamLiveSearchActorsTypeahead(c echo.Context) error { 394 + ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandlePlaceStreamLiveSearchActorsTypeahead") 395 + defer span.End() 396 + 397 + var limit int 398 + if p := c.QueryParam("limit"); p != "" { 399 + var err error 400 + limit, err = strconv.Atoi(p) 401 + if err != nil { 402 + return err 403 + } 404 + } else { 405 + limit = 10 406 + } 407 + q := c.QueryParam("q") 408 + var out *placestream.LiveSearchActorsTypeahead_Output 409 + var handleErr error 410 + // func (s *Server) handlePlaceStreamLiveSearchActorsTypeahead(ctx context.Context,limit int,q string) (*placestream.LiveSearchActorsTypeahead_Output, error) 411 + out, handleErr = s.handlePlaceStreamLiveSearchActorsTypeahead(ctx, limit, q) 371 412 if handleErr != nil { 372 413 return handleErr 373 414 }
+203
pkg/streamplace/cbor_gen.go
··· 5270 5270 5271 5271 return nil 5272 5272 } 5273 + func (t *LiveRecommendations) MarshalCBOR(w io.Writer) error { 5274 + if t == nil { 5275 + _, err := w.Write(cbg.CborNull) 5276 + return err 5277 + } 5278 + 5279 + cw := cbg.NewCborWriter(w) 5280 + 5281 + if _, err := cw.Write([]byte{163}); err != nil { 5282 + return err 5283 + } 5284 + 5285 + // t.LexiconTypeID (string) (string) 5286 + if len("$type") > 1000000 { 5287 + return xerrors.Errorf("Value in field \"$type\" was too long") 5288 + } 5289 + 5290 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 5291 + return err 5292 + } 5293 + if _, err := cw.WriteString(string("$type")); err != nil { 5294 + return err 5295 + } 5296 + 5297 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.live.recommendations"))); err != nil { 5298 + return err 5299 + } 5300 + if _, err := cw.WriteString(string("place.stream.live.recommendations")); err != nil { 5301 + return err 5302 + } 5303 + 5304 + // t.CreatedAt (string) (string) 5305 + if len("createdAt") > 1000000 { 5306 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 5307 + } 5308 + 5309 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 5310 + return err 5311 + } 5312 + if _, err := cw.WriteString(string("createdAt")); err != nil { 5313 + return err 5314 + } 5315 + 5316 + if len(t.CreatedAt) > 1000000 { 5317 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 5318 + } 5319 + 5320 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 5321 + return err 5322 + } 5323 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 5324 + return err 5325 + } 5326 + 5327 + // t.Streamers ([]string) (slice) 5328 + if len("streamers") > 1000000 { 5329 + return xerrors.Errorf("Value in field \"streamers\" was too long") 5330 + } 5331 + 5332 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("streamers"))); err != nil { 5333 + return err 5334 + } 5335 + if _, err := cw.WriteString(string("streamers")); err != nil { 5336 + return err 5337 + } 5338 + 5339 + if len(t.Streamers) > 8192 { 5340 + return xerrors.Errorf("Slice value in field t.Streamers was too long") 5341 + } 5342 + 5343 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Streamers))); err != nil { 5344 + return err 5345 + } 5346 + for _, v := range t.Streamers { 5347 + if len(v) > 1000000 { 5348 + return xerrors.Errorf("Value in field v was too long") 5349 + } 5350 + 5351 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 5352 + return err 5353 + } 5354 + if _, err := cw.WriteString(string(v)); err != nil { 5355 + return err 5356 + } 5357 + 5358 + } 5359 + return nil 5360 + } 5361 + 5362 + func (t *LiveRecommendations) UnmarshalCBOR(r io.Reader) (err error) { 5363 + *t = LiveRecommendations{} 5364 + 5365 + cr := cbg.NewCborReader(r) 5366 + 5367 + maj, extra, err := cr.ReadHeader() 5368 + if err != nil { 5369 + return err 5370 + } 5371 + defer func() { 5372 + if err == io.EOF { 5373 + err = io.ErrUnexpectedEOF 5374 + } 5375 + }() 5376 + 5377 + if maj != cbg.MajMap { 5378 + return fmt.Errorf("cbor input should be of type map") 5379 + } 5380 + 5381 + if extra > cbg.MaxLength { 5382 + return fmt.Errorf("LiveRecommendations: map struct too large (%d)", extra) 5383 + } 5384 + 5385 + n := extra 5386 + 5387 + nameBuf := make([]byte, 9) 5388 + for i := uint64(0); i < n; i++ { 5389 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 5390 + if err != nil { 5391 + return err 5392 + } 5393 + 5394 + if !ok { 5395 + // Field doesn't exist on this type, so ignore it 5396 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 5397 + return err 5398 + } 5399 + continue 5400 + } 5401 + 5402 + switch string(nameBuf[:nameLen]) { 5403 + // t.LexiconTypeID (string) (string) 5404 + case "$type": 5405 + 5406 + { 5407 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5408 + if err != nil { 5409 + return err 5410 + } 5411 + 5412 + t.LexiconTypeID = string(sval) 5413 + } 5414 + // t.CreatedAt (string) (string) 5415 + case "createdAt": 5416 + 5417 + { 5418 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5419 + if err != nil { 5420 + return err 5421 + } 5422 + 5423 + t.CreatedAt = string(sval) 5424 + } 5425 + // t.Streamers ([]string) (slice) 5426 + case "streamers": 5427 + 5428 + maj, extra, err = cr.ReadHeader() 5429 + if err != nil { 5430 + return err 5431 + } 5432 + 5433 + if extra > 8192 { 5434 + return fmt.Errorf("t.Streamers: array too large (%d)", extra) 5435 + } 5436 + 5437 + if maj != cbg.MajArray { 5438 + return fmt.Errorf("expected cbor array") 5439 + } 5440 + 5441 + if extra > 0 { 5442 + t.Streamers = make([]string, extra) 5443 + } 5444 + 5445 + for i := 0; i < int(extra); i++ { 5446 + { 5447 + var maj byte 5448 + var extra uint64 5449 + var err error 5450 + _ = maj 5451 + _ = extra 5452 + _ = err 5453 + 5454 + { 5455 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5456 + if err != nil { 5457 + return err 5458 + } 5459 + 5460 + t.Streamers[i] = string(sval) 5461 + } 5462 + 5463 + } 5464 + } 5465 + 5466 + default: 5467 + // Field doesn't exist on this type, so ignore it 5468 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 5469 + return err 5470 + } 5471 + } 5472 + } 5473 + 5474 + return nil 5475 + }
+72
pkg/streamplace/livegetRecommendations.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + // Lexicon schema: place.stream.live.getRecommendations 4 + 5 + package streamplace 6 + 7 + import ( 8 + "context" 9 + "encoding/json" 10 + "fmt" 11 + 12 + lexutil "github.com/bluesky-social/indigo/lex/util" 13 + ) 14 + 15 + // LiveGetRecommendations_LivestreamRecommendation is a "livestreamRecommendation" in the place.stream.live.getRecommendations schema. 16 + type LiveGetRecommendations_LivestreamRecommendation struct { 17 + LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.live.getRecommendations#livestreamRecommendation"` 18 + // did: The DID of the recommended streamer 19 + Did string `json:"did" cborgen:"did"` 20 + // source: Source of the recommendation 21 + Source string `json:"source" cborgen:"source"` 22 + } 23 + 24 + // LiveGetRecommendations_Output is the output of a place.stream.live.getRecommendations call. 25 + type LiveGetRecommendations_Output struct { 26 + // recommendations: Ordered list of recommendations 27 + Recommendations []*LiveGetRecommendations_Output_Recommendations_Elem `json:"recommendations" cborgen:"recommendations"` 28 + // userDID: The user DID this recommendation is for 29 + UserDID *string `json:"userDID,omitempty" cborgen:"userDID,omitempty"` 30 + } 31 + 32 + type LiveGetRecommendations_Output_Recommendations_Elem struct { 33 + LiveGetRecommendations_LivestreamRecommendation *LiveGetRecommendations_LivestreamRecommendation 34 + } 35 + 36 + func (t *LiveGetRecommendations_Output_Recommendations_Elem) MarshalJSON() ([]byte, error) { 37 + if t.LiveGetRecommendations_LivestreamRecommendation != nil { 38 + t.LiveGetRecommendations_LivestreamRecommendation.LexiconTypeID = "place.stream.live.getRecommendations#livestreamRecommendation" 39 + return json.Marshal(t.LiveGetRecommendations_LivestreamRecommendation) 40 + } 41 + return nil, fmt.Errorf("can not marshal empty union as JSON") 42 + } 43 + 44 + func (t *LiveGetRecommendations_Output_Recommendations_Elem) UnmarshalJSON(b []byte) error { 45 + typ, err := lexutil.TypeExtract(b) 46 + if err != nil { 47 + return err 48 + } 49 + 50 + switch typ { 51 + case "place.stream.live.getRecommendations#livestreamRecommendation": 52 + t.LiveGetRecommendations_LivestreamRecommendation = new(LiveGetRecommendations_LivestreamRecommendation) 53 + return json.Unmarshal(b, t.LiveGetRecommendations_LivestreamRecommendation) 54 + default: 55 + return nil 56 + } 57 + } 58 + 59 + // LiveGetRecommendations calls the XRPC method "place.stream.live.getRecommendations". 60 + // 61 + // userDID: The DID of the user whose recommendations to fetch 62 + func LiveGetRecommendations(ctx context.Context, c lexutil.LexClient, userDID string) (*LiveGetRecommendations_Output, error) { 63 + var out LiveGetRecommendations_Output 64 + 65 + params := map[string]interface{}{} 66 + params["userDID"] = userDID 67 + if err := c.LexDo(ctx, lexutil.Query, "", "place.stream.live.getRecommendations", params, nil, &out); err != nil { 68 + return nil, err 69 + } 70 + 71 + return &out, nil 72 + }
+21
pkg/streamplace/liverecommendations.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + // Lexicon schema: place.stream.live.recommendations 4 + 5 + package streamplace 6 + 7 + import ( 8 + lexutil "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 11 + func init() { 12 + lexutil.RegisterType("place.stream.live.recommendations", &LiveRecommendations{}) 13 + } 14 + 15 + type LiveRecommendations struct { 16 + LexiconTypeID string `json:"$type" cborgen:"$type,const=place.stream.live.recommendations"` 17 + // createdAt: Client-declared timestamp when this list was created. 18 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 19 + // streamers: Ordered list of recommended streamer DIDs 20 + Streamers []string `json:"streamers" cborgen:"streamers"` 21 + }
+44
pkg/streamplace/livesearchActorsTypeahead.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + // Lexicon schema: place.stream.live.searchActorsTypeahead 4 + 5 + package streamplace 6 + 7 + import ( 8 + "context" 9 + 10 + lexutil "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + // LiveSearchActorsTypeahead_Actor is a "actor" in the place.stream.live.searchActorsTypeahead schema. 14 + type LiveSearchActorsTypeahead_Actor struct { 15 + // did: The actor's DID 16 + Did string `json:"did" cborgen:"did"` 17 + // handle: The actor's handle 18 + Handle string `json:"handle" cborgen:"handle"` 19 + } 20 + 21 + // LiveSearchActorsTypeahead_Output is the output of a place.stream.live.searchActorsTypeahead call. 22 + type LiveSearchActorsTypeahead_Output struct { 23 + Actors []*LiveSearchActorsTypeahead_Actor `json:"actors" cborgen:"actors"` 24 + } 25 + 26 + // LiveSearchActorsTypeahead calls the XRPC method "place.stream.live.searchActorsTypeahead". 27 + // 28 + // q: Search query prefix; not a full query string. 29 + func LiveSearchActorsTypeahead(ctx context.Context, c lexutil.LexClient, limit int64, q string) (*LiveSearchActorsTypeahead_Output, error) { 30 + var out LiveSearchActorsTypeahead_Output 31 + 32 + params := map[string]interface{}{} 33 + if limit != 0 { 34 + params["limit"] = limit 35 + } 36 + if q != "" { 37 + params["q"] = q 38 + } 39 + if err := c.LexDo(ctx, lexutil.Query, "", "place.stream.live.searchActorsTypeahead", params, nil, &out); err != nil { 40 + return nil, err 41 + } 42 + 43 + return &out, nil 44 + }
+866 -169
pnpm-lock.yaml
··· 19 19 version: 4.1.0(prettier@3.4.2)(typescript@5.8.3) 20 20 quicktype: 21 21 specifier: ^23.2.6 22 - version: 23.2.6(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13) 22 + version: 23.2.6(@swc/core@1.15.4(@swc/helpers@0.5.17))(encoding@0.1.13) 23 23 devDependencies: 24 24 '@atproto/lex-cli': 25 25 specifier: ^0.9.4 ··· 35 35 version: 5.59.1(@types/node@22.15.17)(typescript@5.8.3) 36 36 lerna: 37 37 specifier: ^8.2.2 38 - version: 8.2.2(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13) 38 + version: 8.2.2(@swc/core@1.15.4(@swc/helpers@0.5.17))(encoding@0.1.13) 39 39 lint-staged: 40 40 specifier: ^15.2.10 41 41 version: 15.2.10 ··· 240 240 react-native: 241 241 specifier: 0.79.3 242 242 version: 0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10) 243 + react-native-draggable-flatlist: 244 + specifier: ^4.0.3 245 + version: 4.0.3(@babel/core@7.26.0)(react-native-gesture-handler@2.26.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native-reanimated@3.18.0(@babel/core@7.26.0)(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)) 243 246 react-native-edge-to-edge: 244 247 specifier: ^1.6.2 245 248 version: 1.6.2(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) ··· 264 267 react-native-screens: 265 268 specifier: ~4.11.1 266 269 version: 4.11.1(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) 270 + react-native-sortables: 271 + specifier: ^1.9.4 272 + version: 1.9.4(react-native-gesture-handler@2.26.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native-reanimated@3.18.0(@babel/core@7.26.0)(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) 267 273 react-native-svg: 268 274 specifier: 15.12.0 269 275 version: 15.12.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) ··· 540 546 '@types/sdp-transform': 541 547 specifier: ^2.15.0 542 548 version: 2.15.0 549 + i18next-cli: 550 + specifier: ^1.32.0 551 + version: 1.32.0(@swc/helpers@0.5.17)(@types/node@22.15.17) 543 552 nodemon: 544 553 specifier: ^3.1.10 545 554 version: 3.1.10 546 555 tsup: 547 556 specifier: ^8.5.0 548 - version: 8.5.0(@swc/core@1.8.0(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.3)(typescript@5.8.3)(yaml@2.5.1) 557 + version: 8.5.0(@swc/core@1.15.4(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.3)(typescript@5.8.3)(yaml@2.8.2) 549 558 550 559 js/config-react-native-webrtc: 551 560 dependencies: ··· 610 619 version: 7.5.0(@electron/fuses@1.8.0) 611 620 '@electron-forge/plugin-webpack': 612 621 specifier: ^7.5.0 613 - version: 7.5.0(@swc/core@1.8.0(@swc/helpers@0.5.17))(bufferutil@4.0.8)(utf-8-validate@5.0.10) 622 + version: 7.5.0(@swc/core@1.15.4(@swc/helpers@0.5.17))(bufferutil@4.0.8)(utf-8-validate@5.0.10) 614 623 '@electron-forge/publisher-s3': 615 624 specifier: ^7.5.0 616 625 version: 7.5.0 ··· 625 634 version: 0.5.10 626 635 '@typescript-eslint/eslint-plugin': 627 636 specifier: ^8.13.0 628 - version: 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 637 + version: 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3))(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3) 629 638 '@typescript-eslint/parser': 630 639 specifier: ^8.13.0 631 - version: 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 640 + version: 8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3) 632 641 '@vercel/webpack-asset-relocator-loader': 633 642 specifier: 1.7.3 634 643 version: 1.7.3 635 644 css-loader: 636 645 specifier: ^7.1.2 637 - version: 7.1.2(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))) 646 + version: 7.1.2(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))) 638 647 electron: 639 648 specifier: 33.0.2 640 649 version: 33.0.2 641 650 eslint: 642 651 specifier: ^9.14.0 643 - version: 9.14.0(jiti@2.4.2) 652 + version: 9.14.0(jiti@2.6.1) 644 653 eslint-plugin-import: 645 654 specifier: ^2.31.0 646 - version: 2.31.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.14.0(jiti@2.4.2)) 655 + version: 2.31.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3))(eslint@9.14.0(jiti@2.6.1)) 647 656 fork-ts-checker-webpack-plugin: 648 657 specifier: ^9.0.2 649 - version: 9.0.2(typescript@5.6.3)(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))) 658 + version: 9.0.2(typescript@5.6.3)(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))) 650 659 node-loader: 651 660 specifier: ^2.0.0 652 - version: 2.0.0(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))) 661 + version: 2.0.0(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))) 653 662 style-loader: 654 663 specifier: ^4.0.0 655 - version: 4.0.0(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))) 664 + version: 4.0.0(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))) 656 665 ts-loader: 657 666 specifier: ^9.5.1 658 - version: 9.5.1(typescript@5.6.3)(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))) 667 + version: 9.5.1(typescript@5.6.3)(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))) 659 668 ts-node: 660 669 specifier: ^10.9.2 661 - version: 10.9.2(@swc/core@1.8.0(@swc/helpers@0.5.17))(@types/node@22.15.17)(typescript@5.6.3) 670 + version: 10.9.2(@swc/core@1.15.4(@swc/helpers@0.5.17))(@types/node@22.15.17)(typescript@5.6.3) 662 671 typescript: 663 672 specifier: ~5.6.3 664 673 version: 5.6.3 ··· 697 706 dependencies: 698 707 '@astrojs/starlight': 699 708 specifier: ^0.34.1 700 - version: 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 709 + version: 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2)) 701 710 '@fontsource/atkinson-hyperlegible-next': 702 711 specifier: ^5.2.2 703 712 version: 5.2.2 ··· 706 715 version: link:../app 707 716 astro: 708 717 specifier: ^5.6.1 709 - version: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 718 + version: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2) 710 719 sharp: 711 720 specifier: ^0.32.5 712 721 version: 0.32.6 713 722 starlight-openapi: 714 723 specifier: ^0.17.0 715 - version: 0.17.0(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3) 724 + version: 0.17.0(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2))(openapi-types@12.1.3) 716 725 starlight-openapi-rapidoc: 717 726 specifier: ^0.8.1-beta 718 - version: 0.8.1-beta(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3) 727 + version: 0.8.1-beta(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2))(openapi-types@12.1.3) 719 728 streamplace: 720 729 specifier: workspace:* 721 730 version: link:../streamplace ··· 1803 1812 '@craftzdog/react-native-buffer@6.0.5': 1804 1813 resolution: {integrity: sha512-Av+YqfwA9e7jhgI9GFE/gTpwl/H+dRRLmZyJPOpKTy107j9Oj7oXlm3/YiMNz+C/CEGqcKAOqnXDLs4OL6AAFw==} 1805 1814 1815 + '@croct/json5-parser@0.2.2': 1816 + resolution: {integrity: sha512-0NJMLrbeLbQ0eCVj3UoH/kG2QckUgOASfwmfDTjyW1xAYPyTNJXcWVT/dssJdTJd0pRchW+qF0VFWQHcxs1OVw==} 1817 + 1818 + '@croct/json@2.1.0': 1819 + resolution: {integrity: sha512-UrWfjNQVlBxN+OVcFwHmkjARMW55MBN04E9KfGac8ac8z1QnFVuiOOFtMWXCk3UwsyRqhsNaFoYLZC+xxqsVjQ==} 1820 + 1806 1821 '@cspotcode/source-map-support@0.8.1': 1807 1822 resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 1808 1823 engines: {node: '>=12'} ··· 2776 2791 cpu: [x64] 2777 2792 os: [win32] 2778 2793 2794 + '@inquirer/ansi@2.0.2': 2795 + resolution: {integrity: sha512-SYLX05PwJVnW+WVegZt1T4Ip1qba1ik+pNJPDiqvk6zS5Y/i8PhRzLpGEtVd7sW0G8cMtkD8t4AZYhQwm8vnww==} 2796 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2797 + 2798 + '@inquirer/checkbox@5.0.3': 2799 + resolution: {integrity: sha512-xtQP2eXMFlOcAhZ4ReKP2KZvDIBb1AnCfZ81wWXG3DXLVH0f0g4obE0XDPH+ukAEMRcZT0kdX2AS1jrWGXbpxw==} 2800 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2801 + peerDependencies: 2802 + '@types/node': '>=18' 2803 + peerDependenciesMeta: 2804 + '@types/node': 2805 + optional: true 2806 + 2807 + '@inquirer/confirm@6.0.3': 2808 + resolution: {integrity: sha512-lyEvibDFL+NA5R4xl8FUmNhmu81B+LDL9L/MpKkZlQDJZXzG8InxiqYxiAlQYa9cqLLhYqKLQwZqXmSTqCLjyw==} 2809 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2810 + peerDependencies: 2811 + '@types/node': '>=18' 2812 + peerDependenciesMeta: 2813 + '@types/node': 2814 + optional: true 2815 + 2816 + '@inquirer/core@11.1.0': 2817 + resolution: {integrity: sha512-+jD/34T1pK8M5QmZD/ENhOfXdl9Zr+BrQAUc5h2anWgi7gggRq15ZbiBeLoObj0TLbdgW7TAIQRU2boMc9uOKQ==} 2818 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2819 + peerDependencies: 2820 + '@types/node': '>=18' 2821 + peerDependenciesMeta: 2822 + '@types/node': 2823 + optional: true 2824 + 2825 + '@inquirer/editor@5.0.3': 2826 + resolution: {integrity: sha512-wYyQo96TsAqIciP/r5D3cFeV8h4WqKQ/YOvTg5yOfP2sqEbVVpbxPpfV3LM5D0EP4zUI3EZVHyIUIllnoIa8OQ==} 2827 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2828 + peerDependencies: 2829 + '@types/node': '>=18' 2830 + peerDependenciesMeta: 2831 + '@types/node': 2832 + optional: true 2833 + 2834 + '@inquirer/expand@5.0.3': 2835 + resolution: {integrity: sha512-2oINvuL27ujjxd95f6K2K909uZOU2x1WiAl7Wb1X/xOtL8CgQ1kSxzykIr7u4xTkXkXOAkCuF45T588/YKee7w==} 2836 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2837 + peerDependencies: 2838 + '@types/node': '>=18' 2839 + peerDependenciesMeta: 2840 + '@types/node': 2841 + optional: true 2842 + 2843 + '@inquirer/external-editor@2.0.2': 2844 + resolution: {integrity: sha512-X/fMXK7vXomRWEex1j8mnj7s1mpnTeP4CO/h2gysJhHLT2WjBnLv4ZQEGpm/kcYI8QfLZ2fgW+9kTKD+jeopLg==} 2845 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2846 + peerDependencies: 2847 + '@types/node': '>=18' 2848 + peerDependenciesMeta: 2849 + '@types/node': 2850 + optional: true 2851 + 2852 + '@inquirer/figures@2.0.2': 2853 + resolution: {integrity: sha512-qXm6EVvQx/FmnSrCWCIGtMHwqeLgxABP8XgcaAoywsL0NFga9gD5kfG0gXiv80GjK9Hsoz4pgGwF/+CjygyV9A==} 2854 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2855 + 2856 + '@inquirer/input@5.0.3': 2857 + resolution: {integrity: sha512-4R0TdWl53dtp79Vs6Df2OHAtA2FVNqya1hND1f5wjHWxZJxwDMSNB1X5ADZJSsQKYAJ5JHCTO+GpJZ42mK0Otw==} 2858 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2859 + peerDependencies: 2860 + '@types/node': '>=18' 2861 + peerDependenciesMeta: 2862 + '@types/node': 2863 + optional: true 2864 + 2865 + '@inquirer/number@4.0.3': 2866 + resolution: {integrity: sha512-TjQLe93GGo5snRlu83JxE38ZPqj5ZVggL+QqqAF2oBA5JOJoxx25GG3EGH/XN/Os5WOmKfO8iLVdCXQxXRZIMQ==} 2867 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2868 + peerDependencies: 2869 + '@types/node': '>=18' 2870 + peerDependenciesMeta: 2871 + '@types/node': 2872 + optional: true 2873 + 2874 + '@inquirer/password@5.0.3': 2875 + resolution: {integrity: sha512-rCozGbUMAHedTeYWEN8sgZH4lRCdgG/WinFkit6ZPsp8JaNg2T0g3QslPBS5XbpORyKP/I+xyBO81kFEvhBmjA==} 2876 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2877 + peerDependencies: 2878 + '@types/node': '>=18' 2879 + peerDependenciesMeta: 2880 + '@types/node': 2881 + optional: true 2882 + 2883 + '@inquirer/prompts@8.1.0': 2884 + resolution: {integrity: sha512-LsZMdKcmRNF5LyTRuZE5nWeOjganzmN3zwbtNfcs6GPh3I2TsTtF1UYZlbxVfhxd+EuUqLGs/Lm3Xt4v6Az1wA==} 2885 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2886 + peerDependencies: 2887 + '@types/node': '>=18' 2888 + peerDependenciesMeta: 2889 + '@types/node': 2890 + optional: true 2891 + 2892 + '@inquirer/rawlist@5.1.0': 2893 + resolution: {integrity: sha512-yUCuVh0jW026Gr2tZlG3kHignxcrLKDR3KBp+eUgNz+BAdSeZk0e18yt2gyBr+giYhj/WSIHCmPDOgp1mT2niQ==} 2894 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2895 + peerDependencies: 2896 + '@types/node': '>=18' 2897 + peerDependenciesMeta: 2898 + '@types/node': 2899 + optional: true 2900 + 2901 + '@inquirer/search@4.0.3': 2902 + resolution: {integrity: sha512-lzqVw0YwuKYetk5VwJ81Ba+dyVlhseHPx9YnRKQgwXdFS0kEavCz2gngnNhnMIxg8+j1N/rUl1t5s1npwa7bqg==} 2903 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2904 + peerDependencies: 2905 + '@types/node': '>=18' 2906 + peerDependenciesMeta: 2907 + '@types/node': 2908 + optional: true 2909 + 2910 + '@inquirer/select@5.0.3': 2911 + resolution: {integrity: sha512-M+ynbwS0ecQFDYMFrQrybA0qL8DV0snpc4kKevCCNaTpfghsRowRY7SlQBeIYNzHqXtiiz4RG9vTOeb/udew7w==} 2912 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2913 + peerDependencies: 2914 + '@types/node': '>=18' 2915 + peerDependenciesMeta: 2916 + '@types/node': 2917 + optional: true 2918 + 2919 + '@inquirer/type@4.0.2': 2920 + resolution: {integrity: sha512-cae7mzluplsjSdgFA6ACLygb5jC8alO0UUnFPyu0E7tNRPrL+q/f8VcSXp+cjZQ7l5CMpDpi2G1+IQvkOiL1Lw==} 2921 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 2922 + peerDependencies: 2923 + '@types/node': '>=18' 2924 + peerDependenciesMeta: 2925 + '@types/node': 2926 + optional: true 2927 + 2779 2928 '@ioredis/commands@1.3.1': 2780 2929 resolution: {integrity: sha512-bYtU8avhGIcje3IhvF9aSjsa5URMZBHnwKtOvXsT4sfYy9gppW11gLPT/9oNqlJZD47yPKveQFTAFWpHjKvUoQ==} 2781 2930 2782 2931 '@ipld/dag-cbor@7.0.3': 2783 2932 resolution: {integrity: sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA==} 2933 + 2934 + '@isaacs/balanced-match@4.0.1': 2935 + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} 2936 + engines: {node: 20 || >=22} 2937 + 2938 + '@isaacs/brace-expansion@5.0.0': 2939 + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} 2940 + engines: {node: 20 || >=22} 2784 2941 2785 2942 '@isaacs/cliui@8.0.2': 2786 2943 resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} ··· 3922 4079 '@scure/bip39@1.4.0': 3923 4080 resolution: {integrity: sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==} 3924 4081 4082 + '@sec-ant/readable-stream@0.4.1': 4083 + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} 4084 + 3925 4085 '@sentry-internal/browser-utils@8.54.0': 3926 4086 resolution: {integrity: sha512-DKWCqb4YQosKn6aD45fhKyzhkdG7N6goGFDeyTaJFREJDFVDXiNDsYZu30nJ6BxMM7uQIaARhPAC5BXfoED3pQ==} 3927 4087 engines: {node: '>=14.18'} ··· 4078 4238 '@sindresorhus/is@4.6.0': 4079 4239 resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} 4080 4240 engines: {node: '>=10'} 4241 + 4242 + '@sindresorhus/merge-streams@4.0.0': 4243 + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} 4244 + engines: {node: '>=18'} 4081 4245 4082 4246 '@sinonjs/commons@3.0.1': 4083 4247 resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} ··· 4454 4618 '@spacingbat3/lss@1.2.0': 4455 4619 resolution: {integrity: sha512-aywhxHNb6l7COooF3m439eT/6QN8E/RSl5IVboSKthMHcp0GlZYMSoS7546rqDLmFRxTD8f1tu/NIS9vtDwYAg==} 4456 4620 4457 - '@swc/core-darwin-arm64@1.8.0': 4458 - resolution: {integrity: sha512-TIus1/SE/Ud4g84hCnchcagu+LfyndSDy5r5qf64nflojejDidPU9Fp1InzQhQpEgIpntnZID/KFCP5rQnvsIw==} 4621 + '@swc/core-darwin-arm64@1.15.4': 4622 + resolution: {integrity: sha512-NU/Of+ShFGG/i0lXKsF6GaGeTBNsr9iD8uUzdXxFfGbEjTeuKNXc5CWn3/Uo4Gr4LMAGD3hsRwG2Jq5iBDMalw==} 4459 4623 engines: {node: '>=10'} 4460 4624 cpu: [arm64] 4461 4625 os: [darwin] 4462 4626 4463 - '@swc/core-darwin-x64@1.8.0': 4464 - resolution: {integrity: sha512-yCb1FHCX/HUmNRGB1X3CFJ1WPKXMosZVUe3K2TrosCGvytwgaLoW5FS0bZg5Qv6cEUERQBg75cJnOUPwLLRCVg==} 4627 + '@swc/core-darwin-x64@1.15.4': 4628 + resolution: {integrity: sha512-9oWYMZHiEfHLqjjRGrXL17I8HdAOpWK/Rps34RKQ74O+eliygi1Iyq1TDUzYqUXcNvqN2K5fHgoMLRIni41ClQ==} 4465 4629 engines: {node: '>=10'} 4466 4630 cpu: [x64] 4467 4631 os: [darwin] 4468 4632 4469 - '@swc/core-linux-arm-gnueabihf@1.8.0': 4470 - resolution: {integrity: sha512-6TdjVdiLaSW+eGiHKEojMDlx673nowrPHa6nM6toWgRzy8tIZgjPOguVKJDoMnoHuvO7SkOLCUiMRw0rTskypA==} 4633 + '@swc/core-linux-arm-gnueabihf@1.15.4': 4634 + resolution: {integrity: sha512-I1dPxXli3N1Vr71JXogUTLcspM5ICgCYaA16RE+JKchj3XKKmxLlYjwAHAA4lh/Cy486ikzACaG6pIBcegoGkg==} 4471 4635 engines: {node: '>=10'} 4472 4636 cpu: [arm] 4473 4637 os: [linux] 4474 4638 4475 - '@swc/core-linux-arm64-gnu@1.8.0': 4476 - resolution: {integrity: sha512-TU2YcTornnyZiJUabRuk7Xtvzaep11FwK77IkFomjN9/Os5s25B8ea652c2fAQMe9RsM84FPVmX303ohxavjKQ==} 4639 + '@swc/core-linux-arm64-gnu@1.15.4': 4640 + resolution: {integrity: sha512-iGpuS/2PDZ68ioAlhkxiN5M4+pB9uDJolTKk4mZ0JM29uFf9YIkiyk7Bbr2y1QtmD82rF0tDHhoG9jtnV8mZMg==} 4477 4641 engines: {node: '>=10'} 4478 4642 cpu: [arm64] 4479 4643 os: [linux] 4480 4644 4481 - '@swc/core-linux-arm64-musl@1.8.0': 4482 - resolution: {integrity: sha512-2CdPTEKxx2hJIj/B0fn8L8k2coo/FDS95smzXyi2bov5FcrP6Ohboq8roFBYgj38fkHusXjY8qt+cCH7yXWAdg==} 4645 + '@swc/core-linux-arm64-musl@1.15.4': 4646 + resolution: {integrity: sha512-Ly95wc+VXDhl08pjAoPUhVu5vNbuPMbURknRZa5QOZuiizJ6DkaSI0/zsEc26PpC6HTc4prNLY3ARVwZ7j/IJQ==} 4483 4647 engines: {node: '>=10'} 4484 4648 cpu: [arm64] 4485 4649 os: [linux] 4486 4650 4487 - '@swc/core-linux-x64-gnu@1.8.0': 4488 - resolution: {integrity: sha512-14StQBifCs/AMsySdU95OmwNJr9LOVqo6rcTFt2b7XaWpe/AyeuMJFxcndLgUewksJHpfepzCTwNdbcYmuNo6A==} 4651 + '@swc/core-linux-x64-gnu@1.15.4': 4652 + resolution: {integrity: sha512-7pIG0BnaMn4zTpHeColPwyrWoTY9Drr+ISZQIgYHUKh3oaPtNCrXb289ScGbPPPjLsSfcGTeOy2pXmNczMC+yg==} 4489 4653 engines: {node: '>=10'} 4490 4654 cpu: [x64] 4491 4655 os: [linux] 4492 4656 4493 - '@swc/core-linux-x64-musl@1.8.0': 4494 - resolution: {integrity: sha512-qemJnAQlYqKCfWNqVv5SG8uGvw8JotwU86cuFUkq35oTB+dsSFM3b83+B1giGTKKFOh2nfWT7bvPXTKk+aUjew==} 4657 + '@swc/core-linux-x64-musl@1.15.4': 4658 + resolution: {integrity: sha512-oaqTV25V9H+PpSkvTcK25q6Q56FvXc6d2xBu486dv9LAPCHWgeAworE8WpBLV26g8rubcN5nGhO5HwSunXA7Ww==} 4495 4659 engines: {node: '>=10'} 4496 4660 cpu: [x64] 4497 4661 os: [linux] 4498 4662 4499 - '@swc/core-win32-arm64-msvc@1.8.0': 4500 - resolution: {integrity: sha512-fXt5vZbnrVdXZzGj2qRnZtY3uh+NtLCaFjS2uD9w8ssdbjhbDZYlJCj2JINOjv35ttEfAD2goiYmVa5P/Ypl+g==} 4663 + '@swc/core-win32-arm64-msvc@1.15.4': 4664 + resolution: {integrity: sha512-VcPuUJw27YbGo1HcOaAriI50dpM3ZZeDW3x2cMnJW6vtkeyzUFk1TADmTwFax0Fn+yicCxhaWjnFE3eAzGAxIQ==} 4501 4665 engines: {node: '>=10'} 4502 4666 cpu: [arm64] 4503 4667 os: [win32] 4504 4668 4505 - '@swc/core-win32-ia32-msvc@1.8.0': 4506 - resolution: {integrity: sha512-W4FA2vSJ+bGYiTj6gspxghSdKQNLfLMo65AH07u797x7I+YJj8amnFY/fQRlroDv5Dez/FHTv14oPlTlNFUpIw==} 4669 + '@swc/core-win32-ia32-msvc@1.15.4': 4670 + resolution: {integrity: sha512-dREjghAZEuKAK9nQzJETAiCSihSpAVS6Vk9+y2ElaoeTj68tNB1txV/m1RTPPD/+Kgbz6ITPNyXRWxPdkP5aXw==} 4507 4671 engines: {node: '>=10'} 4508 4672 cpu: [ia32] 4509 4673 os: [win32] 4510 4674 4511 - '@swc/core-win32-x64-msvc@1.8.0': 4512 - resolution: {integrity: sha512-Il4y8XwKDV0Bnk0IpA00kGcSQC6I9XOIinW5egTutnwIDfDE+qsD0j+0isW5H76GetY3/Ze0lVxeOXLAUgpegA==} 4675 + '@swc/core-win32-x64-msvc@1.15.4': 4676 + resolution: {integrity: sha512-o/odIBuQkoxKbRweJWOMI9LeRSOenFKN2zgPeaaNQ/cyuVk2r6DCAobKMOodvDdZWlMn6N1xJrldeCRSTZIgiQ==} 4513 4677 engines: {node: '>=10'} 4514 4678 cpu: [x64] 4515 4679 os: [win32] 4516 4680 4517 - '@swc/core@1.8.0': 4518 - resolution: {integrity: sha512-EF8C5lp1RKMp3426tAKwQyVbg4Zcn/2FDax3cz8EcOXYQJM/ctB687IvBm9Ciej1wMcQ/dMRg+OB4Xl8BGLBoA==} 4681 + '@swc/core@1.15.4': 4682 + resolution: {integrity: sha512-fH81BPo6EiJ7BUb6Qa5SY/NLWIRVambqU3740g0XPFPEz5KFPnzRYpR6zodQNOcEb9XUtZzRO1Y0WyIJP7iBxQ==} 4519 4683 engines: {node: '>=10'} 4520 4684 peerDependencies: 4521 - '@swc/helpers': '*' 4685 + '@swc/helpers': '>=0.5.17' 4522 4686 peerDependenciesMeta: 4523 4687 '@swc/helpers': 4524 4688 optional: true ··· 4529 4693 '@swc/helpers@0.5.17': 4530 4694 resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} 4531 4695 4532 - '@swc/types@0.1.14': 4533 - resolution: {integrity: sha512-PbSmTiYCN+GMrvfjrMo9bdY+f2COnwbdnoMw7rqU/PI5jXpKjxOGZ0qqZCImxnT81NkNsKnmEpvu+hRXLBeCJg==} 4696 + '@swc/types@0.1.25': 4697 + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} 4534 4698 4535 4699 '@szmarczak/http-timer@4.0.6': 4536 4700 resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} ··· 5723 5887 resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} 5724 5888 engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 5725 5889 5890 + chalk@5.6.2: 5891 + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} 5892 + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 5893 + 5726 5894 character-entities-html4@2.1.0: 5727 5895 resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} 5728 5896 ··· 5738 5906 chardet@0.7.0: 5739 5907 resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} 5740 5908 5909 + chardet@2.1.1: 5910 + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} 5911 + 5741 5912 cheerio-select@2.1.0: 5742 5913 resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} 5743 5914 ··· 5753 5924 resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 5754 5925 engines: {node: '>= 14.16.0'} 5755 5926 5927 + chokidar@5.0.0: 5928 + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} 5929 + engines: {node: '>= 20.19.0'} 5930 + 5756 5931 chownr@1.1.4: 5757 5932 resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} 5758 5933 ··· 5833 6008 resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} 5834 6009 engines: {node: '>=6'} 5835 6010 6011 + cli-spinners@3.3.0: 6012 + resolution: {integrity: sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==} 6013 + engines: {node: '>=18.20'} 6014 + 5836 6015 cli-truncate@3.1.0: 5837 6016 resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} 5838 6017 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} ··· 5845 6024 resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} 5846 6025 engines: {node: '>= 10'} 5847 6026 6027 + cli-width@4.1.0: 6028 + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} 6029 + engines: {node: '>= 12'} 6030 + 5848 6031 cliui@6.0.0: 5849 6032 resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} 5850 6033 ··· 5961 6144 commander@12.1.0: 5962 6145 resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} 5963 6146 engines: {node: '>=18'} 6147 + 6148 + commander@14.0.2: 6149 + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} 6150 + engines: {node: '>=20'} 5964 6151 5965 6152 commander@2.20.3: 5966 6153 resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} ··· 6143 6330 resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 6144 6331 engines: {node: '>= 8'} 6145 6332 6333 + cross-spawn@7.0.6: 6334 + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 6335 + engines: {node: '>= 8'} 6336 + 6146 6337 cross-zip@4.0.1: 6147 6338 resolution: {integrity: sha512-n63i0lZ0rvQ6FXiGQ+/JFCKAUyPFhLQYJIqKaa+tSJtfKeULF/IDNDAbdnSIxgS4NTuw2b0+lj8LzfITuq+ZxQ==} 6148 6339 engines: {node: '>=12.10'} ··· 6886 7077 resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} 6887 7078 engines: {node: '>=16.17'} 6888 7079 7080 + execa@9.6.1: 7081 + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} 7082 + engines: {node: ^18.19.0 || >=20.5.0} 7083 + 6889 7084 expand-template@2.0.3: 6890 7085 resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} 6891 7086 engines: {node: '>=6'} ··· 7177 7372 figures@3.2.0: 7178 7373 resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} 7179 7374 engines: {node: '>=8'} 7375 + 7376 + figures@6.1.0: 7377 + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} 7378 + engines: {node: '>=18'} 7180 7379 7181 7380 file-entry-cache@8.0.0: 7182 7381 resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} ··· 7440 7639 resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} 7441 7640 engines: {node: '>=18'} 7442 7641 7642 + get-east-asian-width@1.4.0: 7643 + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} 7644 + engines: {node: '>=18'} 7645 + 7443 7646 get-folder-size@2.0.1: 7444 7647 resolution: {integrity: sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA==} 7445 7648 hasBin: true ··· 7508 7711 resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} 7509 7712 engines: {node: '>=16'} 7510 7713 7714 + get-stream@9.0.1: 7715 + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} 7716 + engines: {node: '>=18'} 7717 + 7511 7718 get-symbol-description@1.0.2: 7512 7719 resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} 7513 7720 engines: {node: '>= 0.4'} ··· 7570 7777 glob@10.4.5: 7571 7778 resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 7572 7779 hasBin: true 7780 + 7781 + glob@13.0.0: 7782 + resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} 7783 + engines: {node: 20 || >=22} 7573 7784 7574 7785 glob@7.2.3: 7575 7786 resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} ··· 7920 8131 resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} 7921 8132 engines: {node: '>=16.17.0'} 7922 8133 8134 + human-signals@8.0.1: 8135 + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} 8136 + engines: {node: '>=18.18.0'} 8137 + 7923 8138 humanize-ms@1.2.1: 7924 8139 resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} 7925 8140 ··· 7934 8149 i18next-browser-languagedetector@8.2.0: 7935 8150 resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==} 7936 8151 8152 + i18next-cli@1.32.0: 8153 + resolution: {integrity: sha512-Shn31g/rhWUbg2SzPXuaqgUfL5K8IZ+USUIcoNkg6nOCBog3l0aeklWq205a04TxTAEb9YaGCQbD9EC+Jg70OA==} 8154 + engines: {node: '>=22'} 8155 + hasBin: true 8156 + 7937 8157 i18next-fluent@2.0.0: 7938 8158 resolution: {integrity: sha512-k69kvftj02YNMtls1oy5TkbsjEDUlkfXcApBIv3gCnQTTDPTNVekweIGC6V5m/yC2ce6SNqkzlPpdB24OgEEgg==} 7939 8159 ··· 7945 8165 engines: {node: ^18.0.0 || ^20.0.0 || ^22.0.0, npm: '>=6', yarn: '>=1'} 7946 8166 hasBin: true 7947 8167 8168 + i18next-resources-for-ts@2.0.0: 8169 + resolution: {integrity: sha512-RvATolbJlxrwpZh2+R7ZcNtg0ewmXFFx6rdu9i2bUEBvn6ThgA82rxDe3rJQa3hFS0SopX0qPaABqVDN3TUVpw==} 8170 + hasBin: true 8171 + 7948 8172 i18next-resources-to-backend@1.2.1: 7949 8173 resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} 7950 8174 ··· 7965 8189 7966 8190 iconv-lite@0.6.3: 7967 8191 resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 8192 + engines: {node: '>=0.10.0'} 8193 + 8194 + iconv-lite@0.7.1: 8195 + resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} 7968 8196 engines: {node: '>=0.10.0'} 7969 8197 7970 8198 icss-utils@5.1.0: ··· 8063 8291 8064 8292 inline-style-prefixer@7.0.1: 8065 8293 resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} 8294 + 8295 + inquirer@13.1.0: 8296 + resolution: {integrity: sha512-4vv4GS/9HLnn0radvmHlXUXiNkd2gYCBQ4U1rxZWBJDisu2Z06bzUM9CFU8pcu1vwuAQjo6O+CFiqCYNsEi6qQ==} 8297 + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} 8298 + peerDependencies: 8299 + '@types/node': '>=18' 8300 + peerDependenciesMeta: 8301 + '@types/node': 8302 + optional: true 8066 8303 8067 8304 inquirer@8.2.6: 8068 8305 resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} ··· 8205 8442 resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} 8206 8443 engines: {node: '>=8'} 8207 8444 8445 + is-interactive@2.0.0: 8446 + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} 8447 + engines: {node: '>=12'} 8448 + 8208 8449 is-lambda@1.0.1: 8209 8450 resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} 8210 8451 ··· 8284 8525 resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} 8285 8526 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 8286 8527 8528 + is-stream@4.0.1: 8529 + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} 8530 + engines: {node: '>=18'} 8531 + 8287 8532 is-string@1.0.7: 8288 8533 resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} 8289 8534 engines: {node: '>= 0.4'} ··· 8304 8549 resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} 8305 8550 engines: {node: '>=10'} 8306 8551 8552 + is-unicode-supported@2.1.0: 8553 + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} 8554 + engines: {node: '>=18'} 8555 + 8307 8556 is-url@1.2.4: 8308 8557 resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} 8309 8558 ··· 8426 8675 resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} 8427 8676 hasBin: true 8428 8677 8678 + jiti@2.6.1: 8679 + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} 8680 + hasBin: true 8681 + 8429 8682 jose@4.15.9: 8430 8683 resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} 8431 8684 ··· 8508 8761 jsonc-parser@3.2.0: 8509 8762 resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} 8510 8763 8764 + jsonc-parser@3.3.1: 8765 + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} 8766 + 8511 8767 jsonfile@4.0.0: 8512 8768 resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 8513 8769 ··· 8890 9146 resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} 8891 9147 engines: {node: '>=10'} 8892 9148 9149 + log-symbols@7.0.1: 9150 + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} 9151 + engines: {node: '>=18'} 9152 + 8893 9153 log-update@5.0.1: 8894 9154 resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==} 8895 9155 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} ··· 8918 9178 lru-cache@10.4.3: 8919 9179 resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 8920 9180 9181 + lru-cache@11.2.4: 9182 + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} 9183 + engines: {node: 20 || >=22} 9184 + 8921 9185 lru-cache@5.1.1: 8922 9186 resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 8923 9187 ··· 9334 9598 minimalistic-crypto-utils@1.0.1: 9335 9599 resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} 9336 9600 9601 + minimatch@10.1.1: 9602 + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} 9603 + engines: {node: 20 || >=22} 9604 + 9337 9605 minimatch@3.0.5: 9338 9606 resolution: {integrity: sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==} 9339 9607 ··· 9483 9751 resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} 9484 9752 engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} 9485 9753 9754 + mute-stream@3.0.0: 9755 + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} 9756 + engines: {node: ^20.17.0 || >=22.9.0} 9757 + 9486 9758 mz@2.7.0: 9487 9759 resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 9488 9760 ··· 9691 9963 resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} 9692 9964 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 9693 9965 9966 + npm-run-path@6.0.0: 9967 + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} 9968 + engines: {node: '>=18'} 9969 + 9694 9970 npmlog@6.0.2: 9695 9971 resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} 9696 9972 engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} ··· 9834 10110 resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} 9835 10111 engines: {node: '>=10'} 9836 10112 10113 + ora@9.0.0: 10114 + resolution: {integrity: sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==} 10115 + engines: {node: '>=20'} 10116 + 9837 10117 os-tmpdir@1.0.2: 9838 10118 resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} 9839 10119 engines: {node: '>=0.10.0'} ··· 10002 10282 parse-latin@7.0.0: 10003 10283 resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} 10004 10284 10285 + parse-ms@4.0.0: 10286 + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} 10287 + engines: {node: '>=18'} 10288 + 10005 10289 parse-passwd@1.0.0: 10006 10290 resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} 10007 10291 engines: {node: '>=0.10.0'} ··· 10075 10359 resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 10076 10360 engines: {node: '>=16 || 14 >=14.18'} 10077 10361 10362 + path-scurry@2.0.1: 10363 + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} 10364 + engines: {node: 20 || >=22} 10365 + 10078 10366 path-to-regexp@0.1.10: 10079 10367 resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} 10080 10368 ··· 10342 10630 resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} 10343 10631 engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 10344 10632 10633 + pretty-ms@9.3.0: 10634 + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} 10635 + engines: {node: '>=18'} 10636 + 10345 10637 prismjs@1.30.0: 10346 10638 resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} 10347 10639 engines: {node: '>=6'} ··· 10555 10847 react-is@18.3.1: 10556 10848 resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} 10557 10849 10850 + react-native-draggable-flatlist@4.0.3: 10851 + resolution: {integrity: sha512-2F4x5BFieWdGq9SetD2nSAR7s7oQCSgNllYgERRXXtNfSOuAGAVbDb/3H3lP0y5f7rEyNwabKorZAD/SyyNbDw==} 10852 + peerDependencies: 10853 + react-native: '>=0.64.0' 10854 + react-native-gesture-handler: '>=2.0.0' 10855 + react-native-reanimated: '>=2.8.0' 10856 + 10558 10857 react-native-edge-to-edge@1.6.0: 10559 10858 resolution: {integrity: sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og==} 10560 10859 peerDependencies: ··· 10576 10875 react: '*' 10577 10876 react-native: '*' 10578 10877 10878 + react-native-haptic-feedback@2.3.3: 10879 + resolution: {integrity: sha512-svS4D5PxfNv8o68m9ahWfwje5NqukM3qLS48+WTdhbDkNUkOhP9rDfDSRHzlhk4zq+ISjyw95EhLeh8NkKX5vQ==} 10880 + peerDependencies: 10881 + react-native: '>=0.60.0' 10882 + 10579 10883 react-native-is-edge-to-edge@1.1.7: 10580 10884 resolution: {integrity: sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w==} 10581 10885 peerDependencies: ··· 10628 10932 peerDependencies: 10629 10933 react: '*' 10630 10934 react-native: '*' 10935 + 10936 + react-native-sortables@1.9.4: 10937 + resolution: {integrity: sha512-a6hxT+gl14HA5Sm8UiLXJqF8KMEQVa+mUJd75OnzoVsmrxUDtjAatlMdV0kI9qTQDT/ZSFLPRmdUhOR762IA4g==} 10938 + peerDependencies: 10939 + react: '*' 10940 + react-native: '*' 10941 + react-native-gesture-handler: '>=2.0.0' 10942 + react-native-reanimated: '>=3.0.0' 10631 10943 10632 10944 react-native-svg@15.12.0: 10633 10945 resolution: {integrity: sha512-iE25PxIJ6V0C6krReLquVw6R0QTsRTmEQc4K2Co3P6zsimU/jltcDBKYDy1h/5j9S/fqmMeXnpM+9LEWKJKI6A==} ··· 10771 11083 readdirp@4.1.2: 10772 11084 resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} 10773 11085 engines: {node: '>= 14.18.0'} 11086 + 11087 + readdirp@5.0.0: 11088 + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} 11089 + engines: {node: '>= 20.19.0'} 10774 11090 10775 11091 real-require@0.2.0: 10776 11092 resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} ··· 11078 11394 11079 11395 run-async@2.4.1: 11080 11396 resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} 11397 + engines: {node: '>=0.12.0'} 11398 + 11399 + run-async@4.0.6: 11400 + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} 11081 11401 engines: {node: '>=0.12.0'} 11082 11402 11083 11403 run-parallel@1.2.0: ··· 11086 11406 rxjs@7.8.1: 11087 11407 resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} 11088 11408 11409 + rxjs@7.8.2: 11410 + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} 11411 + 11089 11412 safe-array-concat@1.1.2: 11090 11413 resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} 11091 11414 engines: {node: '>=0.4'} ··· 11440 11763 resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 11441 11764 engines: {node: '>= 0.8'} 11442 11765 11766 + stdin-discarder@0.2.2: 11767 + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} 11768 + engines: {node: '>=18'} 11769 + 11443 11770 stream-browserify@3.0.0: 11444 11771 resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} 11445 11772 ··· 11494 11821 resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} 11495 11822 engines: {node: '>=18'} 11496 11823 11824 + string-width@8.1.0: 11825 + resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} 11826 + engines: {node: '>=20'} 11827 + 11497 11828 string.prototype.padend@3.1.6: 11498 11829 resolution: {integrity: sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==} 11499 11830 engines: {node: '>= 0.4'} ··· 11530 11861 resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 11531 11862 engines: {node: '>=12'} 11532 11863 11864 + strip-ansi@7.1.2: 11865 + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} 11866 + engines: {node: '>=12'} 11867 + 11533 11868 strip-bom@3.0.0: 11534 11869 resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} 11535 11870 engines: {node: '>=4'} ··· 11550 11885 resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} 11551 11886 engines: {node: '>=12'} 11552 11887 11888 + strip-final-newline@4.0.0: 11889 + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} 11890 + engines: {node: '>=18'} 11891 + 11553 11892 strip-indent@3.0.0: 11554 11893 resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} 11555 11894 engines: {node: '>=8'} ··· 11642 11981 supports-preserve-symlinks-flag@1.0.0: 11643 11982 resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 11644 11983 engines: {node: '>= 0.4'} 11984 + 11985 + swc-walk@1.0.1: 11986 + resolution: {integrity: sha512-bHR0Zs+MdFxKKq5QXmPZuvbXybAJh4wV56zZT7n7hQC55eHpGvL1TeeHxNwL5XlXYSAXKK57GsKY0aEttGDuWQ==} 11987 + engines: {node: '>=20.2.0'} 11645 11988 11646 11989 symlink-or-copy@1.3.1: 11647 11990 resolution: {integrity: sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==} ··· 12107 12450 unicode-trie@2.0.0: 12108 12451 resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} 12109 12452 12453 + unicorn-magic@0.3.0: 12454 + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} 12455 + engines: {node: '>=18'} 12456 + 12110 12457 unified@11.0.5: 12111 12458 resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} 12112 12459 ··· 12630 12977 resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} 12631 12978 engines: {node: '>=18'} 12632 12979 12980 + wrap-ansi@9.0.2: 12981 + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} 12982 + engines: {node: '>=18'} 12983 + 12633 12984 wrappy@1.0.2: 12634 12985 resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 12635 12986 ··· 12752 13103 engines: {node: '>= 14'} 12753 13104 hasBin: true 12754 13105 13106 + yaml@2.8.2: 13107 + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} 13108 + engines: {node: '>= 14.6'} 13109 + hasBin: true 13110 + 12755 13111 yargs-parser@18.1.3: 12756 13112 resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} 12757 13113 engines: {node: '>=6'} ··· 12893 13249 transitivePeerDependencies: 12894 13250 - supports-color 12895 13251 12896 - '@astrojs/mdx@4.2.6(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))': 13252 + '@astrojs/mdx@4.2.6(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2))': 12897 13253 dependencies: 12898 13254 '@astrojs/markdown-remark': 6.3.1 12899 13255 '@mdx-js/mdx': 3.1.0(acorn@8.14.1) 12900 13256 acorn: 8.14.1 12901 - astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 13257 + astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2) 12902 13258 es-module-lexer: 1.7.0 12903 13259 estree-util-visit: 2.0.0 12904 13260 hast-util-to-html: 9.0.5 ··· 12922 13278 stream-replace-string: 2.0.0 12923 13279 zod: 3.24.4 12924 13280 12925 - '@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))': 13281 + '@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2))': 12926 13282 dependencies: 12927 13283 '@astrojs/markdown-remark': 6.3.1 12928 - '@astrojs/mdx': 4.2.6(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 13284 + '@astrojs/mdx': 4.2.6(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2)) 12929 13285 '@astrojs/sitemap': 3.3.1 12930 13286 '@pagefind/default-ui': 1.3.0 12931 13287 '@types/hast': 3.0.4 12932 13288 '@types/js-yaml': 4.0.9 12933 13289 '@types/mdast': 4.0.4 12934 - astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 12935 - astro-expressive-code: 0.41.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 13290 + astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2) 13291 + astro-expressive-code: 0.41.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2)) 12936 13292 bcp-47: 2.1.0 12937 13293 hast-util-from-html: 2.0.3 12938 13294 hast-util-select: 6.0.4 ··· 14936 15292 - react 14937 15293 - react-native 14938 15294 15295 + '@croct/json5-parser@0.2.2': 15296 + dependencies: 15297 + '@croct/json': 2.1.0 15298 + 15299 + '@croct/json@2.1.0': {} 15300 + 14939 15301 '@cspotcode/source-map-support@0.8.1': 14940 15302 dependencies: 14941 15303 '@jridgewell/trace-mapping': 0.3.9 ··· 15138 15500 - bluebird 15139 15501 - supports-color 15140 15502 15141 - '@electron-forge/plugin-webpack@7.5.0(@swc/core@1.8.0(@swc/helpers@0.5.17))(bufferutil@4.0.8)(utf-8-validate@5.0.10)': 15503 + '@electron-forge/plugin-webpack@7.5.0(@swc/core@1.15.4(@swc/helpers@0.5.17))(bufferutil@4.0.8)(utf-8-validate@5.0.10)': 15142 15504 dependencies: 15143 15505 '@electron-forge/core-utils': 7.5.0 15144 15506 '@electron-forge/plugin-base': 7.5.0 ··· 15148 15510 debug: 4.4.0(supports-color@5.5.0) 15149 15511 fast-glob: 3.3.2 15150 15512 fs-extra: 10.1.0 15151 - html-webpack-plugin: 5.6.0(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))) 15513 + html-webpack-plugin: 5.6.0(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))) 15152 15514 listr2: 7.0.2 15153 - webpack: 5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17)) 15154 - webpack-dev-server: 4.15.2(bufferutil@4.0.8)(debug@4.4.0)(utf-8-validate@5.0.10)(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))) 15515 + webpack: 5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17)) 15516 + webpack-dev-server: 4.15.2(bufferutil@4.0.8)(debug@4.4.0)(utf-8-validate@5.0.10)(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))) 15155 15517 webpack-merge: 5.10.0 15156 15518 transitivePeerDependencies: 15157 15519 - '@rspack/core' ··· 15484 15846 '@esbuild/win32-x64@0.25.3': 15485 15847 optional: true 15486 15848 15487 - '@eslint-community/eslint-utils@4.4.1(eslint@9.14.0(jiti@2.4.2))': 15849 + '@eslint-community/eslint-utils@4.4.1(eslint@9.14.0(jiti@2.6.1))': 15488 15850 dependencies: 15489 - eslint: 9.14.0(jiti@2.4.2) 15851 + eslint: 9.14.0(jiti@2.6.1) 15490 15852 eslint-visitor-keys: 3.4.3 15491 15853 15492 15854 '@eslint-community/regexpp@4.12.1': {} ··· 16496 16858 '@img/sharp-win32-x64@0.33.5': 16497 16859 optional: true 16498 16860 16861 + '@inquirer/ansi@2.0.2': {} 16862 + 16863 + '@inquirer/checkbox@5.0.3(@types/node@22.15.17)': 16864 + dependencies: 16865 + '@inquirer/ansi': 2.0.2 16866 + '@inquirer/core': 11.1.0(@types/node@22.15.17) 16867 + '@inquirer/figures': 2.0.2 16868 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 16869 + optionalDependencies: 16870 + '@types/node': 22.15.17 16871 + 16872 + '@inquirer/confirm@6.0.3(@types/node@22.15.17)': 16873 + dependencies: 16874 + '@inquirer/core': 11.1.0(@types/node@22.15.17) 16875 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 16876 + optionalDependencies: 16877 + '@types/node': 22.15.17 16878 + 16879 + '@inquirer/core@11.1.0(@types/node@22.15.17)': 16880 + dependencies: 16881 + '@inquirer/ansi': 2.0.2 16882 + '@inquirer/figures': 2.0.2 16883 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 16884 + cli-width: 4.1.0 16885 + mute-stream: 3.0.0 16886 + signal-exit: 4.1.0 16887 + wrap-ansi: 9.0.2 16888 + optionalDependencies: 16889 + '@types/node': 22.15.17 16890 + 16891 + '@inquirer/editor@5.0.3(@types/node@22.15.17)': 16892 + dependencies: 16893 + '@inquirer/core': 11.1.0(@types/node@22.15.17) 16894 + '@inquirer/external-editor': 2.0.2(@types/node@22.15.17) 16895 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 16896 + optionalDependencies: 16897 + '@types/node': 22.15.17 16898 + 16899 + '@inquirer/expand@5.0.3(@types/node@22.15.17)': 16900 + dependencies: 16901 + '@inquirer/core': 11.1.0(@types/node@22.15.17) 16902 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 16903 + optionalDependencies: 16904 + '@types/node': 22.15.17 16905 + 16906 + '@inquirer/external-editor@2.0.2(@types/node@22.15.17)': 16907 + dependencies: 16908 + chardet: 2.1.1 16909 + iconv-lite: 0.7.1 16910 + optionalDependencies: 16911 + '@types/node': 22.15.17 16912 + 16913 + '@inquirer/figures@2.0.2': {} 16914 + 16915 + '@inquirer/input@5.0.3(@types/node@22.15.17)': 16916 + dependencies: 16917 + '@inquirer/core': 11.1.0(@types/node@22.15.17) 16918 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 16919 + optionalDependencies: 16920 + '@types/node': 22.15.17 16921 + 16922 + '@inquirer/number@4.0.3(@types/node@22.15.17)': 16923 + dependencies: 16924 + '@inquirer/core': 11.1.0(@types/node@22.15.17) 16925 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 16926 + optionalDependencies: 16927 + '@types/node': 22.15.17 16928 + 16929 + '@inquirer/password@5.0.3(@types/node@22.15.17)': 16930 + dependencies: 16931 + '@inquirer/ansi': 2.0.2 16932 + '@inquirer/core': 11.1.0(@types/node@22.15.17) 16933 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 16934 + optionalDependencies: 16935 + '@types/node': 22.15.17 16936 + 16937 + '@inquirer/prompts@8.1.0(@types/node@22.15.17)': 16938 + dependencies: 16939 + '@inquirer/checkbox': 5.0.3(@types/node@22.15.17) 16940 + '@inquirer/confirm': 6.0.3(@types/node@22.15.17) 16941 + '@inquirer/editor': 5.0.3(@types/node@22.15.17) 16942 + '@inquirer/expand': 5.0.3(@types/node@22.15.17) 16943 + '@inquirer/input': 5.0.3(@types/node@22.15.17) 16944 + '@inquirer/number': 4.0.3(@types/node@22.15.17) 16945 + '@inquirer/password': 5.0.3(@types/node@22.15.17) 16946 + '@inquirer/rawlist': 5.1.0(@types/node@22.15.17) 16947 + '@inquirer/search': 4.0.3(@types/node@22.15.17) 16948 + '@inquirer/select': 5.0.3(@types/node@22.15.17) 16949 + optionalDependencies: 16950 + '@types/node': 22.15.17 16951 + 16952 + '@inquirer/rawlist@5.1.0(@types/node@22.15.17)': 16953 + dependencies: 16954 + '@inquirer/core': 11.1.0(@types/node@22.15.17) 16955 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 16956 + optionalDependencies: 16957 + '@types/node': 22.15.17 16958 + 16959 + '@inquirer/search@4.0.3(@types/node@22.15.17)': 16960 + dependencies: 16961 + '@inquirer/core': 11.1.0(@types/node@22.15.17) 16962 + '@inquirer/figures': 2.0.2 16963 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 16964 + optionalDependencies: 16965 + '@types/node': 22.15.17 16966 + 16967 + '@inquirer/select@5.0.3(@types/node@22.15.17)': 16968 + dependencies: 16969 + '@inquirer/ansi': 2.0.2 16970 + '@inquirer/core': 11.1.0(@types/node@22.15.17) 16971 + '@inquirer/figures': 2.0.2 16972 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 16973 + optionalDependencies: 16974 + '@types/node': 22.15.17 16975 + 16976 + '@inquirer/type@4.0.2(@types/node@22.15.17)': 16977 + optionalDependencies: 16978 + '@types/node': 22.15.17 16979 + 16499 16980 '@ioredis/commands@1.3.1': {} 16500 16981 16501 16982 '@ipld/dag-cbor@7.0.3': 16502 16983 dependencies: 16503 16984 cborg: 1.10.2 16504 16985 multiformats: 9.9.0 16986 + 16987 + '@isaacs/balanced-match@4.0.1': {} 16988 + 16989 + '@isaacs/brace-expansion@5.0.0': 16990 + dependencies: 16991 + '@isaacs/balanced-match': 4.0.1 16505 16992 16506 16993 '@isaacs/cliui@8.0.2': 16507 16994 dependencies: ··· 16617 17104 16618 17105 '@leichtgewicht/ip-codec@2.0.5': {} 16619 17106 16620 - '@lerna/create@8.2.2(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13)(typescript@5.8.3)': 17107 + '@lerna/create@8.2.2(@swc/core@1.15.4(@swc/helpers@0.5.17))(encoding@0.1.13)(typescript@5.8.3)': 16621 17108 dependencies: 16622 17109 '@npmcli/arborist': 7.5.4 16623 17110 '@npmcli/package-json': 5.2.0 16624 17111 '@npmcli/run-script': 8.1.0 16625 - '@nx/devkit': 20.0.8(nx@20.0.8(@swc/core@1.8.0(@swc/helpers@0.5.17))) 17112 + '@nx/devkit': 20.0.8(nx@20.0.8(@swc/core@1.15.4(@swc/helpers@0.5.17))) 16626 17113 '@octokit/plugin-enterprise-rest': 6.0.1 16627 17114 '@octokit/rest': 20.1.2 16628 17115 aproba: 2.0.0 ··· 16661 17148 npm-package-arg: 11.0.2 16662 17149 npm-packlist: 8.0.2 16663 17150 npm-registry-fetch: 17.1.0 16664 - nx: 20.0.8(@swc/core@1.8.0(@swc/helpers@0.5.17)) 17151 + nx: 20.0.8(@swc/core@1.15.4(@swc/helpers@0.5.17)) 16665 17152 p-map: 4.0.0 16666 17153 p-map-series: 2.1.0 16667 17154 p-queue: 6.6.2 ··· 16708 17195 dependencies: 16709 17196 cross-spawn: 7.0.3 16710 17197 16711 - '@mark.probst/typescript-json-schema@0.55.0(@swc/core@1.8.0(@swc/helpers@0.5.17))': 17198 + '@mark.probst/typescript-json-schema@0.55.0(@swc/core@1.15.4(@swc/helpers@0.5.17))': 16712 17199 dependencies: 16713 17200 '@types/json-schema': 7.0.15 16714 17201 '@types/node': 16.18.126 16715 17202 glob: 7.2.3 16716 17203 path-equal: 1.2.5 16717 17204 safe-stable-stringify: 2.5.0 16718 - ts-node: 10.9.2(@swc/core@1.8.0(@swc/helpers@0.5.17))(@types/node@16.18.126)(typescript@4.9.4) 17205 + ts-node: 10.9.2(@swc/core@1.15.4(@swc/helpers@0.5.17))(@types/node@16.18.126)(typescript@4.9.4) 16719 17206 typescript: 4.9.4 16720 17207 yargs: 17.7.2 16721 17208 transitivePeerDependencies: ··· 16931 17418 - bluebird 16932 17419 - supports-color 16933 17420 16934 - '@nx/devkit@20.0.8(nx@20.0.8(@swc/core@1.8.0(@swc/helpers@0.5.17)))': 17421 + '@nx/devkit@20.0.8(nx@20.0.8(@swc/core@1.15.4(@swc/helpers@0.5.17)))': 16935 17422 dependencies: 16936 17423 ejs: 3.1.10 16937 17424 enquirer: 2.3.6 16938 17425 ignore: 5.3.1 16939 17426 minimatch: 9.0.3 16940 - nx: 20.0.8(@swc/core@1.8.0(@swc/helpers@0.5.17)) 17427 + nx: 20.0.8(@swc/core@1.15.4(@swc/helpers@0.5.17)) 16941 17428 semver: 7.7.1 16942 17429 tmp: 0.2.3 16943 17430 tslib: 2.8.1 ··· 17825 18312 '@noble/hashes': 1.5.0 17826 18313 '@scure/base': 1.1.9 17827 18314 18315 + '@sec-ant/readable-stream@0.4.1': {} 18316 + 17828 18317 '@sentry-internal/browser-utils@8.54.0': 17829 18318 dependencies: 17830 18319 '@sentry/core': 8.54.0 ··· 17999 18488 '@sinclair/typebox@0.27.8': {} 18000 18489 18001 18490 '@sindresorhus/is@4.6.0': {} 18491 + 18492 + '@sindresorhus/merge-streams@4.0.0': {} 18002 18493 18003 18494 '@sinonjs/commons@3.0.1': 18004 18495 dependencies: ··· 18613 19104 18614 19105 '@spacingbat3/lss@1.2.0': {} 18615 19106 18616 - '@swc/core-darwin-arm64@1.8.0': 19107 + '@swc/core-darwin-arm64@1.15.4': 18617 19108 optional: true 18618 19109 18619 - '@swc/core-darwin-x64@1.8.0': 19110 + '@swc/core-darwin-x64@1.15.4': 18620 19111 optional: true 18621 19112 18622 - '@swc/core-linux-arm-gnueabihf@1.8.0': 19113 + '@swc/core-linux-arm-gnueabihf@1.15.4': 18623 19114 optional: true 18624 19115 18625 - '@swc/core-linux-arm64-gnu@1.8.0': 19116 + '@swc/core-linux-arm64-gnu@1.15.4': 18626 19117 optional: true 18627 19118 18628 - '@swc/core-linux-arm64-musl@1.8.0': 19119 + '@swc/core-linux-arm64-musl@1.15.4': 18629 19120 optional: true 18630 19121 18631 - '@swc/core-linux-x64-gnu@1.8.0': 19122 + '@swc/core-linux-x64-gnu@1.15.4': 18632 19123 optional: true 18633 19124 18634 - '@swc/core-linux-x64-musl@1.8.0': 19125 + '@swc/core-linux-x64-musl@1.15.4': 18635 19126 optional: true 18636 19127 18637 - '@swc/core-win32-arm64-msvc@1.8.0': 19128 + '@swc/core-win32-arm64-msvc@1.15.4': 18638 19129 optional: true 18639 19130 18640 - '@swc/core-win32-ia32-msvc@1.8.0': 19131 + '@swc/core-win32-ia32-msvc@1.15.4': 18641 19132 optional: true 18642 19133 18643 - '@swc/core-win32-x64-msvc@1.8.0': 19134 + '@swc/core-win32-x64-msvc@1.15.4': 18644 19135 optional: true 18645 19136 18646 - '@swc/core@1.8.0(@swc/helpers@0.5.17)': 19137 + '@swc/core@1.15.4(@swc/helpers@0.5.17)': 18647 19138 dependencies: 18648 19139 '@swc/counter': 0.1.3 18649 - '@swc/types': 0.1.14 19140 + '@swc/types': 0.1.25 18650 19141 optionalDependencies: 18651 - '@swc/core-darwin-arm64': 1.8.0 18652 - '@swc/core-darwin-x64': 1.8.0 18653 - '@swc/core-linux-arm-gnueabihf': 1.8.0 18654 - '@swc/core-linux-arm64-gnu': 1.8.0 18655 - '@swc/core-linux-arm64-musl': 1.8.0 18656 - '@swc/core-linux-x64-gnu': 1.8.0 18657 - '@swc/core-linux-x64-musl': 1.8.0 18658 - '@swc/core-win32-arm64-msvc': 1.8.0 18659 - '@swc/core-win32-ia32-msvc': 1.8.0 18660 - '@swc/core-win32-x64-msvc': 1.8.0 19142 + '@swc/core-darwin-arm64': 1.15.4 19143 + '@swc/core-darwin-x64': 1.15.4 19144 + '@swc/core-linux-arm-gnueabihf': 1.15.4 19145 + '@swc/core-linux-arm64-gnu': 1.15.4 19146 + '@swc/core-linux-arm64-musl': 1.15.4 19147 + '@swc/core-linux-x64-gnu': 1.15.4 19148 + '@swc/core-linux-x64-musl': 1.15.4 19149 + '@swc/core-win32-arm64-msvc': 1.15.4 19150 + '@swc/core-win32-ia32-msvc': 1.15.4 19151 + '@swc/core-win32-x64-msvc': 1.15.4 18661 19152 '@swc/helpers': 0.5.17 18662 - optional: true 18663 19153 18664 - '@swc/counter@0.1.3': 18665 - optional: true 19154 + '@swc/counter@0.1.3': {} 18666 19155 18667 19156 '@swc/helpers@0.5.17': 18668 19157 dependencies: 18669 19158 tslib: 2.8.1 18670 19159 18671 - '@swc/types@0.1.14': 19160 + '@swc/types@0.1.25': 18672 19161 dependencies: 18673 19162 '@swc/counter': 0.1.3 18674 - optional: true 18675 19163 18676 19164 '@szmarczak/http-timer@4.0.6': 18677 19165 dependencies: ··· 18984 19472 '@types/node': 22.15.17 18985 19473 optional: true 18986 19474 18987 - '@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3)': 19475 + '@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3))(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3)': 18988 19476 dependencies: 18989 19477 '@eslint-community/regexpp': 4.12.1 18990 - '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 19478 + '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3) 18991 19479 '@typescript-eslint/scope-manager': 8.13.0 18992 - '@typescript-eslint/type-utils': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 18993 - '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 19480 + '@typescript-eslint/type-utils': 8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3) 19481 + '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3) 18994 19482 '@typescript-eslint/visitor-keys': 8.13.0 18995 - eslint: 9.14.0(jiti@2.4.2) 19483 + eslint: 9.14.0(jiti@2.6.1) 18996 19484 graphemer: 1.4.0 18997 19485 ignore: 5.3.1 18998 19486 natural-compare: 1.4.0 ··· 19002 19490 transitivePeerDependencies: 19003 19491 - supports-color 19004 19492 19005 - '@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3)': 19493 + '@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3)': 19006 19494 dependencies: 19007 19495 '@typescript-eslint/scope-manager': 8.13.0 19008 19496 '@typescript-eslint/types': 8.13.0 19009 19497 '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) 19010 19498 '@typescript-eslint/visitor-keys': 8.13.0 19011 19499 debug: 4.4.0(supports-color@5.5.0) 19012 - eslint: 9.14.0(jiti@2.4.2) 19500 + eslint: 9.14.0(jiti@2.6.1) 19013 19501 optionalDependencies: 19014 19502 typescript: 5.6.3 19015 19503 transitivePeerDependencies: ··· 19020 19508 '@typescript-eslint/types': 8.13.0 19021 19509 '@typescript-eslint/visitor-keys': 8.13.0 19022 19510 19023 - '@typescript-eslint/type-utils@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3)': 19511 + '@typescript-eslint/type-utils@8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3)': 19024 19512 dependencies: 19025 19513 '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) 19026 - '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 19514 + '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3) 19027 19515 debug: 4.4.0(supports-color@5.5.0) 19028 19516 ts-api-utils: 1.4.0(typescript@5.6.3) 19029 19517 optionalDependencies: ··· 19049 19537 transitivePeerDependencies: 19050 19538 - supports-color 19051 19539 19052 - '@typescript-eslint/utils@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3)': 19540 + '@typescript-eslint/utils@8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3)': 19053 19541 dependencies: 19054 - '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@2.4.2)) 19542 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@2.6.1)) 19055 19543 '@typescript-eslint/scope-manager': 8.13.0 19056 19544 '@typescript-eslint/types': 8.13.0 19057 19545 '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) 19058 - eslint: 9.14.0(jiti@2.4.2) 19546 + eslint: 9.14.0(jiti@2.6.1) 19059 19547 transitivePeerDependencies: 19060 19548 - supports-color 19061 19549 - typescript ··· 19487 19975 19488 19976 astring@1.9.0: {} 19489 19977 19490 - astro-expressive-code@0.41.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)): 19978 + astro-expressive-code@0.41.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2)): 19491 19979 dependencies: 19492 - astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 19980 + astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2) 19493 19981 rehype-expressive-code: 0.41.2 19494 19982 19495 - astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1): 19983 + astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2): 19496 19984 dependencies: 19497 19985 '@astrojs/compiler': 2.12.0 19498 19986 '@astrojs/internal-helpers': 0.6.1 ··· 19545 20033 unist-util-visit: 5.0.0 19546 20034 unstorage: 1.16.0(idb-keyval@6.2.1)(ioredis@5.7.0) 19547 20035 vfile: 6.0.3 19548 - vite: 6.3.4(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1) 19549 - vitefu: 1.0.6(vite@6.3.4(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1)) 20036 + vite: 6.3.4(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.8.2) 20037 + vitefu: 1.0.6(vite@6.3.4(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.8.2)) 19550 20038 xxhash-wasm: 1.1.0 19551 20039 yargs-parser: 21.1.1 19552 20040 yocto-spinner: 0.2.2 ··· 20191 20679 20192 20680 chalk@5.4.1: {} 20193 20681 20682 + chalk@5.6.2: {} 20683 + 20194 20684 character-entities-html4@2.1.0: {} 20195 20685 20196 20686 character-entities-legacy@3.0.0: {} ··· 20200 20690 character-reference-invalid@2.0.1: {} 20201 20691 20202 20692 chardet@0.7.0: {} 20693 + 20694 + chardet@2.1.1: {} 20203 20695 20204 20696 cheerio-select@2.1.0: 20205 20697 dependencies: ··· 20240 20732 dependencies: 20241 20733 readdirp: 4.1.2 20242 20734 20735 + chokidar@5.0.0: 20736 + dependencies: 20737 + readdirp: 5.0.0 20738 + 20243 20739 chownr@1.1.4: {} 20244 20740 20245 20741 chownr@2.0.0: {} ··· 20313 20809 20314 20810 cli-spinners@2.9.2: {} 20315 20811 20812 + cli-spinners@3.3.0: {} 20813 + 20316 20814 cli-truncate@3.1.0: 20317 20815 dependencies: 20318 20816 slice-ansi: 5.0.0 ··· 20324 20822 string-width: 7.2.0 20325 20823 20326 20824 cli-width@3.0.0: {} 20825 + 20826 + cli-width@4.1.0: {} 20327 20827 20328 20828 cliui@6.0.0: 20329 20829 dependencies: ··· 20435 20935 20436 20936 commander@12.1.0: {} 20437 20937 20938 + commander@14.0.2: {} 20939 + 20438 20940 commander@2.20.3: {} 20439 20941 20440 20942 commander@4.1.1: {} ··· 20640 21142 shebang-command: 2.0.0 20641 21143 which: 2.0.2 20642 21144 21145 + cross-spawn@7.0.6: 21146 + dependencies: 21147 + path-key: 3.1.1 21148 + shebang-command: 2.0.0 21149 + which: 2.0.2 21150 + 20643 21151 cross-zip@4.0.1: {} 20644 21152 20645 21153 crossws@0.3.4: ··· 20654 21162 dependencies: 20655 21163 hyphenate-style-name: 1.1.0 20656 21164 20657 - css-loader@7.1.2(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))): 21165 + css-loader@7.1.2(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))): 20658 21166 dependencies: 20659 21167 icss-utils: 5.1.0(postcss@8.5.3) 20660 21168 postcss: 8.5.3 ··· 20665 21173 postcss-value-parser: 4.2.0 20666 21174 semver: 7.7.1 20667 21175 optionalDependencies: 20668 - webpack: 5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17)) 21176 + webpack: 5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17)) 20669 21177 20670 21178 css-select@4.3.0: 20671 21179 dependencies: ··· 21296 21804 transitivePeerDependencies: 21297 21805 - supports-color 21298 21806 21299 - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0(jiti@2.4.2)): 21807 + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0(jiti@2.6.1)): 21300 21808 dependencies: 21301 21809 debug: 3.2.7 21302 21810 optionalDependencies: 21303 - '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 21304 - eslint: 9.14.0(jiti@2.4.2) 21811 + '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3) 21812 + eslint: 9.14.0(jiti@2.6.1) 21305 21813 eslint-import-resolver-node: 0.3.9 21306 21814 transitivePeerDependencies: 21307 21815 - supports-color 21308 21816 21309 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.14.0(jiti@2.4.2)): 21817 + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3))(eslint@9.14.0(jiti@2.6.1)): 21310 21818 dependencies: 21311 21819 '@rtsao/scc': 1.1.0 21312 21820 array-includes: 3.1.8 ··· 21315 21823 array.prototype.flatmap: 1.3.2 21316 21824 debug: 3.2.7 21317 21825 doctrine: 2.1.0 21318 - eslint: 9.14.0(jiti@2.4.2) 21826 + eslint: 9.14.0(jiti@2.6.1) 21319 21827 eslint-import-resolver-node: 0.3.9 21320 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0(jiti@2.4.2)) 21828 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0(jiti@2.6.1)) 21321 21829 hasown: 2.0.2 21322 21830 is-core-module: 2.15.1 21323 21831 is-glob: 4.0.3 ··· 21329 21837 string.prototype.trimend: 1.0.8 21330 21838 tsconfig-paths: 3.15.0 21331 21839 optionalDependencies: 21332 - '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 21840 + '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@2.6.1))(typescript@5.6.3) 21333 21841 transitivePeerDependencies: 21334 21842 - eslint-import-resolver-typescript 21335 21843 - eslint-import-resolver-webpack ··· 21349 21857 21350 21858 eslint-visitor-keys@4.2.0: {} 21351 21859 21352 - eslint@9.14.0(jiti@2.4.2): 21860 + eslint@9.14.0(jiti@2.6.1): 21353 21861 dependencies: 21354 - '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@2.4.2)) 21862 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@2.6.1)) 21355 21863 '@eslint-community/regexpp': 4.12.1 21356 21864 '@eslint/config-array': 0.18.0 21357 21865 '@eslint/core': 0.7.0 ··· 21387 21895 optionator: 0.9.4 21388 21896 text-table: 0.2.0 21389 21897 optionalDependencies: 21390 - jiti: 2.4.2 21898 + jiti: 2.6.1 21391 21899 transitivePeerDependencies: 21392 21900 - supports-color 21393 21901 ··· 21508 22016 signal-exit: 4.1.0 21509 22017 strip-final-newline: 3.0.0 21510 22018 22019 + execa@9.6.1: 22020 + dependencies: 22021 + '@sindresorhus/merge-streams': 4.0.0 22022 + cross-spawn: 7.0.6 22023 + figures: 6.1.0 22024 + get-stream: 9.0.1 22025 + human-signals: 8.0.1 22026 + is-plain-obj: 4.1.0 22027 + is-stream: 4.0.1 22028 + npm-run-path: 6.0.0 22029 + pretty-ms: 9.3.0 22030 + signal-exit: 4.1.0 22031 + strip-final-newline: 4.0.0 22032 + yoctocolors: 2.1.1 22033 + 21511 22034 expand-template@2.0.3: {} 21512 22035 21513 22036 expand-tilde@2.0.2: ··· 21997 22520 dependencies: 21998 22521 escape-string-regexp: 1.0.5 21999 22522 22523 + figures@6.1.0: 22524 + dependencies: 22525 + is-unicode-supported: 2.1.0 22526 + 22000 22527 file-entry-cache@8.0.0: 22001 22528 dependencies: 22002 22529 flat-cache: 4.0.1 ··· 22182 22709 cross-spawn: 7.0.3 22183 22710 signal-exit: 4.1.0 22184 22711 22185 - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.6.3)(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))): 22712 + fork-ts-checker-webpack-plugin@9.0.2(typescript@5.6.3)(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))): 22186 22713 dependencies: 22187 22714 '@babel/code-frame': 7.26.2 22188 22715 chalk: 4.1.2 ··· 22197 22724 semver: 7.7.1 22198 22725 tapable: 2.2.1 22199 22726 typescript: 5.6.3 22200 - webpack: 5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17)) 22727 + webpack: 5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17)) 22201 22728 22202 22729 form-data@2.5.1: 22203 22730 dependencies: ··· 22382 22909 get-caller-file@2.0.5: {} 22383 22910 22384 22911 get-east-asian-width@1.2.0: {} 22912 + 22913 + get-east-asian-width@1.4.0: {} 22385 22914 22386 22915 get-folder-size@2.0.1: 22387 22916 dependencies: ··· 22459 22988 22460 22989 get-stream@8.0.1: {} 22461 22990 22991 + get-stream@9.0.1: 22992 + dependencies: 22993 + '@sec-ant/readable-stream': 0.4.1 22994 + is-stream: 4.0.1 22995 + 22462 22996 get-symbol-description@1.0.2: 22463 22997 dependencies: 22464 22998 call-bind: 1.0.7 ··· 22536 23070 package-json-from-dist: 1.0.0 22537 23071 path-scurry: 1.11.1 22538 23072 23073 + glob@13.0.0: 23074 + dependencies: 23075 + minimatch: 10.1.1 23076 + minipass: 7.1.2 23077 + path-scurry: 2.0.1 23078 + 22539 23079 glob@7.2.3: 22540 23080 dependencies: 22541 23081 fs.realpath: 1.0.0 ··· 23007 23547 23008 23548 html-void-elements@3.0.0: {} 23009 23549 23010 - html-webpack-plugin@5.6.0(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))): 23550 + html-webpack-plugin@5.6.0(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))): 23011 23551 dependencies: 23012 23552 '@types/html-minifier-terser': 6.1.0 23013 23553 html-minifier-terser: 6.1.0 ··· 23015 23555 pretty-error: 4.0.0 23016 23556 tapable: 2.2.1 23017 23557 optionalDependencies: 23018 - webpack: 5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17)) 23558 + webpack: 5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17)) 23019 23559 23020 23560 html-whitespace-sensitive-tag-names@3.0.1: {} 23021 23561 ··· 23119 23659 23120 23660 human-signals@5.0.0: {} 23121 23661 23662 + human-signals@8.0.1: {} 23663 + 23122 23664 humanize-ms@1.2.1: 23123 23665 dependencies: 23124 23666 ms: 2.1.3 ··· 23131 23673 dependencies: 23132 23674 '@babel/runtime': 7.26.0 23133 23675 23676 + i18next-cli@1.32.0(@swc/helpers@0.5.17)(@types/node@22.15.17): 23677 + dependencies: 23678 + '@croct/json5-parser': 0.2.2 23679 + '@swc/core': 1.15.4(@swc/helpers@0.5.17) 23680 + chalk: 5.6.2 23681 + chokidar: 5.0.0 23682 + commander: 14.0.2 23683 + execa: 9.6.1 23684 + glob: 13.0.0 23685 + i18next-resources-for-ts: 2.0.0(@swc/helpers@0.5.17) 23686 + inquirer: 13.1.0(@types/node@22.15.17) 23687 + jiti: 2.6.1 23688 + jsonc-parser: 3.3.1 23689 + minimatch: 10.1.1 23690 + ora: 9.0.0 23691 + swc-walk: 1.0.1 23692 + transitivePeerDependencies: 23693 + - '@swc/helpers' 23694 + - '@types/node' 23695 + 23134 23696 i18next-fluent@2.0.0: 23135 23697 dependencies: 23136 23698 '@fluent/bundle': 0.13.0 ··· 23164 23726 transitivePeerDependencies: 23165 23727 - supports-color 23166 23728 23729 + i18next-resources-for-ts@2.0.0(@swc/helpers@0.5.17): 23730 + dependencies: 23731 + '@babel/runtime': 7.28.4 23732 + '@swc/core': 1.15.4(@swc/helpers@0.5.17) 23733 + chokidar: 5.0.0 23734 + yaml: 2.8.2 23735 + transitivePeerDependencies: 23736 + - '@swc/helpers' 23737 + 23167 23738 i18next-resources-to-backend@1.2.1: 23168 23739 dependencies: 23169 23740 '@babel/runtime': 7.26.0 ··· 23189 23760 safer-buffer: 2.1.2 23190 23761 23191 23762 iconv-lite@0.6.3: 23763 + dependencies: 23764 + safer-buffer: 2.1.2 23765 + 23766 + iconv-lite@0.7.1: 23192 23767 dependencies: 23193 23768 safer-buffer: 2.1.2 23194 23769 ··· 23278 23853 dependencies: 23279 23854 css-in-js-utils: 3.1.0 23280 23855 23856 + inquirer@13.1.0(@types/node@22.15.17): 23857 + dependencies: 23858 + '@inquirer/ansi': 2.0.2 23859 + '@inquirer/core': 11.1.0(@types/node@22.15.17) 23860 + '@inquirer/prompts': 8.1.0(@types/node@22.15.17) 23861 + '@inquirer/type': 4.0.2(@types/node@22.15.17) 23862 + mute-stream: 3.0.0 23863 + run-async: 4.0.6 23864 + rxjs: 7.8.2 23865 + optionalDependencies: 23866 + '@types/node': 22.15.17 23867 + 23281 23868 inquirer@8.2.6: 23282 23869 dependencies: 23283 23870 ansi-escapes: 4.3.2 ··· 23421 24008 23422 24009 is-interactive@1.0.0: {} 23423 24010 24011 + is-interactive@2.0.0: {} 24012 + 23424 24013 is-lambda@1.0.1: {} 23425 24014 23426 24015 is-my-ip-valid@1.0.1: ··· 23486 24075 23487 24076 is-stream@3.0.0: {} 23488 24077 24078 + is-stream@4.0.1: {} 24079 + 23489 24080 is-string@1.0.7: 23490 24081 dependencies: 23491 24082 has-tostringtag: 1.0.2 ··· 23503 24094 which-typed-array: 1.1.15 23504 24095 23505 24096 is-unicode-supported@0.1.0: {} 24097 + 24098 + is-unicode-supported@2.1.0: {} 23506 24099 23507 24100 is-url@1.2.4: {} 23508 24101 ··· 23656 24249 23657 24250 jiti@2.4.2: {} 23658 24251 24252 + jiti@2.6.1: {} 24253 + 23659 24254 jose@4.15.9: {} 23660 24255 23661 24256 jose@5.9.6: {} ··· 23716 24311 json5@2.2.3: {} 23717 24312 23718 24313 jsonc-parser@3.2.0: {} 24314 + 24315 + jsonc-parser@3.3.1: {} 23719 24316 23720 24317 jsonfile@4.0.0: 23721 24318 optionalDependencies: ··· 23835 24432 23836 24433 lead@4.0.0: {} 23837 24434 23838 - lerna@8.2.2(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13): 24435 + lerna@8.2.2(@swc/core@1.15.4(@swc/helpers@0.5.17))(encoding@0.1.13): 23839 24436 dependencies: 23840 - '@lerna/create': 8.2.2(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13)(typescript@5.8.3) 24437 + '@lerna/create': 8.2.2(@swc/core@1.15.4(@swc/helpers@0.5.17))(encoding@0.1.13)(typescript@5.8.3) 23841 24438 '@npmcli/arborist': 7.5.4 23842 24439 '@npmcli/package-json': 5.2.0 23843 24440 '@npmcli/run-script': 8.1.0 23844 - '@nx/devkit': 20.0.8(nx@20.0.8(@swc/core@1.8.0(@swc/helpers@0.5.17))) 24441 + '@nx/devkit': 20.0.8(nx@20.0.8(@swc/core@1.15.4(@swc/helpers@0.5.17))) 23845 24442 '@octokit/plugin-enterprise-rest': 6.0.1 23846 24443 '@octokit/rest': 20.1.2 23847 24444 aproba: 2.0.0 ··· 23886 24483 npm-package-arg: 11.0.2 23887 24484 npm-packlist: 8.0.2 23888 24485 npm-registry-fetch: 17.1.0 23889 - nx: 20.0.8(@swc/core@1.8.0(@swc/helpers@0.5.17)) 24486 + nx: 20.0.8(@swc/core@1.15.4(@swc/helpers@0.5.17)) 23890 24487 p-map: 4.0.0 23891 24488 p-map-series: 2.1.0 23892 24489 p-pipe: 3.1.0 ··· 24192 24789 chalk: 4.1.2 24193 24790 is-unicode-supported: 0.1.0 24194 24791 24792 + log-symbols@7.0.1: 24793 + dependencies: 24794 + is-unicode-supported: 2.1.0 24795 + yoctocolors: 2.1.1 24796 + 24195 24797 log-update@5.0.1: 24196 24798 dependencies: 24197 24799 ansi-escapes: 5.0.0 ··· 24223 24825 lowercase-keys@2.0.0: {} 24224 24826 24225 24827 lru-cache@10.4.3: {} 24828 + 24829 + lru-cache@11.2.4: {} 24226 24830 24227 24831 lru-cache@5.1.1: 24228 24832 dependencies: ··· 25066 25670 25067 25671 minimalistic-crypto-utils@1.0.1: {} 25068 25672 25673 + minimatch@10.1.1: 25674 + dependencies: 25675 + '@isaacs/brace-expansion': 5.0.0 25676 + 25069 25677 minimatch@3.0.5: 25070 25678 dependencies: 25071 25679 brace-expansion: 1.1.11 ··· 25212 25820 25213 25821 mute-stream@1.0.0: {} 25214 25822 25823 + mute-stream@3.0.0: {} 25824 + 25215 25825 mz@2.7.0: 25216 25826 dependencies: 25217 25827 any-promise: 1.3.0 ··· 25316 25926 25317 25927 node-int64@0.4.0: {} 25318 25928 25319 - node-loader@2.0.0(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))): 25929 + node-loader@2.0.0(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))): 25320 25930 dependencies: 25321 25931 loader-utils: 2.0.4 25322 - webpack: 5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17)) 25932 + webpack: 5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17)) 25323 25933 25324 25934 node-machine-id@1.1.12: {} 25325 25935 ··· 25449 26059 dependencies: 25450 26060 path-key: 4.0.0 25451 26061 26062 + npm-run-path@6.0.0: 26063 + dependencies: 26064 + path-key: 4.0.0 26065 + unicorn-magic: 0.3.0 26066 + 25452 26067 npmlog@6.0.2: 25453 26068 dependencies: 25454 26069 are-we-there-yet: 3.0.1 ··· 25462 26077 25463 26078 nullthrows@1.1.1: {} 25464 26079 25465 - nx@20.0.8(@swc/core@1.8.0(@swc/helpers@0.5.17)): 26080 + nx@20.0.8(@swc/core@1.15.4(@swc/helpers@0.5.17)): 25466 26081 dependencies: 25467 26082 '@napi-rs/wasm-runtime': 0.2.4 25468 26083 '@yarnpkg/lockfile': 1.1.0 ··· 25507 26122 '@nx/nx-linux-x64-musl': 20.0.8 25508 26123 '@nx/nx-win32-arm64-msvc': 20.0.8 25509 26124 '@nx/nx-win32-x64-msvc': 20.0.8 25510 - '@swc/core': 1.8.0(@swc/helpers@0.5.17) 26125 + '@swc/core': 1.15.4(@swc/helpers@0.5.17) 25511 26126 transitivePeerDependencies: 25512 26127 - debug 25513 26128 ··· 25660 26275 log-symbols: 4.1.0 25661 26276 strip-ansi: 6.0.1 25662 26277 wcwidth: 1.0.1 26278 + 26279 + ora@9.0.0: 26280 + dependencies: 26281 + chalk: 5.6.2 26282 + cli-cursor: 5.0.0 26283 + cli-spinners: 3.3.0 26284 + is-interactive: 2.0.0 26285 + is-unicode-supported: 2.1.0 26286 + log-symbols: 7.0.1 26287 + stdin-discarder: 0.2.2 26288 + string-width: 8.1.0 26289 + strip-ansi: 7.1.2 25663 26290 25664 26291 os-tmpdir@1.0.2: {} 25665 26292 ··· 25898 26525 unist-util-visit-children: 3.0.0 25899 26526 vfile: 6.0.3 25900 26527 26528 + parse-ms@4.0.0: {} 26529 + 25901 26530 parse-passwd@1.0.0: {} 25902 26531 25903 26532 parse-path@7.0.0: ··· 25962 26591 lru-cache: 10.4.3 25963 26592 minipass: 7.1.2 25964 26593 26594 + path-scurry@2.0.1: 26595 + dependencies: 26596 + lru-cache: 11.2.4 26597 + minipass: 7.1.2 26598 + 25965 26599 path-to-regexp@0.1.10: {} 25966 26600 25967 26601 path-type@2.0.0: ··· 26091 26725 26092 26726 possible-typed-array-names@1.0.0: {} 26093 26727 26094 - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.3)(yaml@2.5.1): 26728 + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.3)(yaml@2.8.2): 26095 26729 dependencies: 26096 26730 lilconfig: 3.1.3 26097 26731 optionalDependencies: 26098 - jiti: 2.4.2 26732 + jiti: 2.6.1 26099 26733 postcss: 8.5.3 26100 - yaml: 2.5.1 26734 + yaml: 2.8.2 26101 26735 26102 26736 postcss-modules-extract-imports@3.1.0(postcss@8.5.3): 26103 26737 dependencies: ··· 26194 26828 '@jest/schemas': 29.6.3 26195 26829 ansi-styles: 5.2.0 26196 26830 react-is: 18.3.1 26831 + 26832 + pretty-ms@9.3.0: 26833 + dependencies: 26834 + parse-ms: 4.0.0 26197 26835 26198 26836 prismjs@1.30.0: {} 26199 26837 ··· 26351 26989 transitivePeerDependencies: 26352 26990 - encoding 26353 26991 26354 - quicktype-typescript-input@23.2.6(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13): 26992 + quicktype-typescript-input@23.2.6(@swc/core@1.15.4(@swc/helpers@0.5.17))(encoding@0.1.13): 26355 26993 dependencies: 26356 - '@mark.probst/typescript-json-schema': 0.55.0(@swc/core@1.8.0(@swc/helpers@0.5.17)) 26994 + '@mark.probst/typescript-json-schema': 0.55.0(@swc/core@1.15.4(@swc/helpers@0.5.17)) 26357 26995 quicktype-core: 23.2.6(encoding@0.1.13) 26358 26996 typescript: 4.9.5 26359 26997 transitivePeerDependencies: ··· 26361 26999 - '@swc/wasm' 26362 27000 - encoding 26363 27001 26364 - quicktype@23.2.6(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13): 27002 + quicktype@23.2.6(@swc/core@1.15.4(@swc/helpers@0.5.17))(encoding@0.1.13): 26365 27003 dependencies: 26366 27004 '@glideapps/ts-necessities': 2.4.0 26367 27005 chalk: 4.1.2 ··· 26374 27012 moment: 2.30.1 26375 27013 quicktype-core: 23.2.6(encoding@0.1.13) 26376 27014 quicktype-graphql-input: 23.2.6(encoding@0.1.13) 26377 - quicktype-typescript-input: 23.2.6(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13) 27015 + quicktype-typescript-input: 23.2.6(@swc/core@1.15.4(@swc/helpers@0.5.17))(encoding@0.1.13) 26378 27016 readable-stream: 4.7.0 26379 27017 stream-json: 1.8.0 26380 27018 string-to-stream: 3.0.1 ··· 26464 27102 26465 27103 react-is@18.3.1: {} 26466 27104 27105 + react-native-draggable-flatlist@4.0.3(@babel/core@7.26.0)(react-native-gesture-handler@2.26.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native-reanimated@3.18.0(@babel/core@7.26.0)(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)): 27106 + dependencies: 27107 + '@babel/preset-typescript': 7.24.7(@babel/core@7.26.0) 27108 + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10) 27109 + react-native-gesture-handler: 2.26.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) 27110 + react-native-reanimated: 3.18.0(@babel/core@7.26.0)(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) 27111 + transitivePeerDependencies: 27112 + - '@babel/core' 27113 + - supports-color 27114 + 26467 27115 react-native-edge-to-edge@1.6.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0): 26468 27116 dependencies: 26469 27117 react: 19.0.0 ··· 26504 27152 react: 19.0.0 26505 27153 react-native: 0.79.3(@babel/core@7.26.0)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10) 26506 27154 27155 + react-native-haptic-feedback@2.3.3(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)): 27156 + dependencies: 27157 + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10) 27158 + optional: true 27159 + 26507 27160 react-native-is-edge-to-edge@1.1.7(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0): 26508 27161 dependencies: 26509 27162 react: 19.0.0 ··· 26622 27275 react-native-is-edge-to-edge: 1.1.7(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) 26623 27276 warn-once: 0.1.1 26624 27277 27278 + react-native-sortables@1.9.4(react-native-gesture-handler@2.26.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native-reanimated@3.18.0(@babel/core@7.26.0)(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0))(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0): 27279 + dependencies: 27280 + react: 19.0.0 27281 + react-native: 0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10) 27282 + react-native-gesture-handler: 2.26.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) 27283 + react-native-reanimated: 3.18.0(@babel/core@7.26.0)(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0) 27284 + optionalDependencies: 27285 + react-native-haptic-feedback: 2.3.3(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10)) 27286 + 26625 27287 react-native-svg@15.12.0(react-native@0.79.3(@babel/core@7.26.0)(@types/react@18.3.12)(bufferutil@4.0.8)(react@19.0.0)(utf-8-validate@5.0.10))(react@19.0.0): 26626 27288 dependencies: 26627 27289 css-select: 5.1.0 ··· 26900 27562 picomatch: 2.3.1 26901 27563 26902 27564 readdirp@4.1.2: {} 27565 + 27566 + readdirp@5.0.0: {} 26903 27567 26904 27568 real-require@0.2.0: {} 26905 27569 ··· 27305 27969 27306 27970 run-async@2.4.1: {} 27307 27971 27972 + run-async@4.0.6: {} 27973 + 27308 27974 run-parallel@1.2.0: 27309 27975 dependencies: 27310 27976 queue-microtask: 1.2.3 27311 27977 27312 27978 rxjs@7.8.1: 27979 + dependencies: 27980 + tslib: 2.8.1 27981 + 27982 + rxjs@7.8.2: 27313 27983 dependencies: 27314 27984 tslib: 2.8.1 27315 27985 ··· 27735 28405 27736 28406 standard-as-callback@2.1.0: {} 27737 28407 27738 - starlight-openapi-rapidoc@0.8.1-beta(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3): 28408 + starlight-openapi-rapidoc@0.8.1-beta(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2))(openapi-types@12.1.3): 27739 28409 dependencies: 27740 - '@astrojs/starlight': 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 28410 + '@astrojs/starlight': 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2)) 27741 28411 '@astropub/md': 0.4.0 27742 28412 '@readme/openapi-parser': 2.5.0(openapi-types@12.1.3) 27743 - astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 28413 + astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2) 27744 28414 github-slugger: 2.0.0 27745 28415 transitivePeerDependencies: 27746 28416 - '@astrojs/markdown-remark' 27747 28417 - openapi-types 27748 28418 27749 - starlight-openapi@0.17.0(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3): 28419 + starlight-openapi@0.17.0(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2))(openapi-types@12.1.3): 27750 28420 dependencies: 27751 - '@astrojs/starlight': 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 28421 + '@astrojs/starlight': 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2)) 27752 28422 '@readme/openapi-parser': 2.7.0(openapi-types@12.1.3) 27753 - astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 28423 + astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(ioredis@5.7.0)(jiti@2.6.1)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.8.2) 27754 28424 github-slugger: 2.0.0 27755 28425 url-template: 3.1.1 27756 28426 transitivePeerDependencies: ··· 27759 28429 statuses@1.5.0: {} 27760 28430 27761 28431 statuses@2.0.1: {} 28432 + 28433 + stdin-discarder@0.2.2: {} 27762 28434 27763 28435 stream-browserify@3.0.0: 27764 28436 dependencies: ··· 27827 28499 get-east-asian-width: 1.2.0 27828 28500 strip-ansi: 7.1.0 27829 28501 28502 + string-width@8.1.0: 28503 + dependencies: 28504 + get-east-asian-width: 1.4.0 28505 + strip-ansi: 7.1.2 28506 + 27830 28507 string.prototype.padend@3.1.6: 27831 28508 dependencies: 27832 28509 call-bind: 1.0.7 ··· 27878 28555 dependencies: 27879 28556 ansi-regex: 6.0.1 27880 28557 28558 + strip-ansi@7.1.2: 28559 + dependencies: 28560 + ansi-regex: 6.0.1 28561 + 27881 28562 strip-bom@3.0.0: {} 27882 28563 27883 28564 strip-bom@4.0.0: {} ··· 27887 28568 strip-final-newline@2.0.0: {} 27888 28569 27889 28570 strip-final-newline@3.0.0: {} 28571 + 28572 + strip-final-newline@4.0.0: {} 27890 28573 27891 28574 strip-indent@3.0.0: 27892 28575 dependencies: ··· 27922 28605 stubs@3.0.0: 27923 28606 optional: true 27924 28607 27925 - style-loader@4.0.0(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))): 28608 + style-loader@4.0.0(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))): 27926 28609 dependencies: 27927 - webpack: 5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17)) 28610 + webpack: 5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17)) 27928 28611 27929 28612 style-to-js@1.1.16: 27930 28613 dependencies: ··· 27974 28657 supports-color: 7.2.0 27975 28658 27976 28659 supports-preserve-symlinks-flag@1.0.0: {} 28660 + 28661 + swc-walk@1.0.1: 28662 + dependencies: 28663 + acorn-walk: 8.3.4 27977 28664 27978 28665 symlink-or-copy@1.3.1: {} 27979 28666 ··· 28064 28751 ansi-escapes: 4.3.2 28065 28752 supports-hyperlinks: 2.3.0 28066 28753 28067 - terser-webpack-plugin@5.3.10(@swc/core@1.8.0(@swc/helpers@0.5.17))(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))): 28754 + terser-webpack-plugin@5.3.10(@swc/core@1.15.4(@swc/helpers@0.5.17))(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))): 28068 28755 dependencies: 28069 28756 '@jridgewell/trace-mapping': 0.3.25 28070 28757 jest-worker: 27.5.1 28071 28758 schema-utils: 3.3.0 28072 28759 serialize-javascript: 6.0.2 28073 28760 terser: 5.32.0 28074 - webpack: 5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17)) 28761 + webpack: 5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17)) 28075 28762 optionalDependencies: 28076 - '@swc/core': 1.8.0(@swc/helpers@0.5.17) 28763 + '@swc/core': 1.15.4(@swc/helpers@0.5.17) 28077 28764 28078 28765 terser@5.32.0: 28079 28766 dependencies: ··· 28199 28886 28200 28887 ts-interface-checker@0.1.13: {} 28201 28888 28202 - ts-loader@9.5.1(typescript@5.6.3)(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))): 28889 + ts-loader@9.5.1(typescript@5.6.3)(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))): 28203 28890 dependencies: 28204 28891 chalk: 4.1.2 28205 28892 enhanced-resolve: 5.17.1 ··· 28207 28894 semver: 7.7.1 28208 28895 source-map: 0.7.4 28209 28896 typescript: 5.6.3 28210 - webpack: 5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17)) 28897 + webpack: 5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17)) 28211 28898 28212 28899 ts-morph@24.0.0: 28213 28900 dependencies: 28214 28901 '@ts-morph/common': 0.25.0 28215 28902 code-block-writer: 13.0.3 28216 28903 28217 - ts-node@10.9.2(@swc/core@1.8.0(@swc/helpers@0.5.17))(@types/node@16.18.126)(typescript@4.9.4): 28904 + ts-node@10.9.2(@swc/core@1.15.4(@swc/helpers@0.5.17))(@types/node@16.18.126)(typescript@4.9.4): 28218 28905 dependencies: 28219 28906 '@cspotcode/source-map-support': 0.8.1 28220 28907 '@tsconfig/node10': 1.0.11 ··· 28232 28919 v8-compile-cache-lib: 3.0.1 28233 28920 yn: 3.1.1 28234 28921 optionalDependencies: 28235 - '@swc/core': 1.8.0(@swc/helpers@0.5.17) 28922 + '@swc/core': 1.15.4(@swc/helpers@0.5.17) 28236 28923 28237 - ts-node@10.9.2(@swc/core@1.8.0(@swc/helpers@0.5.17))(@types/node@22.15.17)(typescript@5.6.3): 28924 + ts-node@10.9.2(@swc/core@1.15.4(@swc/helpers@0.5.17))(@types/node@22.15.17)(typescript@5.6.3): 28238 28925 dependencies: 28239 28926 '@cspotcode/source-map-support': 0.8.1 28240 28927 '@tsconfig/node10': 1.0.11 ··· 28252 28939 v8-compile-cache-lib: 3.0.1 28253 28940 yn: 3.1.1 28254 28941 optionalDependencies: 28255 - '@swc/core': 1.8.0(@swc/helpers@0.5.17) 28942 + '@swc/core': 1.15.4(@swc/helpers@0.5.17) 28256 28943 28257 28944 tsconfck@3.1.5(typescript@5.8.3): 28258 28945 optionalDependencies: ··· 28273 28960 28274 28961 tslib@2.8.1: {} 28275 28962 28276 - tsup@8.5.0(@swc/core@1.8.0(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.3)(typescript@5.8.3)(yaml@2.5.1): 28963 + tsup@8.5.0(@swc/core@1.15.4(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.3)(typescript@5.8.3)(yaml@2.8.2): 28277 28964 dependencies: 28278 28965 bundle-require: 5.1.0(esbuild@0.25.3) 28279 28966 cac: 6.7.14 ··· 28284 28971 fix-dts-default-cjs-exports: 1.0.1 28285 28972 joycon: 3.1.1 28286 28973 picocolors: 1.1.1 28287 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.3)(yaml@2.5.1) 28974 + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.3)(yaml@2.8.2) 28288 28975 resolve-from: 5.0.0 28289 28976 rollup: 4.40.1 28290 28977 source-map: 0.8.0-beta.0 ··· 28293 28980 tinyglobby: 0.2.13 28294 28981 tree-kill: 1.2.2 28295 28982 optionalDependencies: 28296 - '@swc/core': 1.8.0(@swc/helpers@0.5.17) 28983 + '@swc/core': 1.15.4(@swc/helpers@0.5.17) 28297 28984 postcss: 8.5.3 28298 28985 typescript: 5.8.3 28299 28986 transitivePeerDependencies: ··· 28464 29151 dependencies: 28465 29152 pako: 0.2.9 28466 29153 tiny-inflate: 1.0.3 29154 + 29155 + unicorn-magic@0.3.0: {} 28467 29156 28468 29157 unified@11.0.5: 28469 29158 dependencies: ··· 28775 29464 replace-ext: 2.0.0 28776 29465 teex: 1.0.1 28777 29466 28778 - vite@6.3.4(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1): 29467 + vite@6.3.4(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.8.2): 28779 29468 dependencies: 28780 29469 esbuild: 0.25.3 28781 29470 fdir: 6.4.4(picomatch@4.0.2) ··· 28786 29475 optionalDependencies: 28787 29476 '@types/node': 22.15.17 28788 29477 fsevents: 2.3.3 28789 - jiti: 2.4.2 29478 + jiti: 2.6.1 28790 29479 lightningcss: 1.29.1 28791 29480 terser: 5.32.0 28792 - yaml: 2.5.1 29481 + yaml: 2.8.2 28793 29482 28794 - vitefu@1.0.6(vite@6.3.4(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1)): 29483 + vitefu@1.0.6(vite@6.3.4(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.8.2)): 28795 29484 optionalDependencies: 28796 - vite: 6.3.4(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1) 29485 + vite: 6.3.4(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.8.2) 28797 29486 28798 29487 vlq@1.0.1: {} 28799 29488 ··· 28844 29533 28845 29534 webidl-conversions@5.0.0: {} 28846 29535 28847 - webpack-dev-middleware@5.3.4(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))): 29536 + webpack-dev-middleware@5.3.4(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))): 28848 29537 dependencies: 28849 29538 colorette: 2.0.20 28850 29539 memfs: 3.5.3 28851 29540 mime-types: 2.1.35 28852 29541 range-parser: 1.2.1 28853 29542 schema-utils: 4.2.0 28854 - webpack: 5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17)) 29543 + webpack: 5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17)) 28855 29544 28856 - webpack-dev-server@4.15.2(bufferutil@4.0.8)(debug@4.4.0)(utf-8-validate@5.0.10)(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))): 29545 + webpack-dev-server@4.15.2(bufferutil@4.0.8)(debug@4.4.0)(utf-8-validate@5.0.10)(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))): 28857 29546 dependencies: 28858 29547 '@types/bonjour': 3.5.13 28859 29548 '@types/connect-history-api-fallback': 1.5.4 ··· 28883 29572 serve-index: 1.9.1 28884 29573 sockjs: 0.3.24 28885 29574 spdy: 4.0.2 28886 - webpack-dev-middleware: 5.3.4(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))) 29575 + webpack-dev-middleware: 5.3.4(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))) 28887 29576 ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) 28888 29577 optionalDependencies: 28889 - webpack: 5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17)) 29578 + webpack: 5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17)) 28890 29579 transitivePeerDependencies: 28891 29580 - bufferutil 28892 29581 - debug ··· 28901 29590 28902 29591 webpack-sources@3.2.3: {} 28903 29592 28904 - webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17)): 29593 + webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17)): 28905 29594 dependencies: 28906 29595 '@types/estree': 1.0.7 28907 29596 '@webassemblyjs/ast': 1.12.1 ··· 28923 29612 neo-async: 2.6.2 28924 29613 schema-utils: 3.3.0 28925 29614 tapable: 2.2.1 28926 - terser-webpack-plugin: 5.3.10(@swc/core@1.8.0(@swc/helpers@0.5.17))(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))) 29615 + terser-webpack-plugin: 5.3.10(@swc/core@1.15.4(@swc/helpers@0.5.17))(webpack@5.94.0(@swc/core@1.15.4(@swc/helpers@0.5.17))) 28927 29616 watchpack: 2.4.2 28928 29617 webpack-sources: 3.2.3 28929 29618 transitivePeerDependencies: ··· 29038 29727 string-width: 7.2.0 29039 29728 strip-ansi: 7.1.0 29040 29729 29730 + wrap-ansi@9.0.2: 29731 + dependencies: 29732 + ansi-styles: 6.2.1 29733 + string-width: 7.2.0 29734 + strip-ansi: 7.1.0 29735 + 29041 29736 wrappy@1.0.2: {} 29042 29737 29043 29738 write-file-atomic@2.4.3: ··· 29129 29824 yallist@5.0.0: {} 29130 29825 29131 29826 yaml@2.5.1: {} 29827 + 29828 + yaml@2.8.2: {} 29132 29829 29133 29830 yargs-parser@18.1.3: 29134 29831 dependencies: