Compare commits
68 Commits
dev
...
b5dd7dd590
| Author | SHA1 | Date | |
|---|---|---|---|
| b5dd7dd590 | |||
| b34f6b2754 | |||
| 00573580e4 | |||
| 6fd2acc332 | |||
| 2e1148e514 | |||
| cdce8d917d | |||
| bbb95ecaa3 | |||
| 10eb3e71ad | |||
| b54ec90b33 | |||
| 451a5d5130 | |||
| c6f11e63a2 | |||
| a3f76b8b41 | |||
| 8c44eaba72 | |||
| fad369b689 | |||
| 3ac3dab628 | |||
| bb15011464 | |||
| 7f88202e18 | |||
| ca7fe9442a | |||
| 5b4fcc6c9e | |||
| 039ebc4ca7 | |||
| 93a1fed958 | |||
| c91a52a31d | |||
| fc4e1281d9 | |||
| 64617f585b | |||
| 97b81b2ddd | |||
| a7421eb789 | |||
| 19ef84c9b4 | |||
| 630abbbf4e | |||
| 1c754a10d7 | |||
| bc69c0f2e6 | |||
| 8303c9c6e2 | |||
| 47d6e9f963 | |||
| ac543ac5bf | |||
| 84e2e16e49 | |||
| ef020aa8c1 | |||
| 616c6beed4 | |||
| 0853ef45e7 | |||
| 5e0531ec92 | |||
| 378aeec7e5 | |||
| d128eb6c7a | |||
| 68844226ca | |||
| 9c2d2093ec | |||
| 02b2ccdee3 | |||
| 2350123136 | |||
| 69a01db926 | |||
| 46483c7097 | |||
| 4ffe26ca56 | |||
| 760aecc376 | |||
| be65325122 | |||
| 866dfb1d8f | |||
| 5350dab324 | |||
| 9aaeddf066 | |||
| 22e8e3ed32 | |||
| a16ffa5c31 | |||
| 68745530f7 | |||
| 8072a64a41 | |||
| f3c5a75445 | |||
| c24428e944 | |||
| 7949ac8b0b | |||
| 70a94717ca | |||
| 5aa4221078 | |||
| 117bf487dc | |||
| 4c753676b0 | |||
| fc2d4adc21 | |||
| 1a16a9ec0b | |||
| 221e80ca82 | |||
| d8fb794d16 | |||
| e614840667 |
@@ -25,6 +25,7 @@ android/app/src/main/assets/public/
|
|||||||
|
|
||||||
# Web/JavaScript
|
# Web/JavaScript
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
build/
|
build/
|
||||||
.svelte-kit/
|
.svelte-kit/
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
FROM node:20-bookworm AS builder
|
FROM node:20-bookworm AS builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends curl
|
||||||
|
|
||||||
RUN npm install -g pnpm@latest
|
RUN npm install -g pnpm@latest
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -42,4 +42,6 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ const config: CapacitorConfig = {
|
|||||||
appId: "social.flotilla",
|
appId: "social.flotilla",
|
||||||
appName: "Flotilla",
|
appName: "Flotilla",
|
||||||
webDir: "build",
|
webDir: "build",
|
||||||
|
ios: {
|
||||||
|
scheme: "Flotilla Chat",
|
||||||
|
},
|
||||||
android: {
|
android: {
|
||||||
adjustMarginsForEdgeToEdge: true,
|
adjustMarginsForEdgeToEdge: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,8 +20,16 @@
|
|||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Flotilla uses the microphone for voice chat in rooms.</string>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -47,11 +55,5 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
|
||||||
<false/>
|
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>remote-notification</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
|
"livekit-client": "^2.17.2",
|
||||||
"nostr-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main",
|
"nostr-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main",
|
||||||
"nostr-tools": "^2.19.4",
|
"nostr-tools": "^2.19.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
|
|||||||
Generated
+88
@@ -134,6 +134,9 @@ importers:
|
|||||||
idb:
|
idb:
|
||||||
specifier: ^8.0.3
|
specifier: ^8.0.3
|
||||||
version: 8.0.3
|
version: 8.0.3
|
||||||
|
livekit-client:
|
||||||
|
specifier: ^2.17.2
|
||||||
|
version: 2.17.3(@types/dom-mediacapture-record@1.0.22)
|
||||||
nostr-signer-capacitor-plugin:
|
nostr-signer-capacitor-plugin:
|
||||||
specifier: github:coracle-social/nostr-signer-capacitor-plugin#main
|
specifier: github:coracle-social/nostr-signer-capacitor-plugin#main
|
||||||
version: https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1)
|
version: https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1)
|
||||||
@@ -737,6 +740,9 @@ packages:
|
|||||||
'@braintree/sanitize-url@7.1.1':
|
'@braintree/sanitize-url@7.1.1':
|
||||||
resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
|
resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
|
||||||
|
|
||||||
|
'@bufbuild/protobuf@1.10.1':
|
||||||
|
resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==}
|
||||||
|
|
||||||
'@canvas/image-data@1.1.0':
|
'@canvas/image-data@1.1.0':
|
||||||
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
|
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
|
||||||
|
|
||||||
@@ -1283,6 +1289,12 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
|
'@livekit/mutex@1.1.1':
|
||||||
|
resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==}
|
||||||
|
|
||||||
|
'@livekit/protocol@1.44.0':
|
||||||
|
resolution: {integrity: sha512-/vfhDUGcUKO8Q43r6i+5FrDhl5oZjm/X3U4x2Iciqvgn5C8qbj+57YPcWSJ1kyIZm5Cm6AV2nAPjMm3ETD/iyg==}
|
||||||
|
|
||||||
'@noble/ciphers@0.5.3':
|
'@noble/ciphers@0.5.3':
|
||||||
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
|
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
|
||||||
|
|
||||||
@@ -1823,6 +1835,9 @@ packages:
|
|||||||
'@types/cookie@0.6.0':
|
'@types/cookie@0.6.0':
|
||||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||||
|
|
||||||
|
'@types/dom-mediacapture-record@1.0.22':
|
||||||
|
resolution: {integrity: sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==}
|
||||||
|
|
||||||
'@types/eslint@9.6.1':
|
'@types/eslint@9.6.1':
|
||||||
resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
|
resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
|
||||||
|
|
||||||
@@ -3334,6 +3349,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jose@6.2.1:
|
||||||
|
resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==}
|
||||||
|
|
||||||
js-base64@3.7.8:
|
js-base64@3.7.8:
|
||||||
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
|
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
|
||||||
|
|
||||||
@@ -3438,6 +3456,11 @@ packages:
|
|||||||
linkifyjs@4.3.2:
|
linkifyjs@4.3.2:
|
||||||
resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==}
|
resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==}
|
||||||
|
|
||||||
|
livekit-client@2.17.3:
|
||||||
|
resolution: {integrity: sha512-htwsAL/BMylY/zwdcT/z00U789csbi9DldSW7DO+5tz7Q15pwu++E1X+ZdtZDfkmlysfQLLibdcqlyg9FY7veQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/dom-mediacapture-record': ^1
|
||||||
|
|
||||||
load-json-file@4.0.0:
|
load-json-file@4.0.0:
|
||||||
resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
|
resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3472,6 +3495,10 @@ packages:
|
|||||||
lodash@4.17.23:
|
lodash@4.17.23:
|
||||||
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
|
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
|
||||||
|
|
||||||
|
loglevel@1.9.2:
|
||||||
|
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
|
||||||
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
lru-cache@10.4.3:
|
lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
@@ -4273,6 +4300,9 @@ packages:
|
|||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||||
|
|
||||||
sade@1.8.1:
|
sade@1.8.1:
|
||||||
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -4302,6 +4332,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
|
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
|
||||||
engines: {node: '>=11.0.0'}
|
engines: {node: '>=11.0.0'}
|
||||||
|
|
||||||
|
sdp-transform@2.15.0:
|
||||||
|
resolution: {integrity: sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
sdp@3.2.1:
|
||||||
|
resolution: {integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==}
|
||||||
|
|
||||||
semver@5.7.2:
|
semver@5.7.2:
|
||||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -4703,6 +4740,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
typed-emitter@2.1.0:
|
||||||
|
resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==}
|
||||||
|
|
||||||
typescript-eslint@8.53.1:
|
typescript-eslint@8.53.1:
|
||||||
resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==}
|
resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -4847,6 +4887,10 @@ packages:
|
|||||||
webidl-conversions@4.0.2:
|
webidl-conversions@4.0.2:
|
||||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||||
|
|
||||||
|
webrtc-adapter@9.0.4:
|
||||||
|
resolution: {integrity: sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw==}
|
||||||
|
engines: {node: '>=6.0.0', npm: '>=3.10.0'}
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
@@ -5733,6 +5777,8 @@ snapshots:
|
|||||||
|
|
||||||
'@braintree/sanitize-url@7.1.1': {}
|
'@braintree/sanitize-url@7.1.1': {}
|
||||||
|
|
||||||
|
'@bufbuild/protobuf@1.10.1': {}
|
||||||
|
|
||||||
'@canvas/image-data@1.1.0': {}
|
'@canvas/image-data@1.1.0': {}
|
||||||
|
|
||||||
'@capacitor-community/safe-area@8.0.1(@capacitor/core@8.0.1)':
|
'@capacitor-community/safe-area@8.0.1(@capacitor/core@8.0.1)':
|
||||||
@@ -6298,6 +6344,12 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@livekit/mutex@1.1.1': {}
|
||||||
|
|
||||||
|
'@livekit/protocol@1.44.0':
|
||||||
|
dependencies:
|
||||||
|
'@bufbuild/protobuf': 1.10.1
|
||||||
|
|
||||||
'@noble/ciphers@0.5.3': {}
|
'@noble/ciphers@0.5.3': {}
|
||||||
|
|
||||||
'@noble/ciphers@1.3.0': {}
|
'@noble/ciphers@1.3.0': {}
|
||||||
@@ -6859,6 +6911,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/cookie@0.6.0': {}
|
'@types/cookie@0.6.0': {}
|
||||||
|
|
||||||
|
'@types/dom-mediacapture-record@1.0.22': {}
|
||||||
|
|
||||||
'@types/eslint@9.6.1':
|
'@types/eslint@9.6.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@@ -8530,6 +8584,8 @@ snapshots:
|
|||||||
|
|
||||||
jiti@1.21.7: {}
|
jiti@1.21.7: {}
|
||||||
|
|
||||||
|
jose@6.2.1: {}
|
||||||
|
|
||||||
js-base64@3.7.8: {}
|
js-base64@3.7.8: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
@@ -8605,6 +8661,19 @@ snapshots:
|
|||||||
|
|
||||||
linkifyjs@4.3.2: {}
|
linkifyjs@4.3.2: {}
|
||||||
|
|
||||||
|
livekit-client@2.17.3(@types/dom-mediacapture-record@1.0.22):
|
||||||
|
dependencies:
|
||||||
|
'@livekit/mutex': 1.1.1
|
||||||
|
'@livekit/protocol': 1.44.0
|
||||||
|
'@types/dom-mediacapture-record': 1.0.22
|
||||||
|
events: 3.3.0
|
||||||
|
jose: 6.2.1
|
||||||
|
loglevel: 1.9.2
|
||||||
|
sdp-transform: 2.15.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
typed-emitter: 2.1.0
|
||||||
|
webrtc-adapter: 9.0.4
|
||||||
|
|
||||||
load-json-file@4.0.0:
|
load-json-file@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -8637,6 +8706,8 @@ snapshots:
|
|||||||
|
|
||||||
lodash@4.17.23: {}
|
lodash@4.17.23: {}
|
||||||
|
|
||||||
|
loglevel@1.9.2: {}
|
||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
lru-cache@11.2.4: {}
|
lru-cache@11.2.4: {}
|
||||||
@@ -9427,6 +9498,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
sade@1.8.1:
|
sade@1.8.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
mri: 1.2.0
|
mri: 1.2.0
|
||||||
@@ -9458,6 +9534,10 @@ snapshots:
|
|||||||
|
|
||||||
sax@1.4.4: {}
|
sax@1.4.4: {}
|
||||||
|
|
||||||
|
sdp-transform@2.15.0: {}
|
||||||
|
|
||||||
|
sdp@3.2.1: {}
|
||||||
|
|
||||||
semver@5.7.2: {}
|
semver@5.7.2: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
@@ -9982,6 +10062,10 @@ snapshots:
|
|||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
reflect.getprototypeof: 1.0.10
|
reflect.getprototypeof: 1.0.10
|
||||||
|
|
||||||
|
typed-emitter@2.1.0:
|
||||||
|
optionalDependencies:
|
||||||
|
rxjs: 7.8.2
|
||||||
|
|
||||||
typescript-eslint@8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
|
typescript-eslint@8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||||
@@ -10090,6 +10174,10 @@ snapshots:
|
|||||||
|
|
||||||
webidl-conversions@4.0.2: {}
|
webidl-conversions@4.0.2: {}
|
||||||
|
|
||||||
|
webrtc-adapter@9.0.4:
|
||||||
|
dependencies:
|
||||||
|
sdp: 3.2.1
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
|
|||||||
@@ -422,6 +422,14 @@ body.keyboard-open .hide-on-keyboard {
|
|||||||
@apply cb cw fixed z-compose;
|
@apply cb cw fixed z-compose;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat__compose-zone {
|
||||||
|
@apply cb cw fixed z-compose;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat__compose-zone .chat__compose-inner {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat__scroll-down {
|
.chat__scroll-down {
|
||||||
@apply pb-sai fixed bottom-28 right-4 z-feature md:bottom-16;
|
@apply pb-sai fixed bottom-28 right-4 z-feature md:bottom-16;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pubkey: string
|
pubkey?: string
|
||||||
class?: string
|
class?: string
|
||||||
size?: number
|
size?: number
|
||||||
url?: string
|
url?: string
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import Lock from "@assets/icons/lock.svg?dataurl"
|
import Lock from "@assets/icons/lock.svg?dataurl"
|
||||||
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||||
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
||||||
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
<strong class="text-lg">Room Settings</strong>
|
<strong class="text-lg">Room Settings</strong>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon={VolumeLoud} />
|
<Icon icon={Bell} />
|
||||||
<span>Notifications</span>
|
<span>Notifications</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
|
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
|
||||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||||
|
import Volume from "@assets/icons/volume.svg?dataurl"
|
||||||
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {uploadFile} from "@app/core/commands"
|
import {uploadFile} from "@app/core/commands"
|
||||||
|
import {deriveHasLivekit, getRoomType, RoomType} from "@app/core/state"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -27,12 +29,25 @@
|
|||||||
const {url, header, footer, onsubmit, initialValues = makeRoomMeta()}: Props = $props()
|
const {url, header, footer, onsubmit, initialValues = makeRoomMeta()}: Props = $props()
|
||||||
|
|
||||||
const values = $state(initialValues)
|
const values = $state(initialValues)
|
||||||
|
const relayHasLivekit = deriveHasLivekit(url)
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
const room = $state.snapshot(values)
|
const room = $state.snapshot(values)
|
||||||
|
|
||||||
|
if (roomType === RoomType.Voice && !$relayHasLivekit) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "This relay does not support voice rooms.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
room.livekit = roomType === RoomType.Voice
|
||||||
|
|
||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
const {error, result} = await uploadFile(imageFile, {maxWidth: 256, maxHeight: 256})
|
const {error, result} = await uploadFile(imageFile, {
|
||||||
|
maxWidth: 256,
|
||||||
|
maxHeight: 256,
|
||||||
|
})
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return pushToast({theme: "error", message: error})
|
return pushToast({theme: "error", message: error})
|
||||||
@@ -76,6 +91,7 @@
|
|||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
let imageFile = $state<File | undefined>()
|
let imageFile = $state<File | undefined>()
|
||||||
let imagePreview = $state(initialValues.picture)
|
let imagePreview = $state(initialValues.picture)
|
||||||
|
let roomType = $state(getRoomType(initialValues))
|
||||||
|
|
||||||
const handleImageUpload = async (event: Event) => {
|
const handleImageUpload = async (event: Event) => {
|
||||||
const file = (event.target as HTMLInputElement).files?.[0]
|
const file = (event.target as HTMLInputElement).files?.[0]
|
||||||
@@ -145,7 +161,7 @@
|
|||||||
{#if imagePreview}
|
{#if imagePreview}
|
||||||
<ImageIcon src={imagePreview} alt="" class="rounded-lg" />
|
<ImageIcon src={imagePreview} alt="" class="rounded-lg" />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon={Hashtag} />
|
<Icon icon={roomType === RoomType.Voice ? Volume : Hashtag} />
|
||||||
{/if}
|
{/if}
|
||||||
<input bind:value={values.name} class="grow" type="text" />
|
<input bind:value={values.name} class="grow" type="text" />
|
||||||
</label>
|
</label>
|
||||||
@@ -161,6 +177,22 @@
|
|||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
{#if $relayHasLivekit}
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Room type</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<select
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
bind:value={roomType}
|
||||||
|
aria-label="Room type">
|
||||||
|
<option value={RoomType.Text}>Text</option>
|
||||||
|
<option value={RoomType.Voice}>Voice</option>
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
{/if}
|
||||||
<strong class="md:hidden">Permissions</strong>
|
<strong class="md:hidden">Permissions</strong>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
|
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
|
||||||
|
|||||||
@@ -1,22 +1,41 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||||
|
import Volume from "@assets/icons/volume.svg?dataurl"
|
||||||
|
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
import {deriveRoom} from "@app/core/state"
|
import {deriveRoom} from "@app/core/state"
|
||||||
|
import {currentVoiceSession} from "@app/voice"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
h: string
|
h: string
|
||||||
url: string
|
url: string
|
||||||
size?: number
|
size?: number
|
||||||
|
fallbackIcon?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, h, size = 5}: Props = $props()
|
const {url, h, size = 5, fallbackIcon = Hashtag}: Props = $props()
|
||||||
|
|
||||||
const room = deriveRoom(url, h)
|
const room = deriveRoom(url, h)
|
||||||
|
const isVoiceRoom = $derived($room.livekit)
|
||||||
|
const isVoiceRoomActive = $derived(
|
||||||
|
$currentVoiceSession?.url === url && $currentVoiceSession?.h === h,
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $room.picture}
|
{#if isVoiceRoom}
|
||||||
|
<div class="flex shrink-0 items-center gap-1.5">
|
||||||
|
<Icon
|
||||||
|
size={size + 1}
|
||||||
|
icon={isVoiceRoomActive ? VolumeLoud : Volume}
|
||||||
|
class={isVoiceRoomActive ? "text-primary -translate-x-0.5" : ""} />
|
||||||
|
{#if $room.picture}
|
||||||
|
<span class="text-base">/</span>
|
||||||
|
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if $room.picture}
|
||||||
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon={Hashtag} {size} />
|
<Icon icon={fallbackIcon} {size} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
const room = deriveRoom(url, h)
|
const room = deriveRoom(url, h)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="ellipsize {props.class}">
|
<span class="ellipsize min-w-0 {props.class}">
|
||||||
{$room?.name || h}
|
{$room?.name || h}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
import CaseMinimalistic from "@assets/icons/case-minimalistic.svg?dataurl"
|
import CaseMinimalistic from "@assets/icons/case-minimalistic.svg?dataurl"
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||||
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
import VolumeCross from "@assets/icons/volume-cross.svg?dataurl"
|
import BellOff from "@assets/icons/bell-off.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
import SpaceReports from "@app/components/SpaceReports.svelte"
|
import SpaceReports from "@app/components/SpaceReports.svelte"
|
||||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||||
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
|
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
|
||||||
|
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||||
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
|
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
|
||||||
import {
|
import {
|
||||||
ENABLE_ZAPS,
|
ENABLE_ZAPS,
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
deriveSpaceMembers,
|
deriveSpaceMembers,
|
||||||
deriveUserRooms,
|
deriveUserRooms,
|
||||||
deriveOtherRooms,
|
deriveOtherRooms,
|
||||||
|
deriveOtherVoiceRooms,
|
||||||
userSpaceUrls,
|
userSpaceUrls,
|
||||||
hasNip29,
|
hasNip29,
|
||||||
deriveUserCanCreateRoom,
|
deriveUserCanCreateRoom,
|
||||||
@@ -68,6 +70,7 @@
|
|||||||
const calendarPath = makeSpacePath(url, "calendar")
|
const calendarPath = makeSpacePath(url, "calendar")
|
||||||
const userRooms = deriveUserRooms(url)
|
const userRooms = deriveUserRooms(url)
|
||||||
const otherRooms = deriveOtherRooms(url)
|
const otherRooms = deriveOtherRooms(url)
|
||||||
|
const otherVoiceRooms = deriveOtherVoiceRooms(url)
|
||||||
const members = deriveSpaceMembers(url)
|
const members = deriveSpaceMembers(url)
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
const reports = deriveEventsForUrl(url, [{kinds: [REPORT]}])
|
const reports = deriveEventsForUrl(url, [{kinds: [REPORT]}])
|
||||||
@@ -133,9 +136,9 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={element} class="flex h-full flex-col justify-between">
|
<div bind:this={element} class="flex min-h-0 flex-1 flex-col">
|
||||||
<SecondaryNavSection class="pb-0">
|
<SecondaryNavSection class="min-h-0 flex-1 flex flex-col overflow-hidden pb-0">
|
||||||
<div>
|
<div class="flex-shrink-0">
|
||||||
<Button
|
<Button
|
||||||
class="flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
class="flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
||||||
onclick={openMenu}>
|
onclick={openMenu}>
|
||||||
@@ -143,7 +146,7 @@
|
|||||||
<strong class="ellipsize flex items-center gap-1">
|
<strong class="ellipsize flex items-center gap-1">
|
||||||
<RelayName {url} />
|
<RelayName {url} />
|
||||||
{#if $notificationSettings.push && !$shouldNotify}
|
{#if $notificationSettings.push && !$shouldNotify}
|
||||||
<Icon icon={VolumeCross} size={3} class="opacity-50" />
|
<Icon icon={BellOff} size={3} class="opacity-50" />
|
||||||
{/if}
|
{/if}
|
||||||
</strong>
|
</strong>
|
||||||
<Icon icon={AltArrowDown} />
|
<Icon icon={AltArrowDown} />
|
||||||
@@ -192,12 +195,12 @@
|
|||||||
<li>
|
<li>
|
||||||
{#if $notificationSettings.push}
|
{#if $notificationSettings.push}
|
||||||
<Button onclick={toggleSpaceNotifications}>
|
<Button onclick={toggleSpaceNotifications}>
|
||||||
<Icon icon={$shouldNotify ? VolumeLoud : VolumeCross} />
|
<Icon icon={$shouldNotify ? Bell : BellOff} />
|
||||||
{$shouldNotify ? "Turn off" : "Turn on"} notifications
|
{$shouldNotify ? "Turn off" : "Turn on"} notifications
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Link href="/settings/alerts">
|
<Link href="/settings/alerts">
|
||||||
<Icon icon={VolumeLoud} />
|
<Icon icon={Bell} />
|
||||||
Enable notifications
|
Enable notifications
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -219,8 +222,7 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex min-h-0 flex-1 flex-col gap-1 overflow-auto overflow-x-hidden">
|
||||||
class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto overflow-x-hidden">
|
|
||||||
{#if hasNip29($relay)}
|
{#if hasNip29($relay)}
|
||||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
|
<SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
|
||||||
<Icon icon={History} /> Recent Activity
|
<Icon icon={History} /> Recent Activity
|
||||||
@@ -252,14 +254,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if hasNip29($relay)}
|
{#if hasNip29($relay)}
|
||||||
{#if $userRooms.length > 0}
|
{#if $userRooms.length > 0}
|
||||||
<div class="h-2"></div>
|
<div class="h-2 flex-shrink-0"></div>
|
||||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $userRooms as h, i (h)}
|
{#each $userRooms as h (h)}
|
||||||
<SpaceMenuRoomItem notify {replaceState} {url} {h} />
|
<SpaceMenuRoomItem notify {replaceState} {url} {h} />
|
||||||
{/each}
|
{/each}
|
||||||
{#if $otherRooms.length > 0}
|
{#if $otherRooms.length > 0}
|
||||||
<div class="h-2"></div>
|
<div class="h-2 flex-shrink-0"></div>
|
||||||
<SecondaryNavHeader>
|
<SecondaryNavHeader>
|
||||||
{#if $userRooms.length > 0}
|
{#if $userRooms.length > 0}
|
||||||
Other Rooms
|
Other Rooms
|
||||||
@@ -274,9 +276,16 @@
|
|||||||
<input bind:value={term} onblur={clearTerm} class="grow" />
|
<input bind:value={term} onblur={clearTerm} class="grow" />
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $roomSearch.searchValues(term) as h, i (h)}
|
{#each $roomSearch.searchValues(term) as h (h)}
|
||||||
<SpaceMenuRoomItem {replaceState} {url} {h} />
|
<SpaceMenuRoomItem {replaceState} {url} {h} />
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if $otherVoiceRooms.length > 0}
|
||||||
|
<div class="h-2 flex-shrink-0"></div>
|
||||||
|
<SecondaryNavHeader>Voice Rooms</SecondaryNavHeader>
|
||||||
|
{#each $otherVoiceRooms as h (h)}
|
||||||
|
<SpaceMenuRoomItem {replaceState} {url} {h} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
{#if $canCreateRoom}
|
{#if $canCreateRoom}
|
||||||
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
||||||
<Icon icon={AddCircle} />
|
<Icon icon={AddCircle} />
|
||||||
@@ -284,9 +293,12 @@
|
|||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="h-5 flex-shrink-0"></div>
|
||||||
</div>
|
</div>
|
||||||
</SecondaryNavSection>
|
</SecondaryNavSection>
|
||||||
<div class="flex flex-col gap-2 pb-2 p-4 pt-0">
|
<div
|
||||||
|
class="flex flex-shrink-0 flex-col gap-2 p-2 pt-0 -mt-4 pb-[calc(var(--saib)+3rem)] sm:pb-2 z-nav">
|
||||||
|
<VoiceWidget />
|
||||||
<Button class="btn btn-neutral btn-sm h-10" onclick={showDetail}>
|
<Button class="btn btn-neutral btn-sm h-10" onclick={showDetail}>
|
||||||
<SocketStatusIndicator {url} />
|
<SocketStatusIndicator {url} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import VolumeCross from "@assets/icons/volume-cross.svg?dataurl"
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
|
import BellOff from "@assets/icons/bell-off.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||||
|
import VoiceRoomItem from "@app/components/VoiceRoomItem.svelte"
|
||||||
|
import {deriveRoom, deriveShouldNotify, getRoomType, RoomType} from "@app/core/state"
|
||||||
import {notifications} from "@app/util/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
import {makeRoomPath} from "@app/util/routes"
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
import {deriveShouldNotify} from "@app/core/state"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: any
|
url: any
|
||||||
@@ -17,18 +18,24 @@
|
|||||||
|
|
||||||
const {url, h, notify = false, replaceState = false}: Props = $props()
|
const {url, h, notify = false, replaceState = false}: Props = $props()
|
||||||
|
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
const roomType = $derived(getRoomType($room))
|
||||||
const path = makeRoomPath(url, h)
|
const path = makeRoomPath(url, h)
|
||||||
const shouldNotifyForSpace = deriveShouldNotify(url)
|
const shouldNotifyForSpace = deriveShouldNotify(url)
|
||||||
const shouldNotifyForRoom = deriveShouldNotify(url, h)
|
const shouldNotifyForRoom = deriveShouldNotify(url, h)
|
||||||
const showDifferenceIcon = $derived($shouldNotifyForRoom !== $shouldNotifyForSpace)
|
const showDifferenceIcon = $derived($shouldNotifyForRoom !== $shouldNotifyForSpace)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SecondaryNavItem
|
{#if roomType === RoomType.Voice}
|
||||||
href={path}
|
<VoiceRoomItem {url} {h} {replaceState} />
|
||||||
{replaceState}
|
{:else}
|
||||||
notification={notify ? $notifications.has(path) : false}>
|
<SecondaryNavItem
|
||||||
<RoomNameWithImage {url} {h} />
|
href={path}
|
||||||
{#if showDifferenceIcon}
|
{replaceState}
|
||||||
<Icon icon={$shouldNotifyForRoom ? VolumeLoud : VolumeCross} size={4} class="opacity-50" />
|
notification={notify ? $notifications.has(path) : false}>
|
||||||
{/if}
|
<RoomNameWithImage {url} {h} />
|
||||||
</SecondaryNavItem>
|
{#if showDifferenceIcon}
|
||||||
|
<Icon icon={$shouldNotifyForRoom ? Bell : BellOff} size={4} class="opacity-50" />
|
||||||
|
{/if}
|
||||||
|
</SecondaryNavItem>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
|
import {loadProfile, displayProfileByPubkey} from "@welshman/app"
|
||||||
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
|
import RoomImage from "@app/components/RoomImage.svelte"
|
||||||
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
|
import {
|
||||||
|
deriveVoiceParticipants,
|
||||||
|
joinVoiceRoom,
|
||||||
|
cancelJoinVoiceRoom,
|
||||||
|
currentVoiceRoom,
|
||||||
|
voiceState,
|
||||||
|
isParticipantSpeaking,
|
||||||
|
participantKey,
|
||||||
|
type VoiceParticipant,
|
||||||
|
} from "@app/voice"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
replaceState?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h, replaceState = false}: Props = $props()
|
||||||
|
|
||||||
|
const participants = deriveVoiceParticipants(url, h)
|
||||||
|
const isActive = $derived(
|
||||||
|
$voiceState === "connected" && $currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h,
|
||||||
|
)
|
||||||
|
const isJoining = $derived(
|
||||||
|
$voiceState === "joining" && $currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h,
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
if (isActive) return
|
||||||
|
|
||||||
|
if (isJoining) {
|
||||||
|
cancelJoinVoiceRoom()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await joinVoiceRoom(url, h)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to join voice room", e)
|
||||||
|
pushToast({theme: "error", message: "Failed to join voice room"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
for (const p of $participants) {
|
||||||
|
if (p.pubkey) loadProfile(p.pubkey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SecondaryNavItem
|
||||||
|
href={makeRoomPath(url, h)}
|
||||||
|
{replaceState}
|
||||||
|
onclick={handleClick}
|
||||||
|
class={cx("!items-start", isActive && "!bg-base-100 !text-base-content")}>
|
||||||
|
<div class="flex w-full min-w-0 flex-col gap-2">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
{#if isJoining}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{:else}
|
||||||
|
<RoomImage {url} {h} size={4} />
|
||||||
|
{/if}
|
||||||
|
<RoomName {url} {h} />
|
||||||
|
</div>
|
||||||
|
{#if $participants.length > 0}
|
||||||
|
{#each $participants as p (participantKey(p as VoiceParticipant))}
|
||||||
|
<div class="flex items-center gap-2 ml-6">
|
||||||
|
<div
|
||||||
|
class={cx(
|
||||||
|
"inline-flex shrink-0 items-center justify-center rounded-full transition-shadow",
|
||||||
|
isActive && $isParticipantSpeaking(p) && "ring-2 ring-success",
|
||||||
|
)}>
|
||||||
|
<ProfileCircle pubkey={p.pubkey} size={5} class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<span class="ellipsize text-xs opacity-70">
|
||||||
|
{p.pubkey ? displayProfileByPubkey(p.pubkey) : "Unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</SecondaryNavItem>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {fly} from "svelte/transition"
|
||||||
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||||
|
import MicrophoneOff from "@assets/icons/microphone-off.svg?dataurl"
|
||||||
|
import PhoneRounded from "@assets/icons/phone-rounded.svg?dataurl"
|
||||||
|
import PhoneCallingRounded from "@assets/icons/phone-calling-rounded.svg?dataurl"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import {displayRoom} from "@app/core/state"
|
||||||
|
import {
|
||||||
|
currentVoiceSession,
|
||||||
|
currentVoiceRoom,
|
||||||
|
voiceState,
|
||||||
|
leaveVoiceRoom,
|
||||||
|
toggleMute,
|
||||||
|
rejoinVoiceRoom,
|
||||||
|
cancelJoinVoiceRoom,
|
||||||
|
} from "@app/voice"
|
||||||
|
|
||||||
|
const roomName = $derived(
|
||||||
|
$currentVoiceRoom ? displayRoom($currentVoiceRoom.url, $currentVoiceRoom.h) : "",
|
||||||
|
)
|
||||||
|
const spaceName = $derived($currentVoiceRoom ? displayRelayUrl($currentVoiceRoom.url) : "")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $currentVoiceRoom}
|
||||||
|
<div
|
||||||
|
in:fly={{y: 60, duration: 350}}
|
||||||
|
out:fly={{y: 60, duration: 250}}
|
||||||
|
class="flex flex-col gap-2 rounded-box bg-base-100 p-3">
|
||||||
|
<div class="flex flex-col gap-0.5">
|
||||||
|
{#if $voiceState === "joining"}
|
||||||
|
<span class="text-sm font-semibold text-warning">Joining...</span>
|
||||||
|
{:else if $voiceState === "connected"}
|
||||||
|
<span class="text-sm font-semibold text-success">Voice Connected</span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-sm font-semibold text-neutral-content">Disconnected</span>
|
||||||
|
{/if}
|
||||||
|
<span class="ellipsize text-xs opacity-70">
|
||||||
|
{roomName} / {spaceName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
{#if $voiceState === "joining"}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
<Button
|
||||||
|
data-tip="Cancel"
|
||||||
|
class="center tooltip tooltip-top btn btn-sm btn-square btn-ghost"
|
||||||
|
onclick={cancelJoinVoiceRoom}>
|
||||||
|
<Icon icon={CloseCircle} size={4} />
|
||||||
|
</Button>
|
||||||
|
{:else if $voiceState === "connected" && $currentVoiceSession}
|
||||||
|
<Button
|
||||||
|
data-tip={$currentVoiceSession.muted ? "Unmute" : "Mute"}
|
||||||
|
class="center tooltip tooltip-top btn btn-sm btn-square {$currentVoiceSession.muted
|
||||||
|
? 'btn-error'
|
||||||
|
: 'btn-ghost'}"
|
||||||
|
onclick={toggleMute}>
|
||||||
|
<Icon icon={$currentVoiceSession.muted ? MicrophoneOff : Microphone} size={4} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-tip="Leave room"
|
||||||
|
class="center tooltip tooltip-top btn btn-sm btn-square btn-error"
|
||||||
|
onclick={leaveVoiceRoom}>
|
||||||
|
<Icon icon={PhoneRounded} size={4} />
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button
|
||||||
|
data-tip="Join Voice"
|
||||||
|
class="center tooltip tooltip-top btn btn-sm btn-square btn-success"
|
||||||
|
onclick={rejoinVoiceRoom}>
|
||||||
|
<Icon icon={PhoneCallingRounded} size={4} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
+51
-8
@@ -125,6 +125,7 @@ import type {
|
|||||||
RelayProfile,
|
RelayProfile,
|
||||||
PublishedList,
|
PublishedList,
|
||||||
PublishedRoomMeta,
|
PublishedRoomMeta,
|
||||||
|
RoomMeta,
|
||||||
List,
|
List,
|
||||||
Filter,
|
Filter,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
@@ -146,6 +147,7 @@ import {
|
|||||||
displayProfileByPubkey,
|
displayProfileByPubkey,
|
||||||
getProfile,
|
getProfile,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
|
import {checkRelayHasLivekit} from "$lib/livekit"
|
||||||
import {readFeed} from "@lib/feeds"
|
import {readFeed} from "@lib/feeds"
|
||||||
|
|
||||||
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
||||||
@@ -567,11 +569,19 @@ export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => {
|
|||||||
|
|
||||||
// Rooms
|
// Rooms
|
||||||
|
|
||||||
|
export enum RoomType {
|
||||||
|
Text = "text",
|
||||||
|
Voice = "voice",
|
||||||
|
}
|
||||||
|
|
||||||
export type Room = PublishedRoomMeta & {
|
export type Room = PublishedRoomMeta & {
|
||||||
id: string
|
id: string
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getRoomType = (room: RoomMeta): RoomType =>
|
||||||
|
room.livekit ? RoomType.Voice : RoomType.Text
|
||||||
|
|
||||||
export const makeRoomId = (url: string, h: string) => `${url}'${h}`
|
export const makeRoomId = (url: string, h: string) => `${url}'${h}`
|
||||||
|
|
||||||
export const splitRoomId = (id: string) => id.split("'")
|
export const splitRoomId = (id: string) => id.split("'")
|
||||||
@@ -663,6 +673,30 @@ export const displayRoom = (url: string, h: string) => getRoom(makeRoomId(url, h
|
|||||||
|
|
||||||
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
|
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
|
||||||
|
|
||||||
|
export const deriveVoiceRooms = (url: string) =>
|
||||||
|
derived(roomsById, $roomsById => {
|
||||||
|
const set = new Set<string>()
|
||||||
|
for (const room of $roomsById.values()) {
|
||||||
|
if (room.url === url && room.livekit) {
|
||||||
|
set.add(room.h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deriveOtherVoiceRooms = (url: string) =>
|
||||||
|
derived([deriveVoiceRooms(url), deriveUserRooms(url)], ([$roomsWithLivekit, $userRooms]) => {
|
||||||
|
const rooms: string[] = []
|
||||||
|
|
||||||
|
for (const h of $roomsWithLivekit) {
|
||||||
|
if (!$userRooms.includes(h)) {
|
||||||
|
rooms.push(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortBy(roomComparator(url), uniq(rooms))
|
||||||
|
})
|
||||||
|
|
||||||
// User space/room lists
|
// User space/room lists
|
||||||
|
|
||||||
export const groupListsByPubkey = deriveItemsByKey({
|
export const groupListsByPubkey = deriveItemsByKey({
|
||||||
@@ -752,17 +786,20 @@ export const deriveUserRooms = (url: string) =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const deriveOtherRooms = (url: string) =>
|
export const deriveOtherRooms = (url: string) =>
|
||||||
derived([deriveUserRooms(url), roomsByUrl], ([$userRooms, $roomsByUrl]) => {
|
derived(
|
||||||
const rooms: string[] = []
|
[deriveUserRooms(url), deriveVoiceRooms(url), roomsByUrl],
|
||||||
|
([$userRooms, voiceRooms, $roomsByUrl]) => {
|
||||||
|
const rooms: string[] = []
|
||||||
|
|
||||||
for (const {h} of $roomsByUrl.get(url) || []) {
|
for (const {h} of $roomsByUrl.get(url) || []) {
|
||||||
if (!$userRooms.includes(h)) {
|
if (!$userRooms.includes(h) && !voiceRooms.has(h)) {
|
||||||
rooms.push(h)
|
rooms.push(h)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return sortBy(roomComparator(url), uniq(rooms))
|
return sortBy(roomComparator(url), uniq(rooms))
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// Space/room memberships
|
// Space/room memberships
|
||||||
|
|
||||||
@@ -1165,6 +1202,12 @@ export const deriveSupportedMethods = simpleCache(([url]: [string]) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const deriveHasLivekit = simpleCache(([url]: [string]) =>
|
||||||
|
readable<boolean | undefined>(undefined, set => {
|
||||||
|
checkRelayHasLivekit(url).then(has => set(has))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const deriveTimeout = (timeout: number) => {
|
export const deriveTimeout = (timeout: number) => {
|
||||||
const store = writable<boolean>(false)
|
const store = writable<boolean>(false)
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import {
|
|||||||
loadFeedsForPubkey,
|
loadFeedsForPubkey,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {hasBlossomSupport} from "@app/core/commands"
|
import {hasBlossomSupport} from "@app/core/commands"
|
||||||
|
import {LIVEKIT_PARTICIPANTS} from "@app/voice"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
@@ -316,6 +317,12 @@ const syncSpace = (url: string, rooms: string[]) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pullAndListen({
|
||||||
|
url,
|
||||||
|
signal: controller.signal,
|
||||||
|
filters: [{kinds: [LIVEKIT_PARTICIPANTS]}],
|
||||||
|
})
|
||||||
|
|
||||||
return () => controller.abort()
|
return () => controller.abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
/**
|
||||||
|
* Voice rooms via LiveKit. Note: Voice does not work on localhost in Firefox
|
||||||
|
* (ICE candidate gathering fails). Use Chrome or test from deployed HTTPS.
|
||||||
|
*/
|
||||||
|
import {DisconnectReason, Room, RoomEvent, Track} from "livekit-client"
|
||||||
|
import {derived, get, writable} from "svelte/store"
|
||||||
|
import {map, removeUndefined, uniqBy} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||||
|
import {signer} from "@welshman/app"
|
||||||
|
import {getLivekitEndpoint} from "$lib/livekit"
|
||||||
|
import {AbortError, whenAborted, whenTimeout} from "$lib/util"
|
||||||
|
import {deriveLatestEventForUrl} from "@app/core/state"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
export const LIVEKIT_PARTICIPANTS = 39004
|
||||||
|
|
||||||
|
export {checkRelayHasLivekit} from "$lib/livekit"
|
||||||
|
|
||||||
|
export type VoiceSession = {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
room: Room
|
||||||
|
muted: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Pubkey = string
|
||||||
|
|
||||||
|
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
||||||
|
|
||||||
|
export type VoiceState = "joining" | "connected" | "disconnected"
|
||||||
|
|
||||||
|
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
||||||
|
|
||||||
|
export const voiceState = writable<VoiceState>("disconnected")
|
||||||
|
|
||||||
|
export const currentVoiceRoom = writable<{url: string; h: string} | undefined>(undefined)
|
||||||
|
|
||||||
|
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
|
||||||
|
|
||||||
|
const addParticipant = (identity: string) => {
|
||||||
|
participantPubkeyMap.update(m => {
|
||||||
|
const next = new Map(m)
|
||||||
|
next.set(identity, pubkeyFromLiveKitIdentity(identity) ?? "")
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteParticipant = (identity: string) => {
|
||||||
|
participantPubkeyMap.update(m => {
|
||||||
|
const next = new Map(m)
|
||||||
|
next.delete(identity)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
|
||||||
|
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
|
||||||
|
|
||||||
|
export const participantFromLiveKitIdentity = (identity: string): VoiceParticipant => {
|
||||||
|
const pk = pubkeyFromLiveKitIdentity(identity)
|
||||||
|
return pk ? {pubkey: pk, identity} : {identity}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const participantKey = (p: VoiceParticipant) => p.pubkey ?? p.identity
|
||||||
|
|
||||||
|
export const speakingParticipants = writable<VoiceParticipant[]>([])
|
||||||
|
|
||||||
|
export const isParticipantSpeaking = derived(
|
||||||
|
speakingParticipants,
|
||||||
|
$participants => (p: VoiceParticipant) =>
|
||||||
|
$participants.some(sp => participantKey(sp) === participantKey(p)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const fetchLivekitToken = async (
|
||||||
|
url: string,
|
||||||
|
groupId: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<{server_url: string; participant_token: string}> => {
|
||||||
|
const endpoint = getLivekitEndpoint(url, groupId)
|
||||||
|
|
||||||
|
const $signer = signer.get()
|
||||||
|
if (!$signer) throw new Error("No signer available")
|
||||||
|
|
||||||
|
if (signal?.aborted) throw new DOMException("Aborted", "AbortError")
|
||||||
|
|
||||||
|
const template = await makeHttpAuth(endpoint, "GET")
|
||||||
|
const signedEvent = await $signer.sign(template)
|
||||||
|
const authHeader = makeHttpAuthHeader(signedEvent)
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
headers: {Authorization: authHeader},
|
||||||
|
signal,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text()
|
||||||
|
throw new Error(`Token request failed (${response.status}): ${text}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveVoiceParticipants = (url: string, h: string) =>
|
||||||
|
// We use the livekit identity list while in a call, and fall back to the list in kind 39004.
|
||||||
|
derived(
|
||||||
|
[
|
||||||
|
participantPubkeyMap,
|
||||||
|
currentVoiceRoom,
|
||||||
|
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
|
||||||
|
],
|
||||||
|
([$participantPubkeyMap, $currentVoiceRoom, $publishedParticipantList]) => {
|
||||||
|
const inCall =
|
||||||
|
$participantPubkeyMap.size > 0 &&
|
||||||
|
$currentVoiceRoom?.url === url &&
|
||||||
|
$currentVoiceRoom?.h === h
|
||||||
|
|
||||||
|
if (inCall) {
|
||||||
|
const participants = [...$participantPubkeyMap.keys()].map(participantFromLiveKitIdentity)
|
||||||
|
return uniqBy((p: VoiceParticipant) => participantKey(p), participants)
|
||||||
|
} else {
|
||||||
|
const latestEvent = $publishedParticipantList as TrustedEvent | undefined
|
||||||
|
if (!latestEvent) return []
|
||||||
|
const participants = removeUndefined(
|
||||||
|
map(
|
||||||
|
(tag: string[]) => (tag[1] ? participantFromLiveKitIdentity(tag[1]) : undefined),
|
||||||
|
getTags("participant", latestEvent.tags),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return uniqBy((p: VoiceParticipant) => participantKey(p), participants)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
||||||
|
speakingParticipants.set([])
|
||||||
|
participantPubkeyMap.set(new Map())
|
||||||
|
currentVoiceSession.set(undefined)
|
||||||
|
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
||||||
|
voiceState.set("disconnected")
|
||||||
|
const message =
|
||||||
|
reason === DisconnectReason.JOIN_FAILURE
|
||||||
|
? "Could not connect to voice room. Please try again."
|
||||||
|
: "Voice connection lost."
|
||||||
|
pushToast({theme: "error", message})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTrackSubscribed = (track: Track) => {
|
||||||
|
if (track.kind === Track.Kind.Audio) {
|
||||||
|
const element = track.attach()
|
||||||
|
element.style.display = "none"
|
||||||
|
document.body.appendChild(element)
|
||||||
|
element.play().catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTrackUnsubscribed = (track: Track) => {
|
||||||
|
track.detach().forEach(el => el.remove())
|
||||||
|
}
|
||||||
|
|
||||||
|
const onActiveSpeakersChanged = (participants: {identity: string}[]) => {
|
||||||
|
speakingParticipants.set(participants.map(p => participantFromLiveKitIdentity(p.identity)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const playJoinSound = () => {
|
||||||
|
const audio = new Audio("/join-voice-room.mp3")
|
||||||
|
audio.play().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onParticipantConnected = (participant: {identity: string}) => {
|
||||||
|
addParticipant(participant.identity)
|
||||||
|
playJoinSound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onParticipantDisconnected = (participant: {identity: string}) => {
|
||||||
|
deleteParticipant(participant.identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
let joinAbortController: AbortController | undefined
|
||||||
|
|
||||||
|
export const cancelJoinVoiceRoom = () => {
|
||||||
|
joinAbortController?.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const joinVoiceRoom = async (url: string, h: string): Promise<void> => {
|
||||||
|
cancelJoinVoiceRoom()
|
||||||
|
|
||||||
|
const session = get(currentVoiceSession)
|
||||||
|
if (session) await leaveVoiceRoom()
|
||||||
|
|
||||||
|
currentVoiceRoom.set({url, h})
|
||||||
|
voiceState.set("joining")
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
joinAbortController = controller
|
||||||
|
const signal = controller.signal
|
||||||
|
const isActive = () => joinAbortController === controller
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {server_url, participant_token} = await fetchLivekitToken(url, h, signal)
|
||||||
|
|
||||||
|
if (signal.aborted) throw new AbortError()
|
||||||
|
|
||||||
|
const room = new Room({adaptiveStream: true, dynacast: true})
|
||||||
|
|
||||||
|
room.on(RoomEvent.Disconnected, onRoomDisconnected)
|
||||||
|
room.on(RoomEvent.ParticipantConnected, onParticipantConnected)
|
||||||
|
room.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
|
||||||
|
room.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
||||||
|
room.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
||||||
|
room.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
room.connect(server_url, participant_token, {maxRetries: 0}),
|
||||||
|
whenTimeout(5_000, {
|
||||||
|
message: "Connection timed out. Please check your network and try again.",
|
||||||
|
}),
|
||||||
|
whenAborted(signal),
|
||||||
|
])
|
||||||
|
} catch (e) {
|
||||||
|
room.disconnect()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
participantPubkeyMap.set(new Map())
|
||||||
|
addParticipant(room.localParticipant.identity)
|
||||||
|
for (const p of room.remoteParticipants.values()) {
|
||||||
|
addParticipant(p.identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
let muted = false
|
||||||
|
try {
|
||||||
|
await room.localParticipant.setMicrophoneEnabled(true)
|
||||||
|
} catch (e) {
|
||||||
|
muted = true
|
||||||
|
pushToast({theme: "error", message: "Could not access microphone"})
|
||||||
|
}
|
||||||
|
|
||||||
|
currentVoiceSession.set({url, h, room, muted})
|
||||||
|
voiceState.set("connected")
|
||||||
|
playJoinSound()
|
||||||
|
} catch (e) {
|
||||||
|
if (isActive()) voiceState.set("disconnected")
|
||||||
|
if (e instanceof AbortError) return
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
if (isActive()) joinAbortController = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const leaveVoiceRoom = async () => {
|
||||||
|
const session = get(currentVoiceSession)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
const audio = new Audio("/leave-voice-room.mp3")
|
||||||
|
audio.play().catch(() => {})
|
||||||
|
|
||||||
|
speakingParticipants.set([])
|
||||||
|
participantPubkeyMap.set(new Map())
|
||||||
|
voiceState.set("disconnected")
|
||||||
|
session.room.disconnect()
|
||||||
|
currentVoiceSession.set(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rejoinVoiceRoom = () => {
|
||||||
|
const target = get(currentVoiceRoom)
|
||||||
|
if (target) joinVoiceRoom(target.url, target.h)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleMute = async () => {
|
||||||
|
const session = get(currentVoiceSession)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
const muted = !session.muted
|
||||||
|
if (muted) {
|
||||||
|
// Disable and re-enable microphone to trigger permission prompt
|
||||||
|
session.room.localParticipant.setMicrophoneEnabled(false)
|
||||||
|
currentVoiceSession.set({...session, muted})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await session.room.localParticipant.setMicrophoneEnabled(true)
|
||||||
|
currentVoiceSession.set({...session, muted})
|
||||||
|
} catch (e) {
|
||||||
|
pushToast({theme: "error", message: "Could not access microphone"})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7 8C7 5.23858 9.23858 3 12 3C14.7614 3 17 5.23858 17 8V11C17 13.7614 14.7614 16 12 16C9.23858 16 7 13.7614 7 11V8Z" stroke="#000000" stroke-width="1.5"/>
|
||||||
|
<path d="M13 8L17 8" stroke="#000000" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M13 11L17 11" stroke="#000000" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M20 10V11C20 15.4183 16.4183 19 12 19C7.58172 19 4 15.4183 4 11V10" stroke="#000000" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M12 19V22" stroke="#000000" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M22 2L2 22" stroke="#000000" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 734 B |
@@ -34,7 +34,7 @@
|
|||||||
{href}
|
{href}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
data-sveltekit-replacestate={replaceState}
|
data-sveltekit-replacestate={replaceState}
|
||||||
class="{restProps.class} relative flex items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content"
|
class="{restProps.class} relative flex flex-shrink-0 items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content"
|
||||||
class:text-base-content={active}
|
class:text-base-content={active}
|
||||||
class:bg-base-100={active}>
|
class:bg-base-100={active}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
{...restProps}
|
{...restProps}
|
||||||
class="{restProps.class} relative flex w-full items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content"
|
class="{restProps.class} relative flex flex-shrink-0 w-full items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content"
|
||||||
class:text-base-content={active}
|
class:text-base-content={active}
|
||||||
class:bg-base-100={active}>
|
class:bg-base-100={active}>
|
||||||
{#if notification}
|
{#if notification}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const toHttpUrl = (url: string) =>
|
||||||
|
url
|
||||||
|
.replace(/^wss:\/\//, "https://")
|
||||||
|
.replace(/^ws:\/\//, "http://")
|
||||||
|
.replace(/\/$/, "")
|
||||||
|
|
||||||
|
const livekitEndpoint = (url: string, groupId?: string) => {
|
||||||
|
const base = `${toHttpUrl(url)}/.well-known/nip29/livekit`
|
||||||
|
return groupId ? `${base}/${groupId}` : base
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkRelayHasLivekit = async (url: string): Promise<boolean> => {
|
||||||
|
const endpoint = livekitEndpoint(url)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint)
|
||||||
|
return response.status === 204
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLivekitEndpoint = (url: string, groupId: string) => livekitEndpoint(url, groupId)
|
||||||
@@ -19,6 +19,36 @@ export const ucFirst = (s: string) => s.slice(0, 1).toUpperCase() + s.slice(1)
|
|||||||
|
|
||||||
export const errorMessage = (err: unknown) => String(err).replace(/^.*Error: /, "")
|
export const errorMessage = (err: unknown) => String(err).replace(/^.*Error: /, "")
|
||||||
|
|
||||||
|
export class AbortError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("Aborted")
|
||||||
|
this.name = "AbortError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimeoutError extends Error {
|
||||||
|
constructor(message = "Timed out") {
|
||||||
|
super(message)
|
||||||
|
this.name = "TimeoutError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a promise that rejects with AbortError when signal aborts. Use with Promise.race. */
|
||||||
|
export const whenAborted = (signal?: AbortSignal) => {
|
||||||
|
if (!signal) return new Promise<never>(() => {})
|
||||||
|
|
||||||
|
return new Promise<never>((_, reject) => {
|
||||||
|
const onAborted = () => reject(new AbortError())
|
||||||
|
if (signal.aborted) onAborted()
|
||||||
|
else signal.addEventListener("abort", onAborted, {once: true})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a promise that rejects with TimeoutError after ms. Use with Promise.race. */
|
||||||
|
export const whenTimeout = (ms: number, opts: {message?: string} = {}) => {
|
||||||
|
return new Promise<never>((_, reject) => setTimeout(() => reject(new TimeoutError()), ms))
|
||||||
|
}
|
||||||
|
|
||||||
export const buildUrl = (base: string | URL, ...pathname: string[]) => {
|
export const buildUrl = (base: string | URL, ...pathname: string[]) => {
|
||||||
const url = new URL(base)
|
const url = new URL(base)
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="ml-sai mt-sai mb-sai relative z-nav w-14 flex-shrink-0 bg-base-200 pt-4">
|
<div class="ml-sai mt-sai mb-sai relative z-nav w-14 flex-shrink-0 bg-base-200 pt-4">
|
||||||
<PrimaryNavSpaces />
|
<PrimaryNavSpaces />
|
||||||
</div>
|
</div>
|
||||||
<SecondaryNav class="!w-auto !flex flex-grow">
|
<SecondaryNav class="!flex !min-h-0 !w-auto flex-grow pb-4">
|
||||||
<SpaceMenu {url} />
|
<SpaceMenu {url} />
|
||||||
</SecondaryNav>
|
</SecondaryNav>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -42,11 +42,15 @@
|
|||||||
decodeRelay,
|
decodeRelay,
|
||||||
deriveRoom,
|
deriveRoom,
|
||||||
deriveUserRoomMembershipStatus,
|
deriveUserRoomMembershipStatus,
|
||||||
|
getRoomType,
|
||||||
MESSAGE_KINDS,
|
MESSAGE_KINDS,
|
||||||
MembershipStatus,
|
MembershipStatus,
|
||||||
PROTECTED,
|
PROTECTED,
|
||||||
|
RoomType,
|
||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
|
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||||
|
import {voiceState} from "@app/voice"
|
||||||
import {makeFeed} from "@app/core/requests"
|
import {makeFeed} from "@app/core/requests"
|
||||||
import {popKey} from "@lib/implicit"
|
import {popKey} from "@lib/implicit"
|
||||||
import {checked} from "@app/util/notifications"
|
import {checked} from "@app/util/notifications"
|
||||||
@@ -58,6 +62,7 @@
|
|||||||
const lastChecked = $checked[$page.url.pathname]
|
const lastChecked = $checked[$page.url.pathname]
|
||||||
const url = decodeRelay(relay)
|
const url = decodeRelay(relay)
|
||||||
const room = deriveRoom(url, h)
|
const room = deriveRoom(url, h)
|
||||||
|
const isVoiceRoom = $derived(getRoomType($room) === RoomType.Voice)
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
||||||
const at = $derived(parseInt($page.url.searchParams.get("at")!))
|
const at = $derived(parseInt($page.url.searchParams.get("at")!))
|
||||||
@@ -358,8 +363,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SpaceBar>
|
<SpaceBar>
|
||||||
{#snippet title()}
|
{#snippet icon()}
|
||||||
<RoomImage {url} {h} />
|
<RoomImage {url} {h} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
<RoomName {url} {h} />
|
<RoomName {url} {h} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
@@ -442,52 +449,61 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|
||||||
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
<div
|
||||||
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
|
class="chat__compose-zone flex flex-col gap-1 bg-base-200 md:flex-row md:gap-0"
|
||||||
<!-- pass -->
|
bind:this={chatCompose}>
|
||||||
{:else if $room.isRestricted && $membershipStatus !== MembershipStatus.Granted}
|
<div class="chat__compose-inner min-w-0 flex-1">
|
||||||
<div class="bg-alt card m-4 flex flex-row items-center justify-between px-4 py-3">
|
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
|
||||||
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
<!-- pass -->
|
||||||
{#if !$room.isClosed}
|
{:else if $room.isRestricted && $membershipStatus !== MembershipStatus.Granted}
|
||||||
{#if $membershipStatus === MembershipStatus.Pending}
|
<div class="bg-alt card m-4 flex flex-row items-center justify-between px-4 py-3">
|
||||||
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
||||||
<Icon icon={ClockCircle} />
|
{#if !$room.isClosed}
|
||||||
Access Pending
|
{#if $membershipStatus === MembershipStatus.Pending}
|
||||||
</Button>
|
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
||||||
{:else}
|
<Icon icon={ClockCircle} />
|
||||||
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
Access Pending
|
||||||
{#if joining}
|
</Button>
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
{:else}
|
||||||
{:else}
|
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
||||||
<Icon icon={Login2} />
|
{#if joining}
|
||||||
{/if}
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
Ask to Join
|
{:else}
|
||||||
</Button>
|
<Icon icon={Login2} />
|
||||||
|
{/if}
|
||||||
|
Ask to Join
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
{#if parent}
|
||||||
|
<RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||||
|
{/if}
|
||||||
|
{#if share}
|
||||||
|
<RoomComposeParent event={share} clear={clearShare} verb="Sharing" />
|
||||||
|
{/if}
|
||||||
|
{#if eventToEdit}
|
||||||
|
<RoomComposeEdit clear={clearEventToEdit} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#key eventToEdit}
|
||||||
|
<RoomCompose
|
||||||
|
{url}
|
||||||
|
{h}
|
||||||
|
{onSubmit}
|
||||||
|
{onEscape}
|
||||||
|
{onEditPrevious}
|
||||||
|
content={eventToEdit?.content}
|
||||||
|
bind:this={compose} />
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if isVoiceRoom || $voiceState === "joining" || $voiceState === "connected"}
|
||||||
|
<div class="hide-on-keyboard flex-shrink-0 p-2 md:hidden">
|
||||||
|
<VoiceWidget />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
|
||||||
<div>
|
|
||||||
{#if parent}
|
|
||||||
<RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
|
||||||
{/if}
|
|
||||||
{#if share}
|
|
||||||
<RoomComposeParent event={share} clear={clearShare} verb="Sharing" />
|
|
||||||
{/if}
|
|
||||||
{#if eventToEdit}
|
|
||||||
<RoomComposeEdit clear={clearEventToEdit} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#key eventToEdit}
|
|
||||||
<RoomCompose
|
|
||||||
{url}
|
|
||||||
{h}
|
|
||||||
{onSubmit}
|
|
||||||
{onEscape}
|
|
||||||
{onEditPrevious}
|
|
||||||
content={eventToEdit?.content}
|
|
||||||
bind:this={compose} />
|
|
||||||
{/key}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user