forked from coracle/flotilla
Compare commits
69 Commits
video-demo
..
1.6.5
| Author | SHA1 | Date | |
|---|---|---|---|
| c691400630 | |||
| 6acbfb1181 | |||
| 5f7474140f | |||
| c56d6f4c75 | |||
| c874ae50e5 | |||
| 25e7cc97f9 | |||
| 837e4bc537 | |||
| 65fa93d853 | |||
| 28b0276f17 | |||
| 10ac15f8a2 | |||
| a45633e214 | |||
| a42ba5446a | |||
| ccfe1bded5 | |||
| dfedf4e879 | |||
| 0682c404f2 | |||
| 80ece70450 | |||
| f6245c712d | |||
| e0c5f0d4f1 | |||
| b616e2ea33 | |||
| 6fb6995103 | |||
| 47bc0c2382 | |||
| 59a919d888 | |||
| 17d673c288 | |||
| 985fd46243 | |||
| 6d99e296e4 | |||
| 21c34efb6a | |||
| 9c2f923c26 | |||
| 52d2d70838 | |||
| edd8824c5e | |||
| 4f12ad9533 | |||
| 2a5850e67f | |||
| 15341edece | |||
| 30f8b4160e | |||
| 937ca5ecf6 | |||
| ba1757d4f1 | |||
| 5a2b5f43b8 | |||
| 2f487705c3 | |||
| 558d59ce88 | |||
| 1030edd322 | |||
| 981c8fd706 | |||
| 45ade602b5 | |||
| ef8a8682cd | |||
| 112ac4b6d5 | |||
| 3a26d2cb0b | |||
| a678bf42f1 | |||
| dc314a1d1b | |||
| 3af56f6bb1 | |||
| a996664e6c | |||
| 6e865fef06 | |||
| 588bd0f341 | |||
| 69f6abf4b6 | |||
| c8eb4ac31a | |||
| e3e69390ce | |||
| d0b34dfdf8 | |||
| bcdb3dc351 | |||
| a7b0031b8d | |||
| 2c05bc6961 | |||
| c2d0ec92bf | |||
| 407b4dce94 | |||
| 796157384f | |||
| 3446977df6 | |||
| f8016aba99 | |||
| 56d8527ed9 | |||
| 302788bcba | |||
| db075e602a | |||
| 67011d4740 | |||
| a35d867b34 | |||
| 23b59e54d7 | |||
| da2665d2bc |
+2
-2
@@ -1,6 +1,6 @@
|
||||
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
||||
VITE_POMADE_SIGNERS=https://pomade.coracle.social,https://pomade.fiatjaf.com,https://pomade.nostrver.se,https://pomade.scuttle.works
|
||||
VITE_POMADE_SIGNERS=
|
||||
VITE_PLATFORM_URL=https://app.flotilla.social
|
||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||
@@ -15,7 +15,7 @@ VITE_PUSH_BRIDGE=wss://npb.coracle.social/
|
||||
VITE_BLOCKED_RELAYS=brb.io,relay.nostr.band,nostr.mutinywallet.com,feeds.nostr.band,nostr.zbd.gg,wot.utxo.one,blastr.f7z.xyz,relay.current.fyi
|
||||
VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
|
||||
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
|
||||
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,nostr-01.uid.ovh,relay.keychat.io,relay.0xchat.com
|
||||
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com
|
||||
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
|
||||
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
||||
VITE_GLITCHTIP_API_KEY=
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: coracle-social/flotilla
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
|
||||
@@ -25,7 +25,6 @@ android/app/src/main/assets/public/
|
||||
|
||||
# Web/JavaScript
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
build/
|
||||
.svelte-kit/
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
# Current
|
||||
|
||||
* Enable email/password login
|
||||
* Add up/edit to direct messages
|
||||
|
||||
# 1.6.5
|
||||
|
||||
* Attempt to fix permission grant for notifications
|
||||
|
||||
+2
-3
@@ -4,8 +4,6 @@
|
||||
|
||||
FROM node:20-bookworm AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl
|
||||
|
||||
RUN npm install -g pnpm@latest
|
||||
|
||||
WORKDIR /app
|
||||
@@ -22,6 +20,7 @@ ENV VITE_BUILD_HASH=${VITE_BUILD_HASH}
|
||||
ENV NODE_OPTIONS=--max_old_space_size=16384
|
||||
RUN pnpm run build
|
||||
|
||||
# Stage 2: Runtime
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
@@ -29,4 +28,4 @@ WORKDIR /app
|
||||
# Copy only the built output - no source, no .env, no dev deps
|
||||
COPY --from=builder /app/build ./build
|
||||
|
||||
CMD ["npx", "serve", "-s", "build"]
|
||||
CMD ["npx", "serve", "build"]
|
||||
|
||||
@@ -6,7 +6,7 @@ If you would like to be interoperable with Flotilla, please check out this guide
|
||||
|
||||
## Environment
|
||||
|
||||
You can also optionally create an `.env.local` file and populate it with the following environment variables (see `.env.template` for examples):
|
||||
You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` for examples):
|
||||
|
||||
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
|
||||
- `VITE_PLATFORM_URL` - The url where the app will be hosted
|
||||
@@ -29,7 +29,7 @@ To run your own Flotilla, it's as simple as:
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm run build
|
||||
npx serve -s build
|
||||
npx serve build
|
||||
```
|
||||
|
||||
Or, if you prefer to use a container:
|
||||
|
||||
@@ -42,6 +42,4 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<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>
|
||||
|
||||
@@ -4,9 +4,6 @@ const config: CapacitorConfig = {
|
||||
appId: "social.flotilla",
|
||||
appName: "Flotilla",
|
||||
webDir: "build",
|
||||
ios: {
|
||||
scheme: "Flotilla Chat",
|
||||
},
|
||||
android: {
|
||||
adjustMarginsForEdgeToEdge: true,
|
||||
},
|
||||
|
||||
@@ -20,16 +20,8 @@
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<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>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -55,5 +47,11 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
+10
-11
@@ -65,16 +65,16 @@
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"@vite-pwa/sveltekit": "^0.6.8",
|
||||
"@welshman/app": "^0.8.9",
|
||||
"@welshman/content": "^0.8.9",
|
||||
"@welshman/editor": "^0.8.9",
|
||||
"@welshman/feeds": "^0.8.9",
|
||||
"@welshman/lib": "^0.8.9",
|
||||
"@welshman/net": "^0.8.9",
|
||||
"@welshman/router": "^0.8.9",
|
||||
"@welshman/signer": "^0.8.9",
|
||||
"@welshman/store": "^0.8.9",
|
||||
"@welshman/util": "^0.8.9",
|
||||
"@welshman/app": "^0.8.8",
|
||||
"@welshman/content": "^0.8.8",
|
||||
"@welshman/editor": "^0.8.8",
|
||||
"@welshman/feeds": "^0.8.8",
|
||||
"@welshman/lib": "^0.8.8",
|
||||
"@welshman/net": "^0.8.8",
|
||||
"@welshman/router": "^0.8.8",
|
||||
"@welshman/signer": "^0.8.8",
|
||||
"@welshman/store": "^0.8.8",
|
||||
"@welshman/util": "^0.8.8",
|
||||
"compressorjs-next": "^1.1.2",
|
||||
"daisyui": "^4.12.24",
|
||||
"date-picker-svelte": "^2.17.0",
|
||||
@@ -83,7 +83,6 @@
|
||||
"fuse.js": "^7.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"idb": "^8.0.3",
|
||||
"livekit-client": "^2.17.2",
|
||||
"nostr-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main",
|
||||
"nostr-tools": "^2.19.4",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
|
||||
Generated
+111
-199
@@ -58,7 +58,7 @@ importers:
|
||||
version: 1.9.7
|
||||
'@pomade/core':
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.9(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
version: 0.2.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@poppanator/sveltekit-svg':
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(rollup@2.80.0)(svelte@5.48.0)(svgo@3.3.2)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0))
|
||||
@@ -81,35 +81,35 @@ importers:
|
||||
specifier: ^0.6.8
|
||||
version: 0.6.8(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.48.0)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0)))(svelte@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.21(@types/node@25.0.10)(terser@5.46.0))(workbox-build@7.3.0)(workbox-window@7.3.0))
|
||||
'@welshman/app':
|
||||
specifier: ^0.8.9
|
||||
version: 0.8.9(56a9569377ccbc308c0adef0d87b4892)
|
||||
specifier: ^0.8.8
|
||||
version: 0.8.8(b90dd618d8ad3ba87405490e903259ce)
|
||||
'@welshman/content':
|
||||
specifier: ^0.8.9
|
||||
version: 0.8.9(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
specifier: ^0.8.8
|
||||
version: 0.8.8(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/editor':
|
||||
specifier: ^0.8.9
|
||||
version: 0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
specifier: ^0.8.8
|
||||
version: 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/feeds':
|
||||
specifier: ^0.8.9
|
||||
version: 0.8.9(e7b1650516a86ec271bd2c0f047c2e03)
|
||||
specifier: ^0.8.8
|
||||
version: 0.8.8(827c582d718d0d373e9315813bab1085)
|
||||
'@welshman/lib':
|
||||
specifier: ^0.8.9
|
||||
version: 0.8.9
|
||||
specifier: ^0.8.8
|
||||
version: 0.8.8
|
||||
'@welshman/net':
|
||||
specifier: ^0.8.9
|
||||
version: 0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
specifier: ^0.8.8
|
||||
version: 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/router':
|
||||
specifier: ^0.8.9
|
||||
version: 0.8.9(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))
|
||||
specifier: ^0.8.8
|
||||
version: 0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))
|
||||
'@welshman/signer':
|
||||
specifier: ^0.8.9
|
||||
version: 0.8.9(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
specifier: ^0.8.8
|
||||
version: 0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/store':
|
||||
specifier: ^0.8.9
|
||||
version: 0.8.9(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)
|
||||
specifier: ^0.8.8
|
||||
version: 0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)
|
||||
'@welshman/util':
|
||||
specifier: ^0.8.9
|
||||
version: 0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
specifier: ^0.8.8
|
||||
version: 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
compressorjs-next:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
@@ -134,9 +134,6 @@ importers:
|
||||
idb:
|
||||
specifier: ^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:
|
||||
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)
|
||||
@@ -740,9 +737,6 @@ packages:
|
||||
'@braintree/sanitize-url@7.1.1':
|
||||
resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
|
||||
|
||||
'@bufbuild/protobuf@1.10.1':
|
||||
resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==}
|
||||
|
||||
'@canvas/image-data@1.1.0':
|
||||
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
|
||||
|
||||
@@ -1289,12 +1283,6 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
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':
|
||||
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
|
||||
|
||||
@@ -1835,9 +1823,6 @@ packages:
|
||||
'@types/cookie@0.6.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
|
||||
|
||||
@@ -1982,83 +1967,83 @@ packages:
|
||||
'@vite-pwa/assets-generator':
|
||||
optional: true
|
||||
|
||||
'@welshman/app@0.8.9':
|
||||
resolution: {integrity: sha512-2ff0Y9JzSVqJz9qY8vPDY7CC9xBZ5KQPLlVRX2OGnwopmLm9P68i6u8eJG53caxCUv+d7RCDXNlYOkFH6hr7nw==}
|
||||
'@welshman/app@0.8.8':
|
||||
resolution: {integrity: sha512-pyySouAJwGZ2RSC29egiFft38Ctuioodon6xWFxB7HvJ9Llsh5b53qjkrQcAYM7lUAzXwtalf2v4Z3EwYdUObg==}
|
||||
peerDependencies:
|
||||
'@pomade/core': ^0.2.1
|
||||
'@welshman/feeds': 0.8.9
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/net': 0.8.9
|
||||
'@welshman/router': 0.8.9
|
||||
'@welshman/signer': 0.8.9
|
||||
'@welshman/store': 0.8.9
|
||||
'@welshman/util': 0.8.9
|
||||
'@pomade/core': ^0.1.3
|
||||
'@welshman/feeds': 0.8.8
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/net': 0.8.8
|
||||
'@welshman/router': 0.8.8
|
||||
'@welshman/signer': 0.8.8
|
||||
'@welshman/store': 0.8.8
|
||||
'@welshman/util': 0.8.8
|
||||
svelte: ^4.0.0 || ^5.0.0
|
||||
|
||||
'@welshman/content@0.8.9':
|
||||
resolution: {integrity: sha512-K9r1hAooqM857Ze4i0kF/LSqOZjhuYDsbY07kA1pjbkfrgf8cLuaVP+qicDxGfHD4kKDWuCb/PkUf2kC8nOjuQ==}
|
||||
'@welshman/content@0.8.8':
|
||||
resolution: {integrity: sha512-5jh2YMoqINzkOEVSDZec6JbAqiC0WThwRuPwJOwiJlAFYQ4LC0MAT1HQ8z9pht/0TXdjYQUu2X+jngqqICNOiw==}
|
||||
peerDependencies:
|
||||
nostr-tools: ^2.19.4
|
||||
|
||||
'@welshman/editor@0.8.9':
|
||||
resolution: {integrity: sha512-iDBp/qaZBGaaKfSk+7hrJkgzssovZLAkoT5ULjFoBpT9NfpJu/5WYfoUYwKxa6fS9Z84veS39y/PW1LFBNPnpg==}
|
||||
'@welshman/editor@0.8.8':
|
||||
resolution: {integrity: sha512-54WD2d6HEEiuoPgl/LeE4eaLtF2/SrYObk+IE9UUrJjoXcK/BK3vt8ltzazvBLR8ntfKOQINc4DhkeuBxiiCpA==}
|
||||
peerDependencies:
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/util': 0.8.9
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/util': 0.8.8
|
||||
nostr-editor: ^1.1.1
|
||||
nostr-tools: ^2.19.4
|
||||
|
||||
'@welshman/feeds@0.8.9':
|
||||
resolution: {integrity: sha512-8JI6rrETqDqa9VdU0eEP1OBvTjnampfgs0ZhdOELy9itFcuqDz8wPScnw3sygApL53tkFv4n2xnjZE7E3N3U/w==}
|
||||
'@welshman/feeds@0.8.8':
|
||||
resolution: {integrity: sha512-o5JuptpWSNr6wtbM0RfSxTJgZStaNxPz160tE9u0SZzs1/a9sq/Yzesw7s+g0nKukRjBbl70DOqpTqOqfXAEIw==}
|
||||
peerDependencies:
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/net': 0.8.9
|
||||
'@welshman/router': 0.8.9
|
||||
'@welshman/signer': 0.8.9
|
||||
'@welshman/util': 0.8.9
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/net': 0.8.8
|
||||
'@welshman/router': 0.8.8
|
||||
'@welshman/signer': 0.8.8
|
||||
'@welshman/util': 0.8.8
|
||||
|
||||
'@welshman/lib@0.8.9':
|
||||
resolution: {integrity: sha512-Gk9MXaJNuLL9EguP2RnoaGaQy6x0BrneZfj9gL5t6ZNIF+1g+maJssKDbCRjdDPeuNQbRhh7AlSGSQUJuhkq6Q==}
|
||||
'@welshman/lib@0.8.8':
|
||||
resolution: {integrity: sha512-77ZfVtodV05276ceR8c+JdDFqhOpmy2W6PkgDYbnKstQzKb5TN6wBvcLKxJppTzWMeWbyi2JADsuOYvW1jpOSQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
'@welshman/net@0.8.9':
|
||||
resolution: {integrity: sha512-0brgfS7pHlE23CmAVLZ/RCGVvyjq+MX4NAhFyqxXuCCcqN7Lf9t60aQ6Z0aKl2dA79yVDtS6d1S53RR1rNS+Ew==}
|
||||
'@welshman/net@0.8.8':
|
||||
resolution: {integrity: sha512-Rug3GzVzyABG21g++cCLOVXdjAieV6rJUZqstE8i/olZvOEWZpZ9R901DoUSDR07U2HTrAwHQrjgb1HmH4jiDQ==}
|
||||
peerDependencies:
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/util': 0.8.9
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/util': 0.8.8
|
||||
|
||||
'@welshman/router@0.8.9':
|
||||
resolution: {integrity: sha512-Kjf7CyO8wvnsVS3TX0eRUVd327F4vsDUdJFpo1MYjKRmgwj7ebOiofY8Fx011BX/GcpVVq7pUFs5792cSjlsrQ==}
|
||||
'@welshman/router@0.8.8':
|
||||
resolution: {integrity: sha512-j5O7F7KGQtOIvBJctEiUNcLfHBUnhHlYHxUx7ImPPurc1zLzt3JovvJJFubXMQoQ26D01DsK/AA1L5WZNebUhA==}
|
||||
peerDependencies:
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/net': 0.8.9
|
||||
'@welshman/util': 0.8.9
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/net': 0.8.8
|
||||
'@welshman/util': 0.8.8
|
||||
|
||||
'@welshman/signer@0.8.9':
|
||||
resolution: {integrity: sha512-PVnZn5Rz+10hH35f350JVo1ug3qFeunraOpCcPkmo8XVA0bEEY5503OMGfu1osfEI6KMseoFPU/AdZrh+w+ZQQ==}
|
||||
version: 0.8.9
|
||||
'@welshman/signer@0.8.8':
|
||||
resolution: {integrity: sha512-rswHrTdc1+yvAno2h3JELzjp+LCfiYfUr8ACvwSSHAqDwrtezppfh0WDEPaYBp2EVSJ6tKMM1sVey0quO63aMw==}
|
||||
version: 0.8.8
|
||||
peerDependencies:
|
||||
'@noble/curves': ^1.9.7
|
||||
'@noble/hashes': ^2.0.1
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/net': 0.8.9
|
||||
'@welshman/util': 0.8.9
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/net': 0.8.8
|
||||
'@welshman/util': 0.8.8
|
||||
nostr-signer-capacitor-plugin: '*'
|
||||
nostr-tools: ^2.19.4
|
||||
|
||||
'@welshman/store@0.8.9':
|
||||
resolution: {integrity: sha512-wchFOvQB/E5/j5oyqw0QmIx1XzWtm0b3b2mtNUKF7bdtX7YskiSeLcXalJTALD+WkW02cGzBw2SvoJjtDiyWnw==}
|
||||
'@welshman/store@0.8.8':
|
||||
resolution: {integrity: sha512-mTFueKZi9CtrtvCZT5eT5QaLMs94LxQg4y7oO5PZp9wv8EGSnB9p7XIflM0OfpKwF7c0pu1RdXcjVlvMDsC6QQ==}
|
||||
peerDependencies:
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/net': 0.8.9
|
||||
'@welshman/util': 0.8.9
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/net': 0.8.8
|
||||
'@welshman/util': 0.8.8
|
||||
svelte: ^4.0.0 || ^5.0.0
|
||||
|
||||
'@welshman/util@0.8.9':
|
||||
resolution: {integrity: sha512-oOijx0PCsTVhPOPr+5HS4mZFntrtHAW8cdBvJqu/Asf+m6UrvVCeuoF3NDtKhWbkuD6uZnfeJy6WDKVhTptZEA==}
|
||||
'@welshman/util@0.8.8':
|
||||
resolution: {integrity: sha512-SNT1VXab6ce36EVfjs1A2uwWs5elYTI4eXi8SUuj42k8CqNIAtG+bOf/JFIxXNTfl3NSxxZdWzpLLZWBqgpAxQ==}
|
||||
peerDependencies:
|
||||
'@noble/curves': ^1.9.7
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/lib': 0.8.8
|
||||
nostr-tools: ^2.19.4
|
||||
|
||||
'@xml-tools/parser@1.0.11':
|
||||
@@ -3349,9 +3334,6 @@ packages:
|
||||
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||
hasBin: true
|
||||
|
||||
jose@6.2.1:
|
||||
resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==}
|
||||
|
||||
js-base64@3.7.8:
|
||||
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
|
||||
|
||||
@@ -3456,11 +3438,6 @@ packages:
|
||||
linkifyjs@4.3.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3495,10 +3472,6 @@ packages:
|
||||
lodash@4.17.23:
|
||||
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:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
@@ -4300,9 +4273,6 @@ packages:
|
||||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
rxjs@7.8.2:
|
||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||
|
||||
sade@1.8.1:
|
||||
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -4332,13 +4302,6 @@ packages:
|
||||
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||
hasBin: true
|
||||
@@ -4740,9 +4703,6 @@ packages:
|
||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
typed-emitter@2.1.0:
|
||||
resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==}
|
||||
|
||||
typescript-eslint@8.53.1:
|
||||
resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -4887,10 +4847,6 @@ packages:
|
||||
webidl-conversions@4.0.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
@@ -5777,8 +5733,6 @@ snapshots:
|
||||
|
||||
'@braintree/sanitize-url@7.1.1': {}
|
||||
|
||||
'@bufbuild/protobuf@1.10.1': {}
|
||||
|
||||
'@canvas/image-data@1.1.0': {}
|
||||
|
||||
'@capacitor-community/safe-area@8.0.1(@capacitor/core@8.0.1)':
|
||||
@@ -6344,12 +6298,6 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@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@1.3.0': {}
|
||||
@@ -6488,15 +6436,15 @@ snapshots:
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@pomade/core@0.2.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.9(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||
'@pomade/core@0.2.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@frostr/bifrost': 1.0.7(typescript@5.9.3)
|
||||
'@noble/hashes': 2.0.1
|
||||
'@peculiar/x509': 1.14.3
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/net': 0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/signer': 0.8.9(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/util': 0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/signer': 0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
cbor-x: 1.6.0
|
||||
hash-wasm: 4.12.0
|
||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||
@@ -6911,8 +6859,6 @@ snapshots:
|
||||
|
||||
'@types/cookie@0.6.0': {}
|
||||
|
||||
'@types/dom-mediacapture-record@1.0.22': {}
|
||||
|
||||
'@types/eslint@9.6.1':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
@@ -7085,26 +7031,26 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@vite-pwa/assets-generator': 0.2.6
|
||||
|
||||
'@welshman/app@0.8.9(56a9569377ccbc308c0adef0d87b4892)':
|
||||
'@welshman/app@0.8.8(b90dd618d8ad3ba87405490e903259ce)':
|
||||
dependencies:
|
||||
'@pomade/core': 0.2.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.9(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/feeds': 0.8.9(e7b1650516a86ec271bd2c0f047c2e03)
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/net': 0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/router': 0.8.9(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))
|
||||
'@welshman/signer': 0.8.9(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/store': 0.8.9(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)
|
||||
'@welshman/util': 0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@pomade/core': 0.2.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/feeds': 0.8.8(827c582d718d0d373e9315813bab1085)
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/router': 0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))
|
||||
'@welshman/signer': 0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/store': 0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)
|
||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
fuse.js: 7.1.0
|
||||
svelte: 5.48.0
|
||||
throttle-debounce: 5.0.2
|
||||
|
||||
'@welshman/content@0.8.9(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||
'@welshman/content@0.8.8(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@braintree/sanitize-url': 7.1.1
|
||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||
|
||||
'@welshman/editor@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||
'@welshman/editor@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.27.2(@tiptap/pm@2.27.2)
|
||||
'@tiptap/extension-code': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
||||
@@ -7119,64 +7065,64 @@ snapshots:
|
||||
'@tiptap/extension-text': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
||||
'@tiptap/pm': 2.27.2
|
||||
'@tiptap/suggestion': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/util': 0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
nostr-editor: 1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))
|
||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||
tippy.js: 6.3.7
|
||||
|
||||
'@welshman/feeds@0.8.9(e7b1650516a86ec271bd2c0f047c2e03)':
|
||||
'@welshman/feeds@0.8.8(827c582d718d0d373e9315813bab1085)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/net': 0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/router': 0.8.9(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))
|
||||
'@welshman/signer': 0.8.9(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/util': 0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/router': 0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))
|
||||
'@welshman/signer': 0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
trava: 1.2.1
|
||||
|
||||
'@welshman/lib@0.8.9':
|
||||
'@welshman/lib@0.8.8':
|
||||
dependencies:
|
||||
'@scure/base': 1.2.6
|
||||
'@types/events': 3.0.3
|
||||
events: 3.3.0
|
||||
|
||||
'@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)':
|
||||
'@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/util': 0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
events: 3.3.0
|
||||
isomorphic-ws: 5.0.0(ws@8.18.3)
|
||||
transitivePeerDependencies:
|
||||
- ws
|
||||
|
||||
'@welshman/router@0.8.9(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))':
|
||||
'@welshman/router@0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/net': 0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/util': 0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
|
||||
'@welshman/signer@0.8.9(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||
'@welshman/signer@0.8.8(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@noble/curves': 1.9.7
|
||||
'@noble/hashes': 2.0.1
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/net': 0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/util': 0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
nostr-signer-capacitor-plugin: https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1)
|
||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||
|
||||
'@welshman/store@0.8.9(@welshman/lib@0.8.9)(@welshman/net@0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)':
|
||||
'@welshman/store@0.8.8(@welshman/lib@0.8.8)(@welshman/net@0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)':
|
||||
dependencies:
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/net': 0.8.9(@welshman/lib@0.8.9)(@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/util': 0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/lib': 0.8.8
|
||||
'@welshman/net': 0.8.8(@welshman/lib@0.8.8)(@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
|
||||
'@welshman/util': 0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
svelte: 5.48.0
|
||||
|
||||
'@welshman/util@0.8.9(@noble/curves@1.9.7)(@welshman/lib@0.8.9)(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||
'@welshman/util@0.8.8(@noble/curves@1.9.7)(@welshman/lib@0.8.8)(nostr-tools@2.20.0(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@noble/curves': 1.9.7
|
||||
'@types/ws': 8.18.1
|
||||
'@welshman/lib': 0.8.9
|
||||
'@welshman/lib': 0.8.8
|
||||
js-base64: 3.7.8
|
||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||
nostr-wasm: 0.1.0
|
||||
@@ -8584,8 +8530,6 @@ snapshots:
|
||||
|
||||
jiti@1.21.7: {}
|
||||
|
||||
jose@6.2.1: {}
|
||||
|
||||
js-base64@3.7.8: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@@ -8661,19 +8605,6 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -8706,8 +8637,6 @@ snapshots:
|
||||
|
||||
lodash@4.17.23: {}
|
||||
|
||||
loglevel@1.9.2: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.4: {}
|
||||
@@ -9498,11 +9427,6 @@ snapshots:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
|
||||
rxjs@7.8.2:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
sade@1.8.1:
|
||||
dependencies:
|
||||
mri: 1.2.0
|
||||
@@ -9534,10 +9458,6 @@ snapshots:
|
||||
|
||||
sax@1.4.4: {}
|
||||
|
||||
sdp-transform@2.15.0: {}
|
||||
|
||||
sdp@3.2.1: {}
|
||||
|
||||
semver@5.7.2: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
@@ -10062,10 +9982,6 @@ snapshots:
|
||||
possible-typed-array-names: 1.1.0
|
||||
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):
|
||||
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)
|
||||
@@ -10174,10 +10090,6 @@ snapshots:
|
||||
|
||||
webidl-conversions@4.0.2: {}
|
||||
|
||||
webrtc-adapter@9.0.4:
|
||||
dependencies:
|
||||
sdp: 3.2.1
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
|
||||
-50
@@ -394,35 +394,6 @@ progress[value]::-webkit-progress-value {
|
||||
@apply w-full md:left-[18.5rem] md:w-[calc(100%-18.5rem-var(--sair))];
|
||||
}
|
||||
|
||||
.cw-video-call-content {
|
||||
@apply w-full md:left-[calc(18.5rem+18rem)] md:w-[calc(100%-18.5rem-18rem-var(--sair))];
|
||||
}
|
||||
|
||||
/* Voice: desktop split — plain CSS so / in calc is not parsed as Tailwind slash syntax */
|
||||
.cw-split-video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cw-split-chat {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.cw-split-video {
|
||||
left: 18.5rem;
|
||||
right: auto;
|
||||
width: calc((100vw - 18.5rem - var(--sair)) / 2);
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.cw-split-chat {
|
||||
left: calc(18.5rem + (100vw - 18.5rem - var(--sair)) / 2);
|
||||
right: auto;
|
||||
width: calc((100vw - 18.5rem - var(--sair)) / 2);
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cw-full {
|
||||
@apply w-full md:left-[4rem] md:w-[calc(100%-4rem-var(--sair))];
|
||||
}
|
||||
@@ -451,27 +422,6 @@ body.keyboard-open .hide-on-keyboard {
|
||||
@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__compose-zone.cw-video-call-content {
|
||||
@apply md:left-[calc(18.5rem+18rem)] md:w-[calc(100%-18.5rem-18rem-var(--sair))];
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.chat__compose-zone.cw-split-chat {
|
||||
left: calc(18.5rem + (100vw - 18.5rem - var(--sair)) / 2);
|
||||
right: auto;
|
||||
width: calc((100vw - 18.5rem - var(--sair)) / 2);
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chat__scroll-down {
|
||||
@apply pb-sai fixed bottom-28 right-4 z-feature md:bottom-16;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Dialog from "@lib/components/Dialog.svelte"
|
||||
import Landing from "@app/components/Landing.svelte"
|
||||
import Toast from "@app/components/Toast.svelte"
|
||||
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
||||
import {modal} from "@app/util/modal"
|
||||
import {modals} from "@app/util/modal"
|
||||
|
||||
interface Props {
|
||||
children: Snippet
|
||||
@@ -19,7 +20,7 @@
|
||||
<PrimaryNav>
|
||||
{@render children?.()}
|
||||
</PrimaryNav>
|
||||
{:else if !$modal}
|
||||
{:else if !$modals[$page.url.hash.slice(1)]}
|
||||
<Dialog children={{component: Landing, props: {}}} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
+86
-143
@@ -2,11 +2,9 @@
|
||||
import type {Snippet} from "svelte"
|
||||
import {onMount} from "svelte"
|
||||
import {
|
||||
ago,
|
||||
int,
|
||||
ms,
|
||||
partition,
|
||||
ifLet,
|
||||
spec,
|
||||
nthEq,
|
||||
nthNe,
|
||||
@@ -48,12 +46,11 @@
|
||||
import ChatMembers from "@app/components/ChatMembers.svelte"
|
||||
import ChatMessage from "@app/components/ChatMessage.svelte"
|
||||
import ChatCompose from "@app/components/ChatCompose.svelte"
|
||||
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
|
||||
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import {userSettingsValues, PLATFORM_NAME, deriveChat} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {makeDelete, prependParent} from "@app/core/commands"
|
||||
import {prependParent} from "@app/core/commands"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type Props = {
|
||||
@@ -81,115 +78,73 @@
|
||||
parent = undefined
|
||||
}
|
||||
|
||||
const clearEventToEdit = () => {
|
||||
eventToEdit = undefined
|
||||
}
|
||||
|
||||
const onSubmit = async (params: EventContent) => {
|
||||
try {
|
||||
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
|
||||
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
|
||||
|
||||
// Remove p tags since they result in forking the conversation
|
||||
params.tags = params.tags.filter(nthNe(0, "p"))
|
||||
// Remove p tags since they result in forking the conversation
|
||||
params.tags = params.tags.filter(nthNe(0, "p"))
|
||||
|
||||
// Add our reply quote to content
|
||||
params = prependParent(parent, params)
|
||||
// Add our reply quote to content
|
||||
params = prependParent(parent, params)
|
||||
|
||||
if (eventToEdit) {
|
||||
if (eventToEdit.content === params.content) {
|
||||
return
|
||||
}
|
||||
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
|
||||
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
|
||||
const templates: EventTemplate[] = []
|
||||
const buffer = []
|
||||
|
||||
await sendWrapped({
|
||||
event: makeDelete({event: eventToEdit, protect: false}),
|
||||
recipients: pubkeys,
|
||||
})
|
||||
const addTemplate = (kind: number, content: string, tags: string[][]) => {
|
||||
content = content.trim()
|
||||
|
||||
if (content) {
|
||||
templates.push(makeEvent(kind, {content, tags: [...tags, ...ptags]}))
|
||||
}
|
||||
|
||||
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
|
||||
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
|
||||
const templates: EventTemplate[] = []
|
||||
const buffer = []
|
||||
|
||||
const addTemplate = (kind: number, content: string, tags: string[][]) => {
|
||||
content = content.trim()
|
||||
|
||||
if (content) {
|
||||
templates.push(
|
||||
makeEvent(kind, {
|
||||
content,
|
||||
tags: [...tags, ...ptags],
|
||||
created_at: eventToEdit?.created_at,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of parse(params)) {
|
||||
const imeta = isLink(p)
|
||||
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
|
||||
: undefined
|
||||
|
||||
if (isLink(p) && imeta) {
|
||||
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||
addTemplate(
|
||||
DIRECT_MESSAGE_FILE,
|
||||
p.value.url.toString(),
|
||||
imeta.slice(1).filter(nthNe(0, "url")),
|
||||
)
|
||||
} else {
|
||||
buffer.push(p.raw)
|
||||
}
|
||||
}
|
||||
|
||||
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||
|
||||
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
|
||||
// Sleep 1 second between each one to make sure timestamps are distinct
|
||||
const thunks = await Promise.all(
|
||||
Array.from(enumerate(templates)).map(([i, event]) =>
|
||||
sendWrapped({
|
||||
event,
|
||||
recipients: pubkeys,
|
||||
delay: $userSettingsValues.send_delay + ms(i),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
pushToast({
|
||||
timeout: 30_000,
|
||||
children: {
|
||||
component: ThunkToast,
|
||||
props: {thunk: mergeThunks(thunks)},
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
clearParent()
|
||||
clearEventToEdit()
|
||||
}
|
||||
}
|
||||
|
||||
const onEscape = () => {
|
||||
for (const p of parse(params)) {
|
||||
const imeta = isLink(p)
|
||||
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
|
||||
: undefined
|
||||
|
||||
if (isLink(p) && imeta) {
|
||||
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||
addTemplate(
|
||||
DIRECT_MESSAGE_FILE,
|
||||
p.value.url.toString(),
|
||||
imeta.slice(1).filter(nthNe(0, "url")),
|
||||
)
|
||||
} else {
|
||||
buffer.push(p.raw)
|
||||
}
|
||||
}
|
||||
|
||||
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||
|
||||
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
|
||||
// Sleep 1 second between each one to make sure timestamps are distinct
|
||||
const thunks = await Promise.all(
|
||||
Array.from(enumerate(templates)).map(([i, event]) =>
|
||||
sendWrapped({
|
||||
event,
|
||||
recipients: pubkeys,
|
||||
delay: $userSettingsValues.send_delay + ms(i),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
pushToast({
|
||||
timeout: 30_000,
|
||||
children: {
|
||||
component: ThunkToast,
|
||||
props: {thunk: mergeThunks(thunks)},
|
||||
},
|
||||
})
|
||||
|
||||
clearParent()
|
||||
clearEventToEdit()
|
||||
}
|
||||
|
||||
const canEditEvent = (event: TrustedEvent) =>
|
||||
event.pubkey === $pubkey &&
|
||||
event.kind === DIRECT_MESSAGE &&
|
||||
event.created_at >= ago(500, MINUTE)
|
||||
|
||||
const onEditEvent = (event: TrustedEvent) => {
|
||||
clearParent()
|
||||
eventToEdit = event
|
||||
}
|
||||
|
||||
const onEditPrevious = () => ifLet($chat?.messages.toReversed().find(canEditEvent), onEditEvent)
|
||||
|
||||
let loading = $state(true)
|
||||
let compose: ChatCompose | undefined = $state()
|
||||
let parent: TrustedEvent | undefined = $state()
|
||||
let eventToEdit: TrustedEvent | undefined = $state()
|
||||
let chatCompose: HTMLElement | undefined = $state()
|
||||
let dynamicPadding: HTMLElement | undefined = $state()
|
||||
|
||||
@@ -249,36 +204,36 @@
|
||||
</script>
|
||||
|
||||
<PageBar>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="ellipsize flex items-center gap-4 whitespace-nowrap">
|
||||
<Button class="flex flex-col gap-1 sm:flex-row sm:gap-2" onclick={showMembers}>
|
||||
{#if others.length === 0}
|
||||
<div class="row-2">
|
||||
<ProfileCircle pubkey={$pubkey!} size={5} />
|
||||
<ProfileName pubkey={$pubkey!} />
|
||||
</div>
|
||||
{:else if others.length === 1}
|
||||
<div class="row-2">
|
||||
<ProfileCircle pubkey={others[0]} size={5} />
|
||||
{#snippet title()}
|
||||
<Button class="flex flex-col gap-1 sm:flex-row sm:gap-2" onclick={showMembers}>
|
||||
{#if others.length === 0}
|
||||
<div class="row-2">
|
||||
<ProfileCircle pubkey={$pubkey!} size={5} />
|
||||
<ProfileName pubkey={$pubkey!} />
|
||||
</div>
|
||||
{:else if others.length === 1}
|
||||
<div class="row-2">
|
||||
<ProfileCircle pubkey={others[0]} size={5} />
|
||||
<ProfileName pubkey={others[0]} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
<ProfileCircles pubkeys={others} size={5} />
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<ProfileName pubkey={others[0]} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
<ProfileCircles pubkeys={others} size={5} />
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<ProfileName pubkey={others[0]} />
|
||||
and
|
||||
{#if others.length === 2}
|
||||
<ProfileName pubkey={others[1]} />
|
||||
{:else}
|
||||
{others.length - 1}
|
||||
{others.length > 2 ? "others" : "other"}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
and
|
||||
{#if others.length === 2}
|
||||
<ProfileName pubkey={others[1]} />
|
||||
{:else}
|
||||
{others.length - 1}
|
||||
{others.length > 2 ? "others" : "other"}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
{#if remove($pubkey, missingRelayLists).length > 0}
|
||||
{@const count = remove($pubkey, missingRelayLists).length}
|
||||
{@const label = count > 1 ? "lists are" : "list is"}
|
||||
@@ -289,7 +244,7 @@
|
||||
{count}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
|
||||
<PageContent class="flex flex-col-reverse gap-2 pt-4">
|
||||
@@ -330,9 +285,7 @@
|
||||
event={$state.snapshot(value as TrustedEvent)}
|
||||
{pubkeys}
|
||||
{showPubkey}
|
||||
{replyTo}
|
||||
canEdit={canEditEvent}
|
||||
onEdit={onEditEvent} />
|
||||
{replyTo} />
|
||||
{/if}
|
||||
{/each}
|
||||
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
||||
@@ -352,16 +305,6 @@
|
||||
{#if parent}
|
||||
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||
{/if}
|
||||
{#if eventToEdit}
|
||||
<ChatComposeEdit clear={clearEventToEdit} />
|
||||
{/if}
|
||||
</div>
|
||||
{#key eventToEdit}
|
||||
<ChatCompose
|
||||
bind:this={compose}
|
||||
{onSubmit}
|
||||
{onEscape}
|
||||
{onEditPrevious}
|
||||
content={eventToEdit?.content} />
|
||||
{/key}
|
||||
<ChatCompose bind:this={compose} {onSubmit} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import {onDestroy, onMount} from "svelte"
|
||||
import {writable} from "svelte/store"
|
||||
import type {EventContent} from "@welshman/util"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
@@ -11,13 +10,10 @@
|
||||
import {makeEditor} from "@app/editor"
|
||||
|
||||
type Props = {
|
||||
content?: string
|
||||
onEscape?: () => void
|
||||
onEditPrevious?: () => void
|
||||
onSubmit: (event: EventContent) => void
|
||||
}
|
||||
|
||||
const {content, onEscape, onEditPrevious, onSubmit}: Props = $props()
|
||||
const {onSubmit}: Props = $props()
|
||||
|
||||
const autofocus = !isMobile
|
||||
|
||||
@@ -25,19 +21,6 @@
|
||||
|
||||
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
||||
|
||||
export const canEnterEditPrevious = () =>
|
||||
editor.then(ed => ed.getText({blockSeparator: "\n"}) === "")
|
||||
|
||||
const handleKeyDown = async (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
onEscape?.()
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp" && (await canEnterEditPrevious())) {
|
||||
onEditPrevious?.()
|
||||
}
|
||||
}
|
||||
|
||||
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||
|
||||
const submit = async () => {
|
||||
@@ -55,23 +38,12 @@
|
||||
}
|
||||
|
||||
const editor = makeEditor({
|
||||
content,
|
||||
autofocus,
|
||||
submit,
|
||||
uploading,
|
||||
aggressive: true,
|
||||
encryptFiles: true,
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
const ed = await editor
|
||||
ed.view.dom.addEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
|
||||
onDestroy(async () => {
|
||||
const ed = await editor
|
||||
ed?.view?.dom.removeEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {slide} from "@lib/transition"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
|
||||
const {
|
||||
clear,
|
||||
}: {
|
||||
clear: () => void
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative flex h-8 items-center justify-between border-l-2 border-solid border-primary bg-base-300 px-2 pr-7 text-xs"
|
||||
transition:slide>
|
||||
<p class="text-primary">Editing message</p>
|
||||
<Button onclick={clear} class="flex items-center">
|
||||
<Icon icon={CloseCircle} />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -23,13 +23,11 @@
|
||||
interface Props {
|
||||
event: TrustedEvent
|
||||
replyTo: (event: TrustedEvent) => void
|
||||
canEdit?: (event: TrustedEvent) => boolean
|
||||
onEdit?: (event: TrustedEvent) => void
|
||||
pubkeys: string[]
|
||||
showPubkey?: boolean
|
||||
}
|
||||
|
||||
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
|
||||
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
||||
|
||||
const isOwn = event.pubkey === $pubkey
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||
@@ -37,7 +35,6 @@
|
||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||
|
||||
const reply = () => replyTo(event)
|
||||
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
|
||||
|
||||
const deleteReaction = (event: TrustedEvent) =>
|
||||
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys})
|
||||
@@ -47,7 +44,7 @@
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||
|
||||
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply, edit})
|
||||
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply})
|
||||
|
||||
const togglePopover = () => {
|
||||
if (popoverIsVisible) {
|
||||
@@ -74,7 +71,7 @@
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={ChatMessageMenu}
|
||||
props={{event, pubkeys, popover, replyTo, edit}}
|
||||
props={{event, pubkeys, popover, replyTo}}
|
||||
params={{
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
@@ -96,7 +93,7 @@
|
||||
{/if}
|
||||
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
||||
<TapTarget
|
||||
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px]"
|
||||
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
||||
onTap={showMobileMenu}>
|
||||
{#if showPubkey}
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
|
||||
const {event, pubkeys, popover, replyTo, edit} = $props()
|
||||
const {event, pubkeys, popover, replyTo} = $props()
|
||||
|
||||
const reply = () => replyTo(event)
|
||||
const onEdit = () => edit?.()
|
||||
|
||||
const showInfo = () => {
|
||||
popover.hide()
|
||||
@@ -26,11 +24,6 @@
|
||||
<Icon size={4} icon={Reply} />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if edit}
|
||||
<Button class="btn join-item btn-xs" onclick={onEdit}>
|
||||
<Icon size={4} icon={Pen} />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button class="btn join-item btn-xs" onclick={showInfo}>
|
||||
<Icon size={4} icon={Code2} />
|
||||
</Button>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {sendWrapped} from "@welshman/app"
|
||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
@@ -21,10 +20,9 @@
|
||||
pubkeys: string[]
|
||||
event: TrustedEvent
|
||||
reply: () => void
|
||||
edit?: () => void
|
||||
}
|
||||
|
||||
const {event, pubkeys, reply, edit}: Props = $props()
|
||||
const {event, pubkeys, reply}: Props = $props()
|
||||
|
||||
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
||||
history.back()
|
||||
@@ -41,11 +39,6 @@
|
||||
reply()
|
||||
}
|
||||
|
||||
const sendEdit = () => {
|
||||
history.back()
|
||||
edit?.()
|
||||
}
|
||||
|
||||
const copyText = () => {
|
||||
history.back()
|
||||
clip(event.content)
|
||||
@@ -69,12 +62,6 @@
|
||||
<Icon size={4} icon={Reply} />
|
||||
Send Reply
|
||||
</Button>
|
||||
{#if edit}
|
||||
<Button class="btn btn-neutral w-full" onclick={sendEdit}>
|
||||
<Icon size={4} icon={Pen} />
|
||||
Edit Message
|
||||
</Button>
|
||||
{/if}
|
||||
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
||||
<Icon size={4} icon={SmileCircle} />
|
||||
Send Reaction
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import IconPicker from "@app/components/IconPicker.svelte"
|
||||
|
||||
type Props = {
|
||||
onSelect: (iconUrl: string) => void
|
||||
}
|
||||
|
||||
const {onSelect}: Props = $props()
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
<ModalBody>
|
||||
<IconPicker {onSelect} />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
@@ -82,7 +82,7 @@
|
||||
<p>Your recovery codes have been sent!</p>
|
||||
<p>
|
||||
For security reasons, you may receive three or more emails with recovery codes in them. Please
|
||||
paste <strong>all</strong> recovery codes into the text box below.
|
||||
paste <strong>all</strong> recovery codes into the text box below, on separate lines.
|
||||
</p>
|
||||
<StringMultiInput bind:value={otps} placeholder="Enter your recovery codes..." />
|
||||
</ModalBody>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<p>Your login codes have been sent!</p>
|
||||
<p>
|
||||
For security reasons, you may receive three or more emails with login codes in them. Please
|
||||
paste <strong>all</strong> login codes into the text box below.
|
||||
paste <strong>all</strong> login codes into the text box below, on separate lines.
|
||||
</p>
|
||||
<StringMultiInput bind:value={otps} placeholder="Enter your login codes..." />
|
||||
</ModalBody>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import Server from "@assets/icons/server.svg?dataurl"
|
||||
import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl"
|
||||
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
|
||||
import Moon from "@assets/icons/moon.svg?dataurl"
|
||||
import Settings from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import Wallet from "@assets/icons/wallet.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
@@ -11,69 +14,120 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import Profile from "@app/components/Profile.svelte"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import LogOut from "@app/components/LogOut.svelte"
|
||||
import {PLATFORM_NAME} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {theme} from "@app/util/theme"
|
||||
|
||||
const back = () => history.back()
|
||||
const logout = () => pushModal(LogOut)
|
||||
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
<ModalBody>
|
||||
<div class="flex flex-col gap-8 items-center py-12 max-w-[16rem] m-auto w-full">
|
||||
{#if $pubkey}
|
||||
<Link replaceState href="/settings/profile">
|
||||
<Profile inert pubkey={$pubkey} />
|
||||
<div class="flex flex-col gap-2">
|
||||
<Link replaceState href="/settings/profile">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={UserRounded} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Profile</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Customize your user profile</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Link replaceState href="/settings/alerts">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Bell} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Alerts</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Set up email digests and push notifications</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
{#if Capacitor.getPlatform() !== "ios"}
|
||||
<Link replaceState href="/settings/wallet">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Wallet} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Wallet</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Connect a bitcoin wallet for sending social tips</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 gap-3 w-full">
|
||||
<Link
|
||||
replaceState
|
||||
href="/settings/alerts"
|
||||
class="aspect-square btn h-[unset] btn-neutral flex flex-col gap-4 text-center">
|
||||
<Icon icon={Bell} size={7} />
|
||||
Alerts
|
||||
</Link>
|
||||
{#if Capacitor.getPlatform() !== "ios"}
|
||||
<Link
|
||||
replaceState
|
||||
href="/settings/wallet"
|
||||
class="aspect-square btn h-[unset] btn-neutral flex flex-col gap-4 text-center">
|
||||
<Icon icon={Wallet} size={7} />
|
||||
Wallet
|
||||
</Link>
|
||||
{/if}
|
||||
<Link
|
||||
replaceState
|
||||
href="/settings/relays"
|
||||
class="aspect-square btn h-[unset] btn-neutral flex flex-col gap-4 text-center">
|
||||
<Icon icon={Server} size={7} />
|
||||
Relays
|
||||
</Link>
|
||||
<Link
|
||||
replaceState
|
||||
href="/settings/content"
|
||||
class="aspect-square btn h-[unset] btn-neutral flex flex-col gap-4 text-center">
|
||||
<Icon icon={GalleryMinimalistic} size={7} />
|
||||
Content
|
||||
</Link>
|
||||
<Link
|
||||
replaceState
|
||||
href="/settings/privacy"
|
||||
class="aspect-square btn h-[unset] btn-neutral flex flex-col gap-4 text-center">
|
||||
<Icon icon={Shield} size={7} />
|
||||
Privacy
|
||||
</Link>
|
||||
</div>
|
||||
<div class="flex gap-3 items-center opacity-75 text-sm">
|
||||
<Button onclick={toggleTheme}>Theme</Button>
|
||||
/
|
||||
<Link replaceState href="/settings/about">About</Link>
|
||||
/
|
||||
<Button onclick={logout}>Log Out</Button>
|
||||
</div>
|
||||
<Link replaceState href="/settings/relays">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Server} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Relays</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Control how {PLATFORM_NAME} talks to the network</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Link replaceState href="/settings/content">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Settings} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Settings</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Get into the details about how {PLATFORM_NAME} works</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Button onclick={toggleTheme}>
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Moon} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>Theme</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Switch between light and dark mode</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Button>
|
||||
<Link replaceState href="/settings/about">
|
||||
<CardButton class="btn-neutral">
|
||||
{#snippet icon()}
|
||||
<div><Icon icon={Code2} size={7} /></div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div>About</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Learn about {PLATFORM_NAME} and support the developer</div>
|
||||
{/snippet}
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Button onclick={logout} class="btn btn-neutral">
|
||||
<Icon icon={Exit} /> Log Out
|
||||
</Button>
|
||||
<Button class="btn btn-link w-full md:hidden" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
|
||||
@@ -2,46 +2,38 @@
|
||||
import {onMount, mount, unmount} from "svelte"
|
||||
import Drawer from "@lib/components/Drawer.svelte"
|
||||
import Dialog from "@lib/components/Dialog.svelte"
|
||||
import {modal, modalStack, popModal} from "@app/util/modal"
|
||||
import {modal, clearModals} from "@app/util/modal"
|
||||
|
||||
const closeModal = () => {
|
||||
const closeModals = () => {
|
||||
if ($modal && !$modal.options.noEscape) {
|
||||
popModal()
|
||||
clearModals()
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyDown = (e: any) => {
|
||||
if (e.code === "Escape" && e.target === document.body) {
|
||||
closeModal()
|
||||
closeModals()
|
||||
}
|
||||
}
|
||||
|
||||
let element: HTMLElement
|
||||
const instances: Record<string, any> = {}
|
||||
let instance: any | undefined
|
||||
|
||||
onMount(() => {
|
||||
return modalStack.subscribe($modalStack => {
|
||||
const ids = $modalStack.map(({id}) => id)
|
||||
|
||||
for (const [id, instance] of Object.entries(instances)) {
|
||||
if (!ids.includes(id)) {
|
||||
unmount(instance, {outro: true})
|
||||
delete instances[id]
|
||||
}
|
||||
return modal.subscribe($modal => {
|
||||
if (instance) {
|
||||
unmount(instance, {outro: true})
|
||||
instance = undefined
|
||||
}
|
||||
|
||||
for (const item of $modalStack) {
|
||||
if (instances[item.id]) {
|
||||
continue
|
||||
}
|
||||
|
||||
const {options, component, props} = item
|
||||
if ($modal) {
|
||||
const {options, component, props} = $modal
|
||||
const wrapper = options.drawer ? Drawer : Dialog
|
||||
|
||||
instances[item.id] = mount(wrapper as any, {
|
||||
instance = mount(wrapper as any, {
|
||||
target: element,
|
||||
props: {
|
||||
onClose: closeModal,
|
||||
onClose: closeModals,
|
||||
fullscreen: options.fullscreen,
|
||||
children: {component, props},
|
||||
},
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<p>Let's start by confirming your email.</p>
|
||||
<p>
|
||||
For security reasons, you may receive three or more emails with confirmation codes in them.
|
||||
Please paste <strong>all</strong> confirmation codes into the text box below.
|
||||
Please paste <strong>all</strong> confirmation codes into the text box below, on separate lines.
|
||||
</p>
|
||||
<StringMultiInput bind:value={otps} placeholder="Enter your confirmation codes..." />
|
||||
</ModalBody>
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {splitAt} from "@welshman/lib"
|
||||
import {userProfile} from "@welshman/app"
|
||||
import Widget from "@assets/icons/widget.svg?dataurl"
|
||||
import Compass from "@assets/icons/compass.svg?dataurl"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
||||
import Planet from "@assets/icons/planet-3.svg?dataurl"
|
||||
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import MenuSettings from "@app/components/MenuSettings.svelte"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
import PrimaryNavSpaces from "@app/components/PrimaryNavSpaces.svelte"
|
||||
import {userSpaceUrls, PLATFORM_RELAYS} from "@app/core/state"
|
||||
import MenuSettings from "@app/components/MenuSettings.svelte"
|
||||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||
import {userSpaceUrls, PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import {goToLastChat} from "@app/util/routes"
|
||||
@@ -25,13 +28,44 @@
|
||||
|
||||
const showSettingsMenu = () => pushModal(MenuSettings)
|
||||
|
||||
let windowHeight = $state(0)
|
||||
|
||||
const itemHeight = 56
|
||||
const navPadding = 8 * itemHeight
|
||||
const itemLimit = $derived((windowHeight - navPadding) / itemHeight)
|
||||
const [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, $userSpaceUrls))
|
||||
const anySpaceNotifications = $derived($userSpaceUrls.some(p => $notifications.has(p)))
|
||||
const otherSpaceNotifications = $derived(secondarySpaceUrls.some(p => $notifications.has(p)))
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight={windowHeight} />
|
||||
|
||||
<div
|
||||
class="ml-sai mt-sai mb-sai relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block">
|
||||
<div class="flex h-full flex-col" class:justify-between={PLATFORM_RELAYS.length === 0}>
|
||||
<PrimaryNavSpaces />
|
||||
<div>
|
||||
{#each PLATFORM_RELAYS as url (url)}
|
||||
<PrimaryNavItemSpace {url} />
|
||||
{:else}
|
||||
<PrimaryNavItem title="Home" href="/home" class="tooltip-right">
|
||||
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" size={10} />
|
||||
</PrimaryNavItem>
|
||||
<Divider />
|
||||
{#each primarySpaceUrls as url (url)}
|
||||
<PrimaryNavItemSpace {url} />
|
||||
{/each}
|
||||
<PrimaryNavItem
|
||||
href="/spaces"
|
||||
title="All Spaces"
|
||||
class="tooltip-right"
|
||||
notification={otherSpaceNotifications}>
|
||||
<ImageIcon alt="All Spaces" src={Widget} size={8} />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
|
||||
<ImageIcon alt="Add a Space" src={Compass} size={8} />
|
||||
</PrimaryNavItem>
|
||||
{/each}
|
||||
</div>
|
||||
{#if PLATFORM_RELAYS.length > 0}
|
||||
<Divider />
|
||||
{/if}
|
||||
@@ -82,7 +116,7 @@
|
||||
</PrimaryNavItem>
|
||||
{#if PLATFORM_RELAYS.length !== 1}
|
||||
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
|
||||
<ImageIcon alt="Spaces" src={Planet} size={8} />
|
||||
<ImageIcon alt="Spaces" src={SettingsMinimalistic} size={8} />
|
||||
</PrimaryNavItem>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {splitAt} from "@welshman/lib"
|
||||
import Widget from "@assets/icons/widget.svg?dataurl"
|
||||
import Compass from "@assets/icons/compass.svg?dataurl"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||
import {userSpaceUrls, PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/core/state"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
|
||||
let windowHeight = $state(0)
|
||||
|
||||
const itemHeight = 56
|
||||
const navPadding = 8 * itemHeight
|
||||
const itemLimit = $derived((windowHeight - navPadding) / itemHeight)
|
||||
const [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, $userSpaceUrls))
|
||||
const otherSpaceNotifications = $derived(secondarySpaceUrls.some(p => $notifications.has(p)))
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight={windowHeight} />
|
||||
|
||||
<div>
|
||||
{#each PLATFORM_RELAYS as url (url)}
|
||||
<PrimaryNavItemSpace {url} />
|
||||
{:else}
|
||||
<PrimaryNavItem title="Home" href="/home" class="tooltip-right">
|
||||
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" size={10} />
|
||||
</PrimaryNavItem>
|
||||
<Divider />
|
||||
{#each primarySpaceUrls as url (url)}
|
||||
<PrimaryNavItemSpace {url} />
|
||||
{/each}
|
||||
<PrimaryNavItem
|
||||
href="/spaces"
|
||||
title="All Spaces"
|
||||
class="tooltip-right"
|
||||
notification={otherSpaceNotifications}>
|
||||
<ImageIcon alt="All Spaces" src={Widget} size={8} />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
|
||||
<ImageIcon alt="Add a Space" src={Compass} size={8} />
|
||||
</PrimaryNavItem>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -17,20 +17,15 @@
|
||||
url?: string
|
||||
showPubkey?: boolean
|
||||
avatarSize?: number
|
||||
inert?: boolean
|
||||
}
|
||||
|
||||
const {pubkey, url, showPubkey, inert, avatarSize = 10}: Props = $props()
|
||||
const {pubkey, url, showPubkey, avatarSize = 10}: Props = $props()
|
||||
|
||||
const relays = removeUndefined([url])
|
||||
const profileDisplay = deriveProfileDisplay(pubkey, relays)
|
||||
const handle = deriveHandleForPubkey(pubkey)
|
||||
|
||||
const openProfile = () => {
|
||||
if (!inert) {
|
||||
pushModal(ProfileDetail, {pubkey, url})
|
||||
}
|
||||
}
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||
|
||||
const copyPubkey = () => clip(nip19.npubEncode(pubkey))
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
|
||||
type Props = {
|
||||
pubkey?: string
|
||||
pubkey: string
|
||||
class?: string
|
||||
size?: number
|
||||
url?: string
|
||||
|
||||
@@ -1,70 +1,30 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {SvelteSet} from "svelte/reactivity"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {tryCatch} from "@welshman/lib"
|
||||
import {isShareableRelayUrl, isIPAddress, normalizeRelayUrl} from "@welshman/util"
|
||||
import type {Thunk} from "@welshman/app"
|
||||
import {waitForThunkError, relaySearch} from "@welshman/app"
|
||||
import {isShareableRelayUrl, normalizeRelayUrl} from "@welshman/util"
|
||||
import {relaySearch} from "@welshman/app"
|
||||
import {createScroller} from "@lib/html"
|
||||
import {errorMessage} from "@lib/util"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import RelayItem from "@app/components/RelayItem.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
interface Props {
|
||||
relays: Readable<string[]>
|
||||
addRelay: (url: string) => Promise<Thunk>
|
||||
matchRelay?: (url: string) => boolean
|
||||
addRelay: (url: string) => void
|
||||
}
|
||||
|
||||
const {relays, addRelay, matchRelay}: Props = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const customUrl = $derived(tryCatch(() => normalizeRelayUrl(term)))
|
||||
|
||||
const add = async (url: string) => {
|
||||
loading.add(url)
|
||||
|
||||
try {
|
||||
const error = await waitForThunkError(await addRelay(url))
|
||||
|
||||
if (error) {
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: `Failed to add relay: ${errorMessage(error)}`,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loading.delete(url)
|
||||
}
|
||||
}
|
||||
const {relays, addRelay}: Props = $props()
|
||||
|
||||
let term = $state("")
|
||||
let limit = $state(20)
|
||||
let element: Element | undefined = $state()
|
||||
|
||||
const loading = $state(new SvelteSet<string>())
|
||||
|
||||
const searchResults = $derived(
|
||||
$relaySearch
|
||||
.searchValues(term)
|
||||
.filter(url => {
|
||||
if (matchRelay?.(url) === false) return false
|
||||
if ($relays.includes(url)) return false
|
||||
if (isIPAddress(url)) return false
|
||||
|
||||
return true
|
||||
})
|
||||
.slice(0, limit),
|
||||
)
|
||||
const customUrl = $derived(tryCatch(() => normalizeRelayUrl(term)))
|
||||
|
||||
onMount(() => {
|
||||
const scroller = createScroller({
|
||||
@@ -92,35 +52,23 @@
|
||||
<RelayItem url={term}>
|
||||
<Button
|
||||
class="btn btn-outline btn-sm flex items-center"
|
||||
disabled={loading.has(customUrl)}
|
||||
onclick={() => add(customUrl)}>
|
||||
{#if loading.has(customUrl)}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<Icon icon={AddCircle} />
|
||||
{/if}
|
||||
onclick={() => addRelay(customUrl)}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add Relay
|
||||
</Button>
|
||||
</RelayItem>
|
||||
{/if}
|
||||
{#each searchResults as url (url)}
|
||||
{#each $relaySearch
|
||||
.searchValues(term)
|
||||
.filter(url => !$relays.includes(url))
|
||||
.slice(0, limit) as url (url)}
|
||||
<RelayItem {url}>
|
||||
<Button
|
||||
class="btn btn-outline btn-sm flex items-center"
|
||||
disabled={loading.has(url)}
|
||||
onclick={() => add(url)}>
|
||||
{#if loading.has(url)}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<Icon icon={AddCircle} />
|
||||
{/if}
|
||||
<Button class="btn btn-outline btn-sm flex items-center" onclick={() => addRelay(url)}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add Relay
|
||||
</Button>
|
||||
</RelayItem>
|
||||
{/each}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-primary flex-grow" onclick={back}>Done</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type {Readable} from "svelte/store"
|
||||
import {SvelteSet} from "svelte/reactivity"
|
||||
import {waitForThunkError} from "@welshman/app"
|
||||
import type {Thunk} from "@welshman/app"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import {errorMessage} from "@lib/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import RelayAdd from "@app/components/RelayAdd.svelte"
|
||||
import RelayItem from "@app/components/RelayItem.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
subtitle: string
|
||||
relays: Readable<string[]>
|
||||
addRelay: (url: string) => Promise<Thunk>
|
||||
removeRelay: (url: string) => Promise<Thunk>
|
||||
matchRelay?: (url: string) => boolean
|
||||
}
|
||||
|
||||
const {title, subtitle, relays, addRelay, removeRelay, matchRelay}: Props = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const add = () => pushModal(RelayAdd, {relays, addRelay, matchRelay})
|
||||
|
||||
const remove = async (url: string) => {
|
||||
loading.add(url)
|
||||
|
||||
try {
|
||||
const error = await waitForThunkError(await removeRelay(url))
|
||||
|
||||
if (error) {
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: `Failed to remove relay: ${errorMessage(error)}`,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loading.delete(url)
|
||||
}
|
||||
}
|
||||
|
||||
const loading = $state(new SvelteSet<string>())
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
<ModalBody>
|
||||
<h2 class="text-xl">{title}</h2>
|
||||
<p class="text-sm">{subtitle}</p>
|
||||
{#each $relays.toSorted() as url (url)}
|
||||
<RelayItem {url}>
|
||||
<Button
|
||||
class="btn btn-sm btn-neutral"
|
||||
disabled={loading.has(url)}
|
||||
onclick={() => remove(url)}>
|
||||
{#if loading.has(url)}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<Icon icon={CloseCircle} />
|
||||
{/if}
|
||||
Remove
|
||||
</Button>
|
||||
</RelayItem>
|
||||
{/each}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button class="btn btn-primary" onclick={add}>
|
||||
Add Relays
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
@@ -1,27 +0,0 @@
|
||||
<script lang="ts" module>
|
||||
export type ActionItem = {
|
||||
title: string
|
||||
subtitle: string
|
||||
action: string
|
||||
apply: () => unknown
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Stars from "@assets/icons/stars.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
|
||||
const {title, action, subtitle, apply}: ActionItem = $props()
|
||||
</script>
|
||||
|
||||
<div class="card2 card2-sm bg-alt flex justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<strong>{title}</strong>
|
||||
<p class="text-sm">{subtitle}</p>
|
||||
</div>
|
||||
<Button class="btn btn-neutral btn-sm" onclick={apply}>
|
||||
<Icon icon={Stars} />
|
||||
{action}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1,58 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type {Readable} from "svelte/store"
|
||||
import Stars from "@assets/icons/stars.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import type {ActionItem} from "@app/components/RelaySettingsActionItem.svelte"
|
||||
import RelaySettingsActionItem from "@app/components/RelaySettingsActionItem.svelte"
|
||||
|
||||
interface Props {
|
||||
actionItems: Readable<ActionItem[]>
|
||||
}
|
||||
|
||||
const {actionItems}: Props = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const applyAll = () => {
|
||||
for (const actionItem of $actionItems) {
|
||||
actionItem.apply()
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if ($actionItems.length === 0) {
|
||||
back()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
<ModalBody>
|
||||
<div class="flex gap-2 items-center">
|
||||
<Icon icon={DangerTriangle} />
|
||||
<strong class="text-lg">Action Items</strong>
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
Below are recommendations for adjustments to your relay selections that you might consider.
|
||||
</p>
|
||||
{#each $actionItems as actionItem}
|
||||
<RelaySettingsActionItem {...actionItem} />
|
||||
{/each}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go Back
|
||||
</Button>
|
||||
<Button class="btn btn-primary" onclick={applyAll}>
|
||||
<Icon icon={Stars} />
|
||||
Apply All Recommendations
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
@@ -1,50 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type {Readable} from "svelte/store"
|
||||
import Check from "@assets/icons/check.svg?dataurl"
|
||||
import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import RelayList from "@app/components/RelayList.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
interface Props {
|
||||
icon: string
|
||||
title: string
|
||||
subtitle: string
|
||||
relays: Readable<string[]>
|
||||
addRelay: (url: string) => unknown
|
||||
removeRelay: (url: string) => unknown
|
||||
matchRelay?: (url: string) => boolean
|
||||
}
|
||||
|
||||
const {icon, title, relays, subtitle, addRelay, removeRelay, matchRelay}: Props = $props()
|
||||
|
||||
const onclick = () =>
|
||||
pushModal(RelayList, {title, subtitle, relays, addRelay, removeRelay, matchRelay})
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn font-normal flex h-[unset] w-full flex-nowrap py-4 text-left items-start justify-between"
|
||||
{onclick}>
|
||||
<div class="flex flex-grow flex-row items-start gap-4">
|
||||
<div class="flex h-7 w-7 flex-shrink-0 items-center justify-center">
|
||||
<Icon {icon} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-lg">
|
||||
{title}
|
||||
</p>
|
||||
<p class="text-sm">
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
{#if $relays.length <= 1}
|
||||
<Icon icon={DangerTriangle} />
|
||||
{:else}
|
||||
<Icon icon={Check} />
|
||||
{/if}
|
||||
{$relays.length}
|
||||
</div>
|
||||
</button>
|
||||
@@ -16,7 +16,7 @@
|
||||
import Lock from "@assets/icons/lock.svg?dataurl"
|
||||
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -255,7 +255,7 @@
|
||||
<strong class="text-lg">Room Settings</strong>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={Bell} />
|
||||
<Icon icon={VolumeLoud} />
|
||||
<span>Notifications</span>
|
||||
</div>
|
||||
<input
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
|
||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.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 {preventDefault} from "@lib/html"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
@@ -16,7 +15,6 @@
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {uploadFile} from "@app/core/commands"
|
||||
import {deriveHasLivekit, getRoomType, RoomType} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
@@ -29,25 +27,12 @@
|
||||
const {url, header, footer, onsubmit, initialValues = makeRoomMeta()}: Props = $props()
|
||||
|
||||
const values = $state(initialValues)
|
||||
const relayHasLivekit = deriveHasLivekit(url)
|
||||
|
||||
const submit = async () => {
|
||||
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) {
|
||||
const {error, result} = await uploadFile(imageFile, {
|
||||
maxWidth: 256,
|
||||
maxHeight: 256,
|
||||
})
|
||||
const {error, result} = await uploadFile(imageFile, {maxWidth: 256, maxHeight: 256})
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
@@ -91,7 +76,6 @@
|
||||
let loading = $state(false)
|
||||
let imageFile = $state<File | undefined>()
|
||||
let imagePreview = $state(initialValues.picture)
|
||||
let roomType = $state(getRoomType(initialValues))
|
||||
|
||||
const handleImageUpload = async (event: Event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
@@ -161,7 +145,7 @@
|
||||
{#if imagePreview}
|
||||
<ImageIcon src={imagePreview} alt="" class="rounded-lg" />
|
||||
{:else}
|
||||
<Icon icon={roomType === RoomType.Voice ? Volume : Hashtag} />
|
||||
<Icon icon={Hashtag} />
|
||||
{/if}
|
||||
<input bind:value={values.name} class="grow" type="text" />
|
||||
</label>
|
||||
@@ -177,22 +161,6 @@
|
||||
</label>
|
||||
{/snippet}
|
||||
</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>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
|
||||
|
||||
@@ -1,41 +1,22 @@
|
||||
<script lang="ts">
|
||||
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 ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import {deriveRoom} from "@app/core/state"
|
||||
import {currentVoiceSession} from "@app/voice"
|
||||
|
||||
interface Props {
|
||||
h: string
|
||||
url: string
|
||||
size?: number
|
||||
fallbackIcon?: string
|
||||
}
|
||||
|
||||
const {url, h, size = 5, fallbackIcon = Hashtag}: Props = $props()
|
||||
const {url, h, size = 5}: Props = $props()
|
||||
|
||||
const room = deriveRoom(url, h)
|
||||
const isVoiceRoom = $derived($room.livekit)
|
||||
const isVoiceRoomActive = $derived(
|
||||
$currentVoiceSession?.url === url && $currentVoiceSession?.h === h,
|
||||
)
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
{#if $room.picture}
|
||||
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
||||
{:else}
|
||||
<Icon icon={fallbackIcon} {size} />
|
||||
<Icon icon={Hashtag} {size} />
|
||||
{/if}
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
const room = deriveRoom(url, h)
|
||||
</script>
|
||||
|
||||
<span class="ellipsize min-w-0 {props.class}">
|
||||
<span class="ellipsize {props.class}">
|
||||
{$room?.name || h}
|
||||
</span>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {goto} from "$app/navigation"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import {decodeRelay} from "@app/core/state"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
|
||||
interface Props {
|
||||
back?: () => unknown
|
||||
title?: Snippet
|
||||
action?: Snippet
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const {back = () => goto(makeSpacePath(url)), title, action, ...props}: Props = $props()
|
||||
|
||||
const url = decodeRelay($page.params.relay!)
|
||||
</script>
|
||||
|
||||
<PageBar {...props}>
|
||||
<div class="flex">
|
||||
<Button onclick={back} class="place-self-start pr-3 md:hidden">
|
||||
<Icon icon={ArrowLeft} size={7} />
|
||||
</Button>
|
||||
<div class="ellipsize whitespace-nowrap flex flex-grow items-center justify-between gap-4">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex gap-2 items-center">
|
||||
{@render title?.()}
|
||||
</div>
|
||||
<div class="text-xs text-primary md:hidden">
|
||||
{displayRelayUrl(url)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-start">
|
||||
{@render action?.()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageBar>
|
||||
@@ -3,7 +3,7 @@
|
||||
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
||||
import {manageRelay, forceLoadRelay} from "@welshman/app"
|
||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||
import Planet from "@assets/icons/planet-3.svg?dataurl"
|
||||
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
||||
import {preventDefault} from "@lib/html"
|
||||
@@ -164,7 +164,7 @@
|
||||
{#if imagePreview}
|
||||
<ImageIcon src={imagePreview} alt="" />
|
||||
{:else}
|
||||
<Icon icon={Planet} />
|
||||
<Icon icon={SettingsMinimalistic} />
|
||||
{/if}
|
||||
<input bind:value={values.name} class="grow" type="text" />
|
||||
</label>
|
||||
|
||||
@@ -159,11 +159,9 @@
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
{#if $userIsAdmin}
|
||||
<Button class="btn btn-primary" onclick={addMember}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add members
|
||||
</Button>
|
||||
{/if}
|
||||
<Button class="btn btn-primary" onclick={addMember}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add members
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
import CaseMinimalistic from "@assets/icons/case-minimalistic.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import BellOff from "@assets/icons/bell-off.svg?dataurl"
|
||||
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
|
||||
import VolumeCross from "@assets/icons/volume-cross.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -38,7 +38,6 @@
|
||||
import SpaceReports from "@app/components/SpaceReports.svelte"
|
||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
|
||||
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
|
||||
import {
|
||||
ENABLE_ZAPS,
|
||||
@@ -46,7 +45,6 @@
|
||||
deriveSpaceMembers,
|
||||
deriveUserRooms,
|
||||
deriveOtherRooms,
|
||||
deriveOtherVoiceRooms,
|
||||
userSpaceUrls,
|
||||
hasNip29,
|
||||
deriveUserCanCreateRoom,
|
||||
@@ -70,7 +68,6 @@
|
||||
const calendarPath = makeSpacePath(url, "calendar")
|
||||
const userRooms = deriveUserRooms(url)
|
||||
const otherRooms = deriveOtherRooms(url)
|
||||
const otherVoiceRooms = deriveOtherVoiceRooms(url)
|
||||
const members = deriveSpaceMembers(url)
|
||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||
const reports = deriveEventsForUrl(url, [{kinds: [REPORT]}])
|
||||
@@ -136,9 +133,9 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={element} class="flex min-h-0 flex-1 flex-col">
|
||||
<SecondaryNavSection class="min-h-0 flex-1 flex flex-col overflow-hidden pb-0">
|
||||
<div class="flex-shrink-0">
|
||||
<div bind:this={element} class="flex h-full flex-col justify-between">
|
||||
<SecondaryNavSection class="pb-0">
|
||||
<div>
|
||||
<Button
|
||||
class="flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
||||
onclick={openMenu}>
|
||||
@@ -146,7 +143,7 @@
|
||||
<strong class="ellipsize flex items-center gap-1">
|
||||
<RelayName {url} />
|
||||
{#if $notificationSettings.push && !$shouldNotify}
|
||||
<Icon icon={BellOff} size={3} class="opacity-50" />
|
||||
<Icon icon={VolumeCross} size={3} class="opacity-50" />
|
||||
{/if}
|
||||
</strong>
|
||||
<Icon icon={AltArrowDown} />
|
||||
@@ -195,12 +192,12 @@
|
||||
<li>
|
||||
{#if $notificationSettings.push}
|
||||
<Button onclick={toggleSpaceNotifications}>
|
||||
<Icon icon={$shouldNotify ? Bell : BellOff} />
|
||||
<Icon icon={$shouldNotify ? VolumeLoud : VolumeCross} />
|
||||
{$shouldNotify ? "Turn off" : "Turn on"} notifications
|
||||
</Button>
|
||||
{:else}
|
||||
<Link href="/settings/alerts">
|
||||
<Icon icon={Bell} />
|
||||
<Icon icon={VolumeLoud} />
|
||||
Enable notifications
|
||||
</Link>
|
||||
{/if}
|
||||
@@ -222,7 +219,8 @@
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-1 overflow-auto overflow-x-hidden">
|
||||
<div
|
||||
class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto overflow-x-hidden">
|
||||
{#if hasNip29($relay)}
|
||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
|
||||
<Icon icon={History} /> Recent Activity
|
||||
@@ -254,14 +252,14 @@
|
||||
{/if}
|
||||
{#if hasNip29($relay)}
|
||||
{#if $userRooms.length > 0}
|
||||
<div class="h-2 flex-shrink-0"></div>
|
||||
<div class="h-2"></div>
|
||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||
{/if}
|
||||
{#each $userRooms as h (h)}
|
||||
{#each $userRooms as h, i (h)}
|
||||
<SpaceMenuRoomItem notify {replaceState} {url} {h} />
|
||||
{/each}
|
||||
{#if $otherRooms.length > 0}
|
||||
<div class="h-2 flex-shrink-0"></div>
|
||||
<div class="h-2"></div>
|
||||
<SecondaryNavHeader>
|
||||
{#if $userRooms.length > 0}
|
||||
Other Rooms
|
||||
@@ -276,16 +274,9 @@
|
||||
<input bind:value={term} onblur={clearTerm} class="grow" />
|
||||
</label>
|
||||
{/if}
|
||||
{#each $roomSearch.searchValues(term) as h (h)}
|
||||
{#each $roomSearch.searchValues(term) as h, i (h)}
|
||||
<SpaceMenuRoomItem {replaceState} {url} {h} />
|
||||
{/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}
|
||||
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
||||
<Icon icon={AddCircle} />
|
||||
@@ -293,12 +284,9 @@
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="h-5 flex-shrink-0"></div>
|
||||
</div>
|
||||
</SecondaryNavSection>
|
||||
<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 />
|
||||
<div class="flex flex-col gap-2 pb-2 p-4 pt-0">
|
||||
<Button class="btn btn-neutral btn-sm h-10" onclick={showDetail}>
|
||||
<SocketStatusIndicator {url} />
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import SpaceMenu from "@app/components/SpaceMenu.svelte"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {pushDrawer} from "@app/util/modal"
|
||||
import {deriveSocketStatus} from "@app/core/state"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const path = makeSpacePath(url) + ":mobile"
|
||||
|
||||
const status = deriveSocketStatus(url)
|
||||
|
||||
const openMenu = () => pushDrawer(SpaceMenu, {url})
|
||||
</script>
|
||||
|
||||
<Button onclick={openMenu} class="btn btn-neutral btn-sm relative md:hidden btn-square">
|
||||
<Icon icon={MenuDots} />
|
||||
{#if $status.theme !== "success"}
|
||||
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-{$status.theme}"></div>
|
||||
{:else if $notifications.has(path)}
|
||||
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-primary"></div>
|
||||
{/if}
|
||||
</Button>
|
||||
@@ -1,13 +1,12 @@
|
||||
<script lang="ts">
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import BellOff from "@assets/icons/bell-off.svg?dataurl"
|
||||
import VolumeCross from "@assets/icons/volume-cross.svg?dataurl"
|
||||
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.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 {makeRoomPath} from "@app/util/routes"
|
||||
import {deriveShouldNotify} from "@app/core/state"
|
||||
|
||||
interface Props {
|
||||
url: any
|
||||
@@ -18,24 +17,18 @@
|
||||
|
||||
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 shouldNotifyForSpace = deriveShouldNotify(url)
|
||||
const shouldNotifyForRoom = deriveShouldNotify(url, h)
|
||||
const showDifferenceIcon = $derived($shouldNotifyForRoom !== $shouldNotifyForSpace)
|
||||
</script>
|
||||
|
||||
{#if roomType === RoomType.Voice}
|
||||
<VoiceRoomItem {url} {h} {replaceState} />
|
||||
{:else}
|
||||
<SecondaryNavItem
|
||||
href={path}
|
||||
{replaceState}
|
||||
notification={notify ? $notifications.has(path) : false}>
|
||||
<RoomNameWithImage {url} {h} />
|
||||
{#if showDifferenceIcon}
|
||||
<Icon icon={$shouldNotifyForRoom ? Bell : BellOff} size={4} class="opacity-50" />
|
||||
{/if}
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
<SecondaryNavItem
|
||||
href={path}
|
||||
{replaceState}
|
||||
notification={notify ? $notifications.has(path) : false}>
|
||||
<RoomNameWithImage {url} {h} />
|
||||
{#if showDifferenceIcon}
|
||||
<Icon icon={$shouldNotifyForRoom ? VolumeLoud : VolumeCross} size={4} class="opacity-50" />
|
||||
{/if}
|
||||
</SecondaryNavItem>
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {Track} from "livekit-client"
|
||||
import {displayProfileByPubkey, loadProfile} from "@welshman/app"
|
||||
import Pin from "@assets/icons/pin.svg?dataurl"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import VideoCallVideo from "@app/components/VideoCallVideo.svelte"
|
||||
import {
|
||||
currentVoiceSession,
|
||||
currentVoiceRoom,
|
||||
videoCallContentActive,
|
||||
videoCallLayoutRevision,
|
||||
videoPrimaryTileKey,
|
||||
toggleVideoPrimaryTile,
|
||||
pubkeyFromLiveKitIdentity,
|
||||
} from "@app/voice"
|
||||
|
||||
type Variant = "mobile" | "desktop-split" | "desktop-full"
|
||||
|
||||
type Props = {
|
||||
variant: Variant
|
||||
url: string
|
||||
h: string
|
||||
visible?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
type Tile = {
|
||||
identity: string
|
||||
isLocal: boolean
|
||||
trackSid: string
|
||||
attachable: Track | undefined
|
||||
source: Track.Source.Camera | Track.Source.ScreenShare
|
||||
}
|
||||
|
||||
type TileLayout = "spotlight" | "default" | "strip"
|
||||
|
||||
const {variant, url, h, visible = true, class: className = ""}: Props = $props()
|
||||
|
||||
const roomMatches = $derived($currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h)
|
||||
|
||||
const allowEmptyPanel = $derived(variant === "desktop-split" || variant === "desktop-full")
|
||||
|
||||
const showPanel = $derived(
|
||||
visible &&
|
||||
roomMatches &&
|
||||
(variant === "mobile" ? $videoCallContentActive : $videoCallContentActive || allowEmptyPanel),
|
||||
)
|
||||
|
||||
const tiles = $derived.by(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- re-run when remote video subscribes
|
||||
$videoCallLayoutRevision
|
||||
const session = $currentVoiceSession
|
||||
if (!session || $currentVoiceRoom?.url !== url || $currentVoiceRoom?.h !== h) {
|
||||
return []
|
||||
}
|
||||
|
||||
const room = session.room
|
||||
const out: Tile[] = []
|
||||
const lp = room.localParticipant
|
||||
|
||||
if (session.cameraOn) {
|
||||
const localPub = lp.getTrackPublication(Track.Source.Camera)
|
||||
out.push({
|
||||
identity: lp.identity,
|
||||
isLocal: true,
|
||||
trackSid: localPub?.trackSid ?? "local-camera",
|
||||
attachable: localPub?.track,
|
||||
source: Track.Source.Camera,
|
||||
})
|
||||
}
|
||||
|
||||
if (session.screenShareOn) {
|
||||
const localPub = lp.getTrackPublication(Track.Source.ScreenShare)
|
||||
out.push({
|
||||
identity: lp.identity,
|
||||
isLocal: true,
|
||||
trackSid: localPub?.trackSid ?? "local-screen",
|
||||
attachable: localPub?.track,
|
||||
source: Track.Source.ScreenShare,
|
||||
})
|
||||
}
|
||||
|
||||
for (const rp of room.remoteParticipants.values()) {
|
||||
const camPub = rp.getTrackPublication(Track.Source.Camera)
|
||||
if (camPub?.isSubscribed && camPub.track) {
|
||||
out.push({
|
||||
identity: rp.identity,
|
||||
isLocal: false,
|
||||
trackSid: camPub.trackSid,
|
||||
attachable: camPub.track,
|
||||
source: Track.Source.Camera,
|
||||
})
|
||||
}
|
||||
const screenPub = rp.getTrackPublication(Track.Source.ScreenShare)
|
||||
if (screenPub?.isSubscribed && screenPub.track) {
|
||||
out.push({
|
||||
identity: rp.identity,
|
||||
isLocal: false,
|
||||
trackSid: screenPub.trackSid,
|
||||
attachable: screenPub.track,
|
||||
source: Track.Source.ScreenShare,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
})
|
||||
|
||||
/** Identity + source only — LiveKit can change trackSid after publish, which broke spotlight + stale-key effect. */
|
||||
const tileKey = (t: Tile) => `${t.identity}\x1f${t.source}`
|
||||
|
||||
const primaryTile = $derived.by(() => {
|
||||
const k = $videoPrimaryTileKey
|
||||
if (k === undefined) return undefined
|
||||
return tiles.find(t => tileKey(t) === k)
|
||||
})
|
||||
|
||||
const secondaryTiles = $derived.by(() => {
|
||||
const p = primaryTile
|
||||
if (p === undefined) return tiles
|
||||
const pk = tileKey(p)
|
||||
return tiles.filter(t => tileKey(t) !== pk)
|
||||
})
|
||||
|
||||
const useSpotlightLayout = $derived(primaryTile !== undefined)
|
||||
const useMultiGrid = $derived(!useSpotlightLayout && tiles.length > 2)
|
||||
|
||||
$effect(() => {
|
||||
const k = $videoPrimaryTileKey
|
||||
if (k === undefined) return
|
||||
if (!tiles.some(t => tileKey(t) === k)) {
|
||||
videoPrimaryTileKey.set(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
for (const t of tiles) {
|
||||
const pk = pubkeyFromLiveKitIdentity(t.identity)
|
||||
if (pk) loadProfile(pk)
|
||||
}
|
||||
})
|
||||
|
||||
const labelFor = (identity: string, source: Tile["source"]) => {
|
||||
const pk = pubkeyFromLiveKitIdentity(identity)
|
||||
const name = pk ? displayProfileByPubkey(pk) : "Unknown"
|
||||
return source === Track.Source.ScreenShare ? `${name} · screen` : name
|
||||
}
|
||||
|
||||
const showTileGrid = $derived(tiles.length > 0)
|
||||
|
||||
const spotlightHandlerFor = (key: string) => () => {
|
||||
toggleVideoPrimaryTile(key)
|
||||
}
|
||||
|
||||
const panelChrome = $derived(
|
||||
cx(
|
||||
variant === "mobile" &&
|
||||
"cb ct cw z-compose bg-base-300/95 fixed inset-x-0 flex min-h-0 flex-col gap-2 overflow-hidden p-2 md:hidden",
|
||||
variant === "desktop-split" &&
|
||||
"cb ct cw-split-video z-compose bg-base-300/95 fixed hidden min-h-0 flex-col gap-2 overflow-hidden p-2 md:flex",
|
||||
variant === "desktop-full" &&
|
||||
"cb ct cw z-compose bg-base-300/95 fixed hidden min-h-0 flex-col gap-2 overflow-hidden p-2 md:flex",
|
||||
className,
|
||||
),
|
||||
)
|
||||
</script>
|
||||
|
||||
{#snippet videoTile(tile: Tile, layout: TileLayout)}
|
||||
<div
|
||||
class={cx(
|
||||
"relative isolate overflow-hidden rounded-box shadow-sm",
|
||||
layout === "spotlight" && "min-h-0 flex-1",
|
||||
layout === "default" && "aspect-video w-full min-h-0",
|
||||
layout === "strip" && "aspect-video w-44 shrink-0",
|
||||
tile.source === Track.Source.ScreenShare ? "bg-black" : "bg-base-100",
|
||||
$videoPrimaryTileKey === tileKey(tile) &&
|
||||
"ring-2 ring-primary ring-offset-2 ring-offset-base-300",
|
||||
)}>
|
||||
{#if tile.attachable}
|
||||
<VideoCallVideo
|
||||
track={tile.attachable}
|
||||
muted={tile.isLocal}
|
||||
fit={tile.source === Track.Source.ScreenShare ? "contain" : "cover"}
|
||||
class="pointer-events-none absolute inset-0" />
|
||||
{:else}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<ProfileCircle pubkey={pubkeyFromLiveKitIdentity(tile.identity)} {url} size={14} />
|
||||
</div>
|
||||
{/if}
|
||||
<span
|
||||
class="pointer-events-none absolute bottom-1 left-1 max-w-[calc(100%-0.5rem)] truncate rounded bg-base-100/80 px-1.5 py-0.5 text-xs">
|
||||
{labelFor(tile.identity, tile.source)}{tile.isLocal ? " (you)" : ""}
|
||||
</span>
|
||||
{#if tiles.length > 1}
|
||||
<Button
|
||||
data-tip={$videoPrimaryTileKey === tileKey(tile) ? "Exit spotlight" : "Spotlight"}
|
||||
class="absolute right-1 top-1 z-20 btn btn-xs btn-circle btn-ghost bg-base-100/70"
|
||||
onclick={spotlightHandlerFor(tileKey(tile))}>
|
||||
<Icon icon={Pin} size={3} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#if showPanel && (showTileGrid || allowEmptyPanel)}
|
||||
<div class={panelChrome}>
|
||||
{#if showTileGrid}
|
||||
{#if useSpotlightLayout && primaryTile}
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-2 overflow-hidden">
|
||||
{@render videoTile(primaryTile, "spotlight")}
|
||||
{#if secondaryTiles.length > 0}
|
||||
<div
|
||||
class="flex max-h-40 shrink-0 flex-row gap-2 overflow-x-auto overflow-y-hidden py-0.5">
|
||||
{#each secondaryTiles as tile (tileKey(tile))}
|
||||
{@render videoTile(tile, "strip")}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if useMultiGrid}
|
||||
<div
|
||||
class="grid min-h-0 flex-1 grid-cols-1 content-start gap-2 overflow-y-auto sm:grid-cols-2">
|
||||
{#each tiles as tile (tileKey(tile))}
|
||||
{@render videoTile(tile, "default")}
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto">
|
||||
{#each tiles as tile (tileKey(tile))}
|
||||
{@render videoTile(tile, "default")}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
class="flex min-h-[12rem] flex-1 flex-col items-center justify-center gap-2 rounded-box bg-base-100/50 p-4 text-center text-sm opacity-80">
|
||||
<p>No camera or screen share yet.</p>
|
||||
<p class="text-xs">
|
||||
Use the camera or screen share control in the voice widget to share video.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,31 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type {Track} from "livekit-client"
|
||||
import cx from "classnames"
|
||||
|
||||
type Props = {
|
||||
track: Track
|
||||
muted?: boolean
|
||||
fit?: "cover" | "contain"
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {track, muted = true, fit = "cover", class: className = ""}: Props = $props()
|
||||
|
||||
let el = $state<HTMLVideoElement | undefined>()
|
||||
|
||||
$effect(() => {
|
||||
const v = el
|
||||
const t = track
|
||||
if (!v) return
|
||||
t.attach(v)
|
||||
return () => {
|
||||
t.detach(v)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<video
|
||||
bind:this={el}
|
||||
class={cx("h-full w-full", fit === "contain" ? "object-contain" : "object-cover", className)}
|
||||
playsinline
|
||||
{muted}></video>
|
||||
@@ -1,91 +0,0 @@
|
||||
<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>
|
||||
@@ -1,100 +0,0 @@
|
||||
<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 Videocamera from "@assets/icons/videocamera.svg?dataurl"
|
||||
import VideocameraRecord from "@assets/icons/videocamera-record.svg?dataurl"
|
||||
import Monitor from "@assets/icons/monitor.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,
|
||||
toggleCamera,
|
||||
toggleScreenShare,
|
||||
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={$currentVoiceSession.cameraOn ? "Turn off camera" : "Turn on camera"}
|
||||
class="center tooltip tooltip-top btn btn-sm btn-square {$currentVoiceSession.cameraOn
|
||||
? 'btn-ghost'
|
||||
: 'btn-error'}"
|
||||
onclick={toggleCamera}>
|
||||
<Icon icon={$currentVoiceSession.cameraOn ? VideocameraRecord : Videocamera} size={4} />
|
||||
</Button>
|
||||
<Button
|
||||
data-tip={$currentVoiceSession.screenShareOn ? "Stop sharing" : "Share screen"}
|
||||
class="center tooltip tooltip-top btn btn-sm btn-square {$currentVoiceSession.screenShareOn
|
||||
? 'btn-ghost'
|
||||
: 'btn-error'}"
|
||||
onclick={toggleScreenShare}>
|
||||
<Icon icon={Monitor} 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}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
uniq,
|
||||
int,
|
||||
YEAR,
|
||||
WEEK,
|
||||
DAY,
|
||||
insertAt,
|
||||
sortBy,
|
||||
now,
|
||||
@@ -47,7 +47,7 @@ export const makeFeed = ({
|
||||
onForwardExhausted?: () => void
|
||||
at?: number
|
||||
}) => {
|
||||
const interval = int(WEEK)
|
||||
const interval = int(DAY)
|
||||
const controller = new AbortController()
|
||||
const events = writable<TrustedEvent[]>([])
|
||||
|
||||
@@ -191,7 +191,7 @@ export const makeCalendarFeed = ({
|
||||
element: HTMLElement
|
||||
onExhausted?: () => void
|
||||
}) => {
|
||||
const interval = int(5, WEEK)
|
||||
const interval = int(5, DAY)
|
||||
const controller = new AbortController()
|
||||
|
||||
let exhaustedScrollers = 0
|
||||
|
||||
+22
-101
@@ -14,7 +14,6 @@ import {
|
||||
uniqBy,
|
||||
sortBy,
|
||||
append,
|
||||
reject,
|
||||
sort,
|
||||
uniq,
|
||||
indexBy,
|
||||
@@ -125,7 +124,6 @@ import type {
|
||||
RelayProfile,
|
||||
PublishedList,
|
||||
PublishedRoomMeta,
|
||||
RoomMeta,
|
||||
List,
|
||||
Filter,
|
||||
} from "@welshman/util"
|
||||
@@ -147,7 +145,6 @@ import {
|
||||
displayProfileByPubkey,
|
||||
getProfile,
|
||||
} from "@welshman/app"
|
||||
import {checkRelayHasLivekit} from "$lib/livekit"
|
||||
import {readFeed} from "@lib/feeds"
|
||||
|
||||
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
||||
@@ -460,7 +457,7 @@ export const splitChatId = (id: string) => getChatPubkeys(id.split(","))
|
||||
|
||||
export const chatsById = call(() => {
|
||||
const chatsById = new Map<string, Chat>()
|
||||
const chatsByPubkey = new Map<string, string[]>()
|
||||
const chatsByPubkey = new Map<string, Chat[]>()
|
||||
|
||||
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
|
||||
chat.search_text =
|
||||
@@ -472,12 +469,6 @@ export const chatsById = call(() => {
|
||||
}
|
||||
|
||||
return readable(chatsById, set => {
|
||||
const indexChatByPubkeys = (chat: Chat) => {
|
||||
for (const pubkey of chat.pubkeys) {
|
||||
chatsByPubkey.set(pubkey, uniq(append(chat.id, chatsByPubkey.get(pubkey) || [])))
|
||||
}
|
||||
}
|
||||
|
||||
const addEvents = (events: TrustedEvent[]) => {
|
||||
let dirty = false
|
||||
for (const event of events) {
|
||||
@@ -493,19 +484,21 @@ export const chatsById = call(() => {
|
||||
const updatedChat = addSearchText({id, pubkeys, messages, last_activity})
|
||||
|
||||
chatsById.set(id, updatedChat)
|
||||
indexChatByPubkeys(updatedChat)
|
||||
|
||||
for (const pubkey of pubkeys) {
|
||||
const pubkeyChats = chatsByPubkey.get(pubkey) || []
|
||||
const uniqueChats = uniqBy(chat => chat.id, append(updatedChat, pubkeyChats))
|
||||
|
||||
chatsByPubkey.set(pubkey, uniqueChats)
|
||||
}
|
||||
|
||||
dirty = true
|
||||
}
|
||||
|
||||
if (event.kind === PROFILE) {
|
||||
for (const chatId of chatsByPubkey.get(event.pubkey) || []) {
|
||||
const chat = chatsById.get(chatId)
|
||||
|
||||
if (chat) {
|
||||
addSearchText(chat)
|
||||
dirty = true
|
||||
}
|
||||
for (const chat of chatsByPubkey.get(event.pubkey) || []) {
|
||||
addSearchText(chat)
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,36 +508,10 @@ export const chatsById = call(() => {
|
||||
}
|
||||
}
|
||||
|
||||
const removeEvents = (removed: Set<string>) => {
|
||||
let dirty = false
|
||||
|
||||
for (const id of removed) {
|
||||
const event = repository.getEvent(id)
|
||||
|
||||
if (event && DM_KINDS.includes(event.kind)) {
|
||||
for (const chatId of chatsByPubkey.get(event.pubkey) || []) {
|
||||
const chat = chatsById.get(chatId)
|
||||
|
||||
if (chat) {
|
||||
chat.messages = reject(spec({id: event.id}), chat.messages)
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty) {
|
||||
set(chatsById)
|
||||
}
|
||||
}
|
||||
|
||||
addEvents(repository.query([{kinds: [...DM_KINDS, DELETE, PROFILE]}]))
|
||||
addEvents(repository.query([{kinds: [...DM_KINDS, PROFILE]}]))
|
||||
|
||||
const unsubscribers = [
|
||||
on(repository, "update", ({added, removed}: RepositoryUpdate) => {
|
||||
addEvents(added)
|
||||
removeEvents(removed)
|
||||
}),
|
||||
on(repository, "update", ({added}: RepositoryUpdate) => addEvents(added)),
|
||||
]
|
||||
|
||||
return () => unsubscribers.forEach(call)
|
||||
@@ -569,25 +536,17 @@ export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => {
|
||||
|
||||
// Rooms
|
||||
|
||||
export enum RoomType {
|
||||
Text = "text",
|
||||
Voice = "voice",
|
||||
}
|
||||
|
||||
export type Room = PublishedRoomMeta & {
|
||||
id: 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 splitRoomId = (id: string) => id.split("'")
|
||||
|
||||
export const hasNip29 = (relay?: RelayProfile) =>
|
||||
Boolean(relay?.supported_nips?.map?.(String)?.includes?.("29"))
|
||||
relay?.supported_nips?.map?.(String)?.includes?.("29")
|
||||
|
||||
export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
|
||||
tracker,
|
||||
@@ -673,30 +632,6 @@ export const displayRoom = (url: string, h: string) => getRoom(makeRoomId(url, h
|
||||
|
||||
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
|
||||
|
||||
export const groupListsByPubkey = deriveItemsByKey({
|
||||
@@ -786,20 +721,17 @@ export const deriveUserRooms = (url: string) =>
|
||||
})
|
||||
|
||||
export const deriveOtherRooms = (url: string) =>
|
||||
derived(
|
||||
[deriveUserRooms(url), deriveVoiceRooms(url), roomsByUrl],
|
||||
([$userRooms, voiceRooms, $roomsByUrl]) => {
|
||||
const rooms: string[] = []
|
||||
derived([deriveUserRooms(url), roomsByUrl], ([$userRooms, $roomsByUrl]) => {
|
||||
const rooms: string[] = []
|
||||
|
||||
for (const {h} of $roomsByUrl.get(url) || []) {
|
||||
if (!$userRooms.includes(h) && !voiceRooms.has(h)) {
|
||||
rooms.push(h)
|
||||
}
|
||||
for (const {h} of $roomsByUrl.get(url) || []) {
|
||||
if (!$userRooms.includes(h)) {
|
||||
rooms.push(h)
|
||||
}
|
||||
}
|
||||
|
||||
return sortBy(roomComparator(url), uniq(rooms))
|
||||
},
|
||||
)
|
||||
return sortBy(roomComparator(url), uniq(rooms))
|
||||
})
|
||||
|
||||
// Space/room memberships
|
||||
|
||||
@@ -1202,12 +1134,6 @@ 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) => {
|
||||
const store = writable<boolean>(false)
|
||||
|
||||
@@ -1267,8 +1193,3 @@ export const shouldNotify = (url: string, h?: string) => getShouldNotify(getSett
|
||||
|
||||
export const deriveShouldNotify = (url: string, h?: string) =>
|
||||
derived(userSettingsValues, $settings => getShouldNotify($settings, url, h))
|
||||
|
||||
// Whatever who cares
|
||||
|
||||
export const hasNip50 = (relay?: RelayProfile) =>
|
||||
Boolean(relay?.supported_nips?.map?.(String)?.includes?.("50"))
|
||||
|
||||
+19
-13
@@ -1,5 +1,6 @@
|
||||
import {call} from "@welshman/lib"
|
||||
import {Preferences} from "@capacitor/preferences"
|
||||
import {Filesystem, Directory} from "@capacitor/filesystem"
|
||||
import {IDB} from "@lib/indexeddb"
|
||||
|
||||
export const kv = call(() => {
|
||||
@@ -30,17 +31,22 @@ export const kv = call(() => {
|
||||
return {get, set, clear}
|
||||
})
|
||||
|
||||
export const db = new IDB({
|
||||
name: "flotilla-9gl",
|
||||
version: 1,
|
||||
stores: [
|
||||
{name: "events", keyPath: "id"},
|
||||
{name: "tracker", keyPath: "id"},
|
||||
{name: "relays", keyPath: "url"},
|
||||
{name: "relayStats", keyPath: "url"},
|
||||
{name: "handles", keyPath: "nip05"},
|
||||
{name: "zappers", keyPath: "lnurl"},
|
||||
{name: "plaintext", keyPath: "key"},
|
||||
{name: "wrapManager", keyPath: "id"},
|
||||
],
|
||||
export const db = new IDB({name: "flotilla-9gl", version: 1})
|
||||
|
||||
// Migration - we used to use capacitor's filesystem for storage, clear it out since we're
|
||||
// going back to indexeddb
|
||||
call(async () => {
|
||||
const res = await Filesystem.readdir({
|
||||
path: "",
|
||||
directory: Directory.Data,
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
res.files.map(file =>
|
||||
Filesystem.deleteFile({
|
||||
path: file.name,
|
||||
directory: Directory.Data,
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -55,7 +55,6 @@ import {
|
||||
loadFeedsForPubkey,
|
||||
} from "@app/core/state"
|
||||
import {hasBlossomSupport} from "@app/core/commands"
|
||||
import {LIVEKIT_PARTICIPANTS} from "@app/voice"
|
||||
|
||||
// Utils
|
||||
|
||||
@@ -317,12 +316,6 @@ const syncSpace = (url: string, rooms: string[]) => {
|
||||
})
|
||||
}
|
||||
|
||||
pullAndListen({
|
||||
url,
|
||||
signal: controller.signal,
|
||||
filters: [{kinds: [LIVEKIT_PARTICIPANTS]}],
|
||||
})
|
||||
|
||||
return () => controller.abort()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ export const setupHistory = () =>
|
||||
if ($page.params.relay) {
|
||||
lastPageBySpaceUrl.set($page.params.relay, $page.url.pathname)
|
||||
}
|
||||
|
||||
if ($page.params.chat) {
|
||||
lastChatUrl = $page.url.pathname
|
||||
}
|
||||
|
||||
+3
-33
@@ -1,5 +1,5 @@
|
||||
import type {Component} from "svelte"
|
||||
import {get, writable} from "svelte/store"
|
||||
import {writable} from "svelte/store"
|
||||
import {randomId, always, assoc, Emitter} from "@welshman/lib"
|
||||
import {deriveDeduplicated} from "@welshman/store"
|
||||
import {goto} from "$app/navigation"
|
||||
@@ -7,7 +7,6 @@ import {page} from "$app/stores"
|
||||
|
||||
export type ModalOptions = {
|
||||
drawer?: boolean
|
||||
nested?: boolean
|
||||
noEscape?: boolean
|
||||
fullscreen?: boolean
|
||||
replaceState?: boolean
|
||||
@@ -25,18 +24,8 @@ export const emitter = new Emitter()
|
||||
|
||||
export const modals = writable<Record<string, Modal>>({})
|
||||
|
||||
const getIdsFromHash = (hash: string) => hash.slice(1).split(",").filter(Boolean)
|
||||
|
||||
export const modalStack = deriveDeduplicated([page, modals], ([$page, $modals]) => {
|
||||
return getIdsFromHash($page.url.hash)
|
||||
.map(id => $modals[id])
|
||||
.filter(Boolean)
|
||||
})
|
||||
|
||||
export const modal = deriveDeduplicated([page, modals], ([$page, $modals]) => {
|
||||
const ids = getIdsFromHash($page.url.hash)
|
||||
|
||||
return $modals[ids.at(-1) || ""]
|
||||
return $modals[$page.url.hash.slice(1)]
|
||||
})
|
||||
|
||||
export const pushModal = (
|
||||
@@ -46,12 +35,10 @@ export const pushModal = (
|
||||
) => {
|
||||
const id = randomId()
|
||||
const path = options.path || ""
|
||||
const existingIds = getIdsFromHash(get(page).url.hash)
|
||||
const ids = options.nested ? [...existingIds, id] : [id]
|
||||
|
||||
modals.update(assoc(id, {id, component, props, options}))
|
||||
|
||||
goto(path + "#" + ids.join(","), {replaceState: options.replaceState})
|
||||
goto(path + "#" + id, {replaceState: options.replaceState})
|
||||
|
||||
return id
|
||||
}
|
||||
@@ -62,24 +49,7 @@ export const pushDrawer = (
|
||||
options: ModalOptions = {},
|
||||
) => pushModal(component, props, {...options, drawer: true})
|
||||
|
||||
export const popModal = () => {
|
||||
const url = get(page).url
|
||||
const ids = getIdsFromHash(url.hash)
|
||||
|
||||
if (ids.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = ids.slice(0, -1).join(",")
|
||||
const hash = next ? `#${next}` : ""
|
||||
|
||||
goto(url.pathname + url.search + hash, {replaceState: true})
|
||||
}
|
||||
|
||||
export const clearModals = () => {
|
||||
const url = get(page).url
|
||||
|
||||
goto(url.pathname + url.search, {replaceState: true})
|
||||
modals.update(always({}))
|
||||
emitter.emit("close")
|
||||
}
|
||||
|
||||
@@ -180,6 +180,7 @@ export const allNotifications = derived(
|
||||
|
||||
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
|
||||
const spacePath = makeSpacePath(url)
|
||||
const spacePathMobile = spacePath + ":mobile"
|
||||
const eventsById = eventsByIdByUrl.get(url) || new Map()
|
||||
const latestEvent = first(sortEventsDesc(eventsById.values()))
|
||||
|
||||
@@ -193,6 +194,7 @@ export const allNotifications = derived(
|
||||
const latestEvent = find(e => e.tags.some(spec(["h", h])), eventsById.values())
|
||||
|
||||
if (hasNotification(roomPath, latestEvent)) {
|
||||
paths.add(spacePathMobile)
|
||||
paths.add(spacePath)
|
||||
paths.add(roomPath)
|
||||
}
|
||||
@@ -201,6 +203,7 @@ export const allNotifications = derived(
|
||||
const messagesPath = makeSpaceChatPath(url)
|
||||
|
||||
if (hasNotification(messagesPath, first(eventsById.values()))) {
|
||||
paths.add(spacePathMobile)
|
||||
paths.add(spacePath)
|
||||
paths.add(messagesPath)
|
||||
}
|
||||
@@ -308,15 +311,14 @@ class CapacitorNotifications implements IPushAdapter {
|
||||
}
|
||||
|
||||
let {token} = notificationState.get()
|
||||
let error = "failed to retrieve token"
|
||||
|
||||
if (!token) {
|
||||
const listeners = [
|
||||
PushNotifications.addListener("registration", ({value}: Token) => {
|
||||
token = value
|
||||
}),
|
||||
PushNotifications.addListener("registrationError", (err: RegistrationError) => {
|
||||
error = err.error
|
||||
PushNotifications.addListener("registrationError", (error: RegistrationError) => {
|
||||
console.error(error)
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -332,7 +334,7 @@ class CapacitorNotifications implements IPushAdapter {
|
||||
notificationState.update(assoc("token", token))
|
||||
}
|
||||
|
||||
return token ? status.receive : error
|
||||
return token ? "granted" : "denied"
|
||||
}
|
||||
|
||||
async _syncServer(signal: AbortSignal) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type {Page} from "@sveltejs/kit"
|
||||
import theme from "tailwindcss/defaultTheme"
|
||||
import {get} from "svelte/store"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {goto} from "$app/navigation"
|
||||
@@ -7,7 +6,7 @@ import {page} from "$app/stores"
|
||||
import {nthEq} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {getAddress} from "@welshman/util"
|
||||
import {tracker} from "@welshman/app"
|
||||
import {tracker, loadRelay} from "@welshman/app"
|
||||
import {identity} from "@welshman/lib"
|
||||
import {
|
||||
getTagValue,
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
decodeRelay,
|
||||
encodeRelay,
|
||||
userSpaceUrls,
|
||||
hasNip29,
|
||||
DM_KINDS,
|
||||
ROOM,
|
||||
} from "@app/core/state"
|
||||
@@ -47,12 +47,12 @@ export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) =>
|
||||
export const goToSpace = async (url: string) => {
|
||||
const prevPath = lastPageBySpaceUrl.get(encodeRelay(url))
|
||||
|
||||
if (prevPath && prevPath !== makeSpacePath(url)) {
|
||||
if (prevPath) {
|
||||
goto(prevPath)
|
||||
} else if (window.matchMedia(`(min-width: ${theme.screens.md})`).matches) {
|
||||
} else if (hasNip29(await loadRelay(url))) {
|
||||
goto(makeSpacePath(url, "recent"))
|
||||
} else {
|
||||
goto(makeSpacePath(url))
|
||||
goto(makeSpacePath(url, "chat"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+177
-238
@@ -45,8 +45,9 @@ import {
|
||||
wrapManager,
|
||||
onRelay,
|
||||
} from "@welshman/app"
|
||||
import type {Unsubscriber} from "svelte/store"
|
||||
import {db} from "@app/core/storage"
|
||||
import {isMobile} from "@lib/html"
|
||||
import type {IDBTable} from "@lib/indexeddb"
|
||||
import {MESSAGE_KINDS, DM_KINDS} from "@app/core/state"
|
||||
|
||||
const kinds = {
|
||||
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS],
|
||||
@@ -61,266 +62,204 @@ const kinds = {
|
||||
ROOM_REMOVE_MEMBER,
|
||||
ROOM_CREATE_PERMISSION,
|
||||
],
|
||||
content: [...MESSAGE_KINDS, ...DM_KINDS],
|
||||
}
|
||||
|
||||
const shouldPersistEvent = (event: TrustedEvent) =>
|
||||
kinds.meta.includes(event.kind) ||
|
||||
kinds.alert.includes(event.kind) ||
|
||||
kinds.space.includes(event.kind) ||
|
||||
kinds.room.includes(event.kind)
|
||||
const rankEvent = (event: TrustedEvent) => {
|
||||
if (kinds.meta.includes(event.kind)) return 9
|
||||
if (kinds.alert.includes(event.kind)) return 8
|
||||
if (kinds.space.includes(event.kind)) return 7
|
||||
if (kinds.room.includes(event.kind)) return 6
|
||||
if (!isMobile && kinds.content.includes(event.kind)) return 5
|
||||
return 0
|
||||
}
|
||||
|
||||
type TrackerItem = {id: string; relays: string[]}
|
||||
const eventsAdapter = {
|
||||
name: "events",
|
||||
keyPath: "id",
|
||||
init: async (table: IDBTable<TrustedEvent>) => {
|
||||
const initialEvents = await table.getAll()
|
||||
|
||||
type PlaintextItem = {key: string; value: string}
|
||||
|
||||
const loadCriticalEvents = async () => {
|
||||
const table = db.table<TrustedEvent>("events")
|
||||
const initialEvents = await table.getAll()
|
||||
const keep: TrustedEvent[] = []
|
||||
const drop: string[] = []
|
||||
|
||||
for (const event of initialEvents) {
|
||||
if (shouldPersistEvent(event)) {
|
||||
// Mark events verified to avoid re-verification of signatures
|
||||
for (const event of initialEvents) {
|
||||
event[verifiedSymbol] = true
|
||||
keep.push(event)
|
||||
} else {
|
||||
drop.push(event.id)
|
||||
}
|
||||
}
|
||||
|
||||
repository.load(keep)
|
||||
repository.load(initialEvents)
|
||||
|
||||
if (drop.length > 0) {
|
||||
void table.bulkDelete(drop)
|
||||
}
|
||||
}
|
||||
return on(
|
||||
repository,
|
||||
"update",
|
||||
batch(3000, async (updates: RepositoryUpdate[]) => {
|
||||
const add: TrustedEvent[] = []
|
||||
const remove = new Set<string>()
|
||||
|
||||
const syncEvents = () => {
|
||||
const table = db.table<TrustedEvent>("events")
|
||||
for (const update of updates) {
|
||||
for (const event of update.added) {
|
||||
if (rankEvent(event) > 0) {
|
||||
add.push(event)
|
||||
remove.delete(event.id)
|
||||
}
|
||||
}
|
||||
|
||||
return on(
|
||||
repository,
|
||||
"update",
|
||||
batch(3000, async (updates: RepositoryUpdate[]) => {
|
||||
const add: TrustedEvent[] = []
|
||||
const remove = new Set<string>()
|
||||
|
||||
for (const update of updates) {
|
||||
for (const event of update.added) {
|
||||
if (shouldPersistEvent(event)) {
|
||||
add.push(event)
|
||||
remove.delete(event.id)
|
||||
for (const id of update.removed) {
|
||||
remove.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of update.removed) {
|
||||
remove.add(id)
|
||||
if (add.length > 0) {
|
||||
await table.bulkPut(add)
|
||||
}
|
||||
|
||||
if (remove.size > 0) {
|
||||
await table.bulkDelete(remove)
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
type TrackerItem = {id: string; relays: string[]}
|
||||
|
||||
const trackerAdapter = {
|
||||
name: "tracker",
|
||||
keyPath: "id",
|
||||
init: async (table: IDBTable<TrackerItem>) => {
|
||||
const relaysById = new Map<string, Set<string>>()
|
||||
|
||||
for (const {id, relays} of await table.getAll()) {
|
||||
relaysById.set(id, new Set(relays))
|
||||
}
|
||||
|
||||
tracker.load(relaysById)
|
||||
|
||||
const _onAdd = async (ids: Iterable<string>) => {
|
||||
const items: TrackerItem[] = []
|
||||
|
||||
for (const id of ids) {
|
||||
const event = repository.getEvent(id)
|
||||
|
||||
if (!event || rankEvent(event) === 0) continue
|
||||
|
||||
const relays = Array.from(tracker.getRelays(id))
|
||||
|
||||
if (relays.length === 0) continue
|
||||
|
||||
items.push({id, relays})
|
||||
}
|
||||
|
||||
if (add.length > 0) {
|
||||
await table.bulkPut(add)
|
||||
}
|
||||
|
||||
if (remove.size > 0) {
|
||||
await table.bulkDelete(remove)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const loadCriticalTracker = async () => {
|
||||
const table = db.table<TrackerItem>("tracker")
|
||||
const relaysById = new Map<string, Set<string>>()
|
||||
const stale: string[] = []
|
||||
|
||||
for (const {id, relays} of await table.getAll()) {
|
||||
if (!repository.getEvent(id)) {
|
||||
stale.push(id)
|
||||
continue
|
||||
await table.bulkPut(items)
|
||||
}
|
||||
|
||||
relaysById.set(id, new Set(relays))
|
||||
}
|
||||
|
||||
tracker.load(relaysById)
|
||||
|
||||
if (stale.length > 0) {
|
||||
void table.bulkDelete(stale)
|
||||
}
|
||||
}
|
||||
|
||||
const syncTracker = () => {
|
||||
const table = db.table<TrackerItem>("tracker")
|
||||
|
||||
const _onAdd = async (ids: Iterable<string>) => {
|
||||
const items: TrackerItem[] = []
|
||||
|
||||
for (const id of ids) {
|
||||
const event = repository.getEvent(id)
|
||||
|
||||
if (!event || !shouldPersistEvent(event)) continue
|
||||
|
||||
const relays = Array.from(tracker.getRelays(id))
|
||||
|
||||
if (relays.length === 0) continue
|
||||
|
||||
items.push({id, relays})
|
||||
const _onRemove = async (ids: Iterable<string>) => {
|
||||
await table.bulkDelete(Array.from(ids))
|
||||
}
|
||||
|
||||
await table.bulkPut(items)
|
||||
}
|
||||
const onAdd = batch(3000, _onAdd)
|
||||
|
||||
const _onRemove = async (ids: Iterable<string>) => {
|
||||
await table.bulkDelete(Array.from(ids))
|
||||
}
|
||||
const onRemove = batch(3000, _onRemove)
|
||||
|
||||
const onAdd = batch(3000, _onAdd)
|
||||
const onRemove = batch(3000, _onRemove)
|
||||
const onLoad = () => _onAdd(tracker.relaysById.keys())
|
||||
const onClear = () => _onRemove(tracker.relaysById.keys())
|
||||
const onLoad = () => _onAdd(tracker.relaysById.keys())
|
||||
|
||||
tracker.on("add", onAdd)
|
||||
tracker.on("remove", onRemove)
|
||||
tracker.on("load", onLoad)
|
||||
tracker.on("clear", onClear)
|
||||
const onClear = () => _onRemove(tracker.relaysById.keys())
|
||||
|
||||
return () => {
|
||||
tracker.off("add", onAdd)
|
||||
tracker.off("remove", onRemove)
|
||||
tracker.off("load", onLoad)
|
||||
tracker.off("clear", onClear)
|
||||
}
|
||||
}
|
||||
tracker.on("add", onAdd)
|
||||
tracker.on("remove", onRemove)
|
||||
tracker.on("load", onLoad)
|
||||
tracker.on("clear", onClear)
|
||||
|
||||
const loadCriticalRelays = async () => {
|
||||
const table = db.table<RelayProfile>("relays")
|
||||
|
||||
relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
|
||||
}
|
||||
|
||||
const syncRelays = () => onRelay(batch(1000, db.table<RelayProfile>("relays").bulkPut))
|
||||
|
||||
const initRelayStats = async () => {
|
||||
const table = db.table<RelayStats>("relayStats")
|
||||
|
||||
relayStatsByUrl.set(indexBy(r => r.url, await table.getAll()))
|
||||
|
||||
return onRelayStats(batch(1000, table.bulkPut))
|
||||
}
|
||||
|
||||
const initHandles = async () => {
|
||||
const table = db.table<Handle>("handles")
|
||||
|
||||
handlesByNip05.set(indexBy(r => r.nip05, await table.getAll()))
|
||||
|
||||
return onHandle(batch(1000, table.bulkPut))
|
||||
}
|
||||
|
||||
const initZappers = async () => {
|
||||
const table = db.table<Zapper>("zappers")
|
||||
|
||||
zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll()))
|
||||
|
||||
return onZapper(batch(3000, table.bulkPut))
|
||||
}
|
||||
|
||||
const initPlaintext = async () => {
|
||||
const table = db.table<PlaintextItem>("plaintext")
|
||||
const initialRecords = await table.getAll()
|
||||
|
||||
plaintext.set(fromPairs(initialRecords.map(({key, value}) => [key, value])))
|
||||
|
||||
return throttled(3000, plaintext).subscribe($plaintext => {
|
||||
table.bulkPut(Object.entries($plaintext).map(([key, value]) => ({key, value})))
|
||||
})
|
||||
}
|
||||
|
||||
const initWrapManager = async () => {
|
||||
const table = db.table<WrapItem>("wrapManager")
|
||||
|
||||
wrapManager.load(await table.getAll())
|
||||
|
||||
const addOne = batch(3000, table.bulkPut)
|
||||
const removeOne = throttle(3000, table.bulkDelete)
|
||||
|
||||
wrapManager.on("add", addOne)
|
||||
wrapManager.on("remove", removeOne)
|
||||
|
||||
return () => {
|
||||
wrapManager.off("add", addOne)
|
||||
wrapManager.off("remove", removeOne)
|
||||
}
|
||||
}
|
||||
|
||||
type StorageSync = {
|
||||
unsubscribe: Unsubscriber
|
||||
ready: Promise<void>
|
||||
}
|
||||
|
||||
export const sync = (): StorageSync => {
|
||||
const unsubscribers: Unsubscriber[] = []
|
||||
const deferredTimers: ReturnType<typeof setTimeout>[] = []
|
||||
let stopped = false
|
||||
|
||||
const addUnsubscriber = (unsubscriber: Unsubscriber) => {
|
||||
if (stopped) {
|
||||
unsubscriber()
|
||||
} else {
|
||||
unsubscribers.push(unsubscriber)
|
||||
return () => {
|
||||
tracker.off("add", onAdd)
|
||||
tracker.off("remove", onRemove)
|
||||
tracker.off("load", onLoad)
|
||||
tracker.off("clear", onClear)
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleDeferred = (task: () => Promise<void>) => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (stopped) return
|
||||
|
||||
void task()
|
||||
}, 0)
|
||||
|
||||
deferredTimers.push(timeout)
|
||||
}
|
||||
|
||||
const ready = (async () => {
|
||||
await db.connect()
|
||||
|
||||
await Promise.all([loadCriticalEvents(), loadCriticalRelays()])
|
||||
await loadCriticalTracker()
|
||||
|
||||
addUnsubscriber(syncEvents())
|
||||
addUnsubscriber(syncTracker())
|
||||
addUnsubscriber(syncRelays())
|
||||
|
||||
scheduleDeferred(async () => {
|
||||
addUnsubscriber(await initRelayStats())
|
||||
})
|
||||
|
||||
scheduleDeferred(async () => {
|
||||
addUnsubscriber(await initHandles())
|
||||
})
|
||||
|
||||
scheduleDeferred(async () => {
|
||||
addUnsubscriber(await initZappers())
|
||||
})
|
||||
|
||||
scheduleDeferred(async () => {
|
||||
addUnsubscriber(await initPlaintext())
|
||||
})
|
||||
|
||||
scheduleDeferred(async () => {
|
||||
addUnsubscriber(await initWrapManager())
|
||||
})
|
||||
})()
|
||||
|
||||
const unsubscribe = () => {
|
||||
stopped = true
|
||||
|
||||
for (const timeout of deferredTimers) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
|
||||
unsubscribers.forEach(unsubscriber => unsubscriber())
|
||||
}
|
||||
|
||||
return {unsubscribe, ready}
|
||||
},
|
||||
}
|
||||
|
||||
const relaysAdapter = {
|
||||
name: "relays",
|
||||
keyPath: "url",
|
||||
init: async (table: IDBTable<RelayProfile>) => {
|
||||
relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
|
||||
|
||||
return onRelay(batch(1000, table.bulkPut))
|
||||
},
|
||||
}
|
||||
|
||||
const relayStatsAdapter = {
|
||||
name: "relayStats",
|
||||
keyPath: "url",
|
||||
init: async (table: IDBTable<RelayStats>) => {
|
||||
relayStatsByUrl.set(indexBy(r => r.url, await table.getAll()))
|
||||
|
||||
return onRelayStats(batch(1000, table.bulkPut))
|
||||
},
|
||||
}
|
||||
|
||||
const handlesAdapter = {
|
||||
name: "handles",
|
||||
keyPath: "nip05",
|
||||
init: async (table: IDBTable<Handle>) => {
|
||||
handlesByNip05.set(indexBy(r => r.nip05, await table.getAll()))
|
||||
|
||||
return onHandle(batch(1000, table.bulkPut))
|
||||
},
|
||||
}
|
||||
|
||||
const zappersAdapter = {
|
||||
name: "zappers",
|
||||
keyPath: "lnurl",
|
||||
init: async (table: IDBTable<Zapper>) => {
|
||||
zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll()))
|
||||
|
||||
return onZapper(batch(3000, table.bulkPut))
|
||||
},
|
||||
}
|
||||
|
||||
type PlaintextItem = {key: string; value: string}
|
||||
|
||||
const plaintextAdapter = {
|
||||
name: "plaintext",
|
||||
keyPath: "key",
|
||||
init: async (table: IDBTable<PlaintextItem>) => {
|
||||
const initialRecords = await table.getAll()
|
||||
|
||||
plaintext.set(fromPairs(initialRecords.map(({key, value}) => [key, value])))
|
||||
|
||||
return throttled(3000, plaintext).subscribe($plaintext => {
|
||||
table.bulkPut(Object.entries($plaintext).map(([key, value]) => ({key, value})))
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const wrapManagerAdapter = {
|
||||
name: "wrapManager",
|
||||
keyPath: "id",
|
||||
init: async (table: IDBTable<WrapItem>) => {
|
||||
wrapManager.load(await table.getAll())
|
||||
|
||||
const addOne = batch(3000, table.bulkPut)
|
||||
|
||||
const removeOne = throttle(3000, table.bulkDelete)
|
||||
|
||||
wrapManager.on("add", addOne)
|
||||
wrapManager.on("remove", removeOne)
|
||||
|
||||
return () => {
|
||||
wrapManager.off("add", addOne)
|
||||
wrapManager.off("remove", removeOne)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const adapters = [
|
||||
eventsAdapter,
|
||||
trackerAdapter,
|
||||
relaysAdapter,
|
||||
relayStatsAdapter,
|
||||
handlesAdapter,
|
||||
zappersAdapter,
|
||||
plaintextAdapter,
|
||||
wrapManagerAdapter,
|
||||
]
|
||||
|
||||
@@ -1,442 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
LocalParticipant,
|
||||
LocalTrackPublication,
|
||||
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
|
||||
cameraOn: boolean
|
||||
screenShareOn: 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())
|
||||
|
||||
/** Bumps when remote video is subscribed/unsubscribed so layout/video UI can react. */
|
||||
export const videoCallLayoutRevision = writable(0)
|
||||
|
||||
/** Spotlight tile id — must match VideoCallContent `tileKey` (identity + source, not trackSid). */
|
||||
export const videoPrimaryTileKey = writable<string | undefined>(undefined)
|
||||
|
||||
export const toggleVideoPrimaryTile = (key: string) => {
|
||||
videoPrimaryTileKey.update(k => (k === key ? undefined : key))
|
||||
}
|
||||
|
||||
const bumpVideoCallLayoutRevision = () => videoCallLayoutRevision.update(n => n + 1)
|
||||
|
||||
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) => {
|
||||
videoCallLayoutRevision.set(0)
|
||||
videoPrimaryTileKey.set(undefined)
|
||||
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(() => {})
|
||||
} else if (track.kind === Track.Kind.Video) {
|
||||
bumpVideoCallLayoutRevision()
|
||||
}
|
||||
}
|
||||
|
||||
const onTrackUnsubscribed = (track: Track) => {
|
||||
track.detach().forEach(el => el.remove())
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
bumpVideoCallLayoutRevision()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const onLocalTrackUnpublished = (
|
||||
publication: LocalTrackPublication,
|
||||
participant: LocalParticipant,
|
||||
) => {
|
||||
if (publication.source !== Track.Source.ScreenShare) return
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session || participant.identity !== session.room.localParticipant.identity) return
|
||||
if (!session.screenShareOn) return
|
||||
currentVoiceSession.set({...session, screenShareOn: false})
|
||||
bumpVideoCallLayoutRevision()
|
||||
}
|
||||
|
||||
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.LocalTrackUnpublished, onLocalTrackUnpublished)
|
||||
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, cameraOn: false, screenShareOn: false})
|
||||
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(() => {})
|
||||
|
||||
if (session.cameraOn) {
|
||||
try {
|
||||
await session.room.localParticipant.setCameraEnabled(false)
|
||||
} catch {
|
||||
/* pass */
|
||||
}
|
||||
}
|
||||
|
||||
if (session.screenShareOn) {
|
||||
try {
|
||||
await session.room.localParticipant.setScreenShareEnabled(false)
|
||||
} catch {
|
||||
/* pass */
|
||||
}
|
||||
}
|
||||
|
||||
voiceState.set("disconnected")
|
||||
videoCallLayoutRevision.set(0)
|
||||
videoPrimaryTileKey.set(undefined)
|
||||
currentVoiceSession.set(undefined)
|
||||
session.room.disconnect()
|
||||
speakingParticipants.set([])
|
||||
participantPubkeyMap.set(new Map())
|
||||
}
|
||||
|
||||
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"})
|
||||
}
|
||||
}
|
||||
|
||||
const VISUAL_SOURCES = [Track.Source.Camera, Track.Source.ScreenShare] as const
|
||||
|
||||
const roomHasSubscribedRemoteVisual = (room: Room): boolean => {
|
||||
for (const p of room.remoteParticipants.values()) {
|
||||
for (const source of VISUAL_SOURCES) {
|
||||
const pub = p.getTrackPublication(source)
|
||||
if (pub?.isSubscribed && pub.track) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** True when local camera/screen share is on or any subscribed remote camera/screen track. */
|
||||
export const videoCallContentActive = derived(
|
||||
[currentVoiceSession, voiceState, videoCallLayoutRevision],
|
||||
([$session, $state, _rev]) => {
|
||||
if ($state !== "connected" || !$session) return false
|
||||
if ($session.cameraOn || $session.screenShareOn) return true
|
||||
return roomHasSubscribedRemoteVisual($session.room)
|
||||
},
|
||||
)
|
||||
|
||||
const countLiveVisualFeeds = (session: VoiceSession): number => {
|
||||
const room = session.room
|
||||
let n = 0
|
||||
const lp = room.localParticipant
|
||||
if (session.cameraOn) {
|
||||
const pub = lp.getTrackPublication(Track.Source.Camera)
|
||||
if (pub?.track) n += 1
|
||||
}
|
||||
if (session.screenShareOn) {
|
||||
const pub = lp.getTrackPublication(Track.Source.ScreenShare)
|
||||
if (pub?.track) n += 1
|
||||
}
|
||||
for (const rp of room.remoteParticipants.values()) {
|
||||
for (const source of VISUAL_SOURCES) {
|
||||
const pub = rp.getTrackPublication(source)
|
||||
if (pub?.isSubscribed && pub.track) n += 1
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
export const videoTileCount = derived(
|
||||
[currentVoiceSession, voiceState, videoCallLayoutRevision],
|
||||
([$session, $state, _rev]) => {
|
||||
if ($state !== "connected" || !$session) return 0
|
||||
return countLiveVisualFeeds($session)
|
||||
},
|
||||
)
|
||||
|
||||
export const toggleCamera = async () => {
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session) return
|
||||
|
||||
const cameraOn = !session.cameraOn
|
||||
if (!cameraOn) {
|
||||
session.room.localParticipant.setCameraEnabled(false)
|
||||
currentVoiceSession.set({...session, cameraOn})
|
||||
bumpVideoCallLayoutRevision()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await session.room.localParticipant.setCameraEnabled(true)
|
||||
currentVoiceSession.set({...session, cameraOn})
|
||||
bumpVideoCallLayoutRevision()
|
||||
} catch (e) {
|
||||
pushToast({theme: "error", message: "Could not access camera"})
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleScreenShare = async () => {
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session) return
|
||||
|
||||
const screenShareOn = !session.screenShareOn
|
||||
if (!screenShareOn) {
|
||||
session.room.localParticipant.setScreenShareEnabled(false)
|
||||
currentVoiceSession.set({...session, screenShareOn})
|
||||
bumpVideoCallLayoutRevision()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await session.room.localParticipant.setScreenShareEnabled(true)
|
||||
currentVoiceSession.set({...session, screenShareOn})
|
||||
bumpVideoCallLayoutRevision()
|
||||
} catch (e) {
|
||||
pushToast({theme: "error", message: "Could not start screen sharing"})
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 734 B |
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import {slide} from "@lib/transition"
|
||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
interface Props {
|
||||
title?: import("svelte").Snippet
|
||||
description?: import("svelte").Snippet
|
||||
children?: import("svelte").Snippet
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const {...props}: Props = $props()
|
||||
|
||||
const toggle = () => {
|
||||
isOpen = !isOpen
|
||||
}
|
||||
|
||||
let isOpen = $state(false)
|
||||
</script>
|
||||
|
||||
<div class="relative flex flex-col gap-4 {props.class}">
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-8 top-8 h-4 w-4 cursor-pointer transition-all"
|
||||
class:rotate-90={!isOpen}
|
||||
onclick={toggle}>
|
||||
<Icon icon={AltArrowDown} />
|
||||
</button>
|
||||
{@render props.title?.()}
|
||||
{@render props.description?.()}
|
||||
{#if isOpen}
|
||||
<div transition:slide>
|
||||
{@render props.children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -3,10 +3,6 @@
|
||||
import cx from "classnames"
|
||||
import {noop} from "@welshman/lib"
|
||||
import {fade, fly} from "@lib/transition"
|
||||
import Close from "@assets/icons/close.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {clearModals} from "@app/util/modal"
|
||||
|
||||
type Props = {
|
||||
onClose?: any
|
||||
@@ -47,11 +43,6 @@
|
||||
</button>
|
||||
<div class={wrapperClass}>
|
||||
<div class={innerClass} transition:fly>
|
||||
<Button
|
||||
class="absolute -top-4 right-3 btn btn-circle btn-neutral btn-sm"
|
||||
onclick={clearModals}>
|
||||
<Icon icon={Close} size={6} />
|
||||
</Button>
|
||||
<children.component {...children.props} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
import {between, throttle} from "@welshman/lib"
|
||||
import {isMobile} from "@lib/html"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Dialog from "@lib/components/Dialog.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import IconPickerModal from "@app/components/IconPickerModal.svelte"
|
||||
import IconPickerPopover from "@app/components/IconPickerPopover.svelte"
|
||||
import {pushModal, popModal} from "@app/util/modal"
|
||||
import IconPicker from "@app/components/IconPicker.svelte"
|
||||
import IconPickerDialog from "@app/components/IconPickerDialog.svelte"
|
||||
|
||||
const {...props} = $props()
|
||||
|
||||
const open = () => {
|
||||
if (isMobile) {
|
||||
pushModal(IconPickerModal, {onSelect: onClick}, {nested: true})
|
||||
showIconPicker = true
|
||||
} else {
|
||||
popover?.show()
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
const close = () => {
|
||||
if (isMobile) {
|
||||
popModal()
|
||||
showIconPicker = false
|
||||
} else {
|
||||
popover?.hide()
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
let showIconPicker = $state(false)
|
||||
let popover: Instance | undefined = $state()
|
||||
</script>
|
||||
|
||||
@@ -48,10 +49,21 @@
|
||||
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={IconPickerPopover}
|
||||
component={IconPickerDialog}
|
||||
props={{onSelect: onClick}}
|
||||
params={{trigger: "manual", interactive: true, placement: "top-end"}}>
|
||||
<Button onclick={open} class={props.class}>
|
||||
{@render props.children?.()}
|
||||
</Button>
|
||||
</Tippy>
|
||||
|
||||
{#if showIconPicker}
|
||||
<Dialog
|
||||
onClose={close}
|
||||
children={{
|
||||
component: IconPicker,
|
||||
props: {
|
||||
onSelect: onClick,
|
||||
},
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {page} from "$app/stores"
|
||||
import {decodeRelay} from "@app/core/state"
|
||||
|
||||
interface Props {
|
||||
children?: Snippet
|
||||
class?: string
|
||||
icon?: Snippet
|
||||
title?: Snippet
|
||||
action?: Snippet
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const {children, ...props}: Props = $props()
|
||||
const {...props}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div data-component="PageBar" class="cw top-sai fixed z-nav p-2 {props.class}">
|
||||
<div class="rounded-xl bg-base-100 p-4 shadow-md h-20 md:h-12 flex flex-col justify-center">
|
||||
{@render children?.()}
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="ellipsize flex items-center gap-4 whitespace-nowrap">
|
||||
{@render props.icon?.()}
|
||||
{@render props.title?.()}
|
||||
</div>
|
||||
{@render props.action?.()}
|
||||
</div>
|
||||
{#if $page.params.relay}
|
||||
<div class="text-xs text-primary md:hidden">
|
||||
{displayRelayUrl(decodeRelay($page.params.relay))}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,19 +5,14 @@
|
||||
interface Props {
|
||||
element?: Element
|
||||
children?: Snippet
|
||||
/** Desktop voice: chat occupies the right half in split view. */
|
||||
contentFrame?: "default" | "split-right"
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
let {children, element = $bindable(), contentFrame = "default", ...props}: Props = $props()
|
||||
let {children, element = $bindable(), ...props}: Props = $props()
|
||||
|
||||
const className = $derived(
|
||||
cx(
|
||||
props.class,
|
||||
"scroll-container cb ct fixed z-feature overflow-y-auto overflow-x-hidden",
|
||||
contentFrame === "split-right" ? "cw-split-chat" : "cw",
|
||||
),
|
||||
const className = cx(
|
||||
props.class,
|
||||
"scroll-container cw cb ct fixed z-feature overflow-y-auto overflow-x-hidden",
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import type {Snippet} from "svelte"
|
||||
|
||||
interface Props {
|
||||
class?: string
|
||||
children?: Snippet
|
||||
children?: import("svelte").Snippet
|
||||
}
|
||||
|
||||
const {children, ...props}: Props = $props()
|
||||
const {children}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cx(
|
||||
"ml-sai mt-sai mb-sai max-h-screen w-60 flex-shrink-0 flex-col gap-1 bg-base-300 z-nav hidden md:flex",
|
||||
props.class,
|
||||
)}>
|
||||
class="ml-sai mt-sai mb-sai hidden max-h-screen w-60 flex-shrink-0 flex-col gap-1 bg-base-300 md:flex">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{href}
|
||||
{...restProps}
|
||||
data-sveltekit-replacestate={replaceState}
|
||||
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="{restProps.class} relative flex items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content"
|
||||
class:text-base-content={active}
|
||||
class:bg-base-100={active}>
|
||||
{@render children?.()}
|
||||
@@ -45,7 +45,7 @@
|
||||
{:else}
|
||||
<button
|
||||
{...restProps}
|
||||
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="{restProps.class} relative flex w-full items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content"
|
||||
class:text-base-content={active}
|
||||
class:bg-base-100={active}>
|
||||
{#if notification}
|
||||
|
||||
+1
-12
@@ -115,18 +115,7 @@ export const compressFile = async (
|
||||
maxHeight: 2048,
|
||||
convertTypes: ["image/png"],
|
||||
...options,
|
||||
success: result => {
|
||||
// canvas.toBlob() returns a Blob, not a File. Capacitor's fetch interceptor
|
||||
// checks instanceof File to handle binary uploads correctly, so we must ensure
|
||||
// we always have a real File, not just a Blob with name/lastModified tacked on.
|
||||
const f =
|
||||
result instanceof File
|
||||
? result
|
||||
: new File([result], (result as any).name || (file as any).name || "upload", {
|
||||
type: result.type,
|
||||
})
|
||||
resolve(f)
|
||||
},
|
||||
success: result => resolve(result as File),
|
||||
error: e => {
|
||||
// Non-images break compressor, return the original file
|
||||
if (e.toString().includes("File or Blob")) {
|
||||
|
||||
+20
-6
@@ -1,32 +1,39 @@
|
||||
import {openDB, deleteDB} from "idb"
|
||||
import type {IDBPDatabase} from "idb"
|
||||
import type {Unsubscriber} from "svelte/store"
|
||||
import {call} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
|
||||
export type IDBStore = {
|
||||
export type IDBAdapter = {
|
||||
name: string
|
||||
keyPath: string
|
||||
init: (table: IDBTable<any>) => Promise<Unsubscriber>
|
||||
}
|
||||
|
||||
export type IDBAdapters = IDBAdapter[]
|
||||
|
||||
export type IDBOptions = {
|
||||
name: string
|
||||
version: number
|
||||
stores: IDBStore[]
|
||||
}
|
||||
|
||||
export class IDB {
|
||||
adapters: IDBAdapters = []
|
||||
connection: Maybe<Promise<IDBPDatabase>>
|
||||
unsubscribers: Maybe<Unsubscriber[]>
|
||||
failedToConnect = false
|
||||
|
||||
constructor(readonly options: IDBOptions) {}
|
||||
|
||||
async connect() {
|
||||
if (!this.failedToConnect && !this.connection) {
|
||||
const {name, version, stores} = this.options
|
||||
const {name, version} = this.options
|
||||
const adapters = this.adapters
|
||||
|
||||
try {
|
||||
this.connection = openDB(name, version, {
|
||||
upgrade(idbDb: IDBPDatabase) {
|
||||
const names = new Set(stores.map(store => store.name))
|
||||
const names = new Set(adapters.map(a => a.name))
|
||||
|
||||
for (const table of idbDb.objectStoreNames) {
|
||||
if (!names.has(table)) {
|
||||
@@ -34,7 +41,7 @@ export class IDB {
|
||||
}
|
||||
}
|
||||
|
||||
for (const {name, keyPath} of stores) {
|
||||
for (const {name, keyPath} of adapters) {
|
||||
try {
|
||||
idbDb.createObjectStore(name, {keyPath})
|
||||
} catch (e) {
|
||||
@@ -45,6 +52,10 @@ export class IDB {
|
||||
blocked() {},
|
||||
blocking() {},
|
||||
})
|
||||
|
||||
this.unsubscribers = await Promise.all(
|
||||
adapters.map(({name, init}) => init(this.table(name))),
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("Failed to connect to indexeddb", e)
|
||||
this.failedToConnect = true
|
||||
@@ -61,7 +72,7 @@ export class IDB {
|
||||
|
||||
if (!connection) return []
|
||||
|
||||
const tx = connection.transaction(table, "readonly")
|
||||
const tx = connection.transaction(table, "readwrite")
|
||||
const store = tx.objectStore(table)
|
||||
const result = await store.getAll()
|
||||
|
||||
@@ -104,6 +115,9 @@ export class IDB {
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.unsubscribers?.forEach(call)
|
||||
this.unsubscribers = undefined
|
||||
|
||||
this.connection?.then(c => c.close())
|
||||
this.connection = undefined
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
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,36 +19,6 @@ export const ucFirst = (s: string) => s.slice(0, 1).toUpperCase() + s.slice(1)
|
||||
|
||||
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[]) => {
|
||||
const url = new URL(base)
|
||||
|
||||
|
||||
@@ -126,12 +126,11 @@
|
||||
}),
|
||||
])
|
||||
|
||||
const storageSync = storage.sync()
|
||||
// Set up our storage adapters
|
||||
db.adapters = storage.adapters
|
||||
|
||||
unsubscribers.push(storageSync.unsubscribe)
|
||||
|
||||
// Wait for critical storage data only
|
||||
await storageSync.ready
|
||||
// Wait until data storage is initialized before syncing other stuff
|
||||
await db.connect()
|
||||
|
||||
// Close the database connection on reload
|
||||
unsubscribers.push(() => db.close())
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import Wallet from "@assets/icons/wallet.svg?dataurl"
|
||||
import Server from "@assets/icons/server.svg?dataurl"
|
||||
import Moon from "@assets/icons/moon.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import InfoSquare from "@assets/icons/info-square.svg?dataurl"
|
||||
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
||||
import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl"
|
||||
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
|
||||
@@ -73,7 +73,7 @@
|
||||
</div>
|
||||
<div in:fly|local={{delay: 350}}>
|
||||
<SecondaryNavItem href="/settings/about">
|
||||
<Icon icon={Code2} /> About
|
||||
<Icon icon={InfoSquare} /> About
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly|local={{delay: 400}}>
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
import {sleep} from "@welshman/lib"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {Badge} from "@capawesome/capacitor-badge"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {Push, clearBadges} from "@app/util/notifications"
|
||||
@@ -34,7 +32,7 @@
|
||||
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: `Failed to request notification permissions (${permissions}).`,
|
||||
message: "Failed to request notification permissions.",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -53,10 +51,7 @@
|
||||
|
||||
<form class="content column gap-4" {onsubmit}>
|
||||
<div class="card2 bg-alt col-4 shadow-md">
|
||||
<strong class="flex items-center gap-3 text-lg">
|
||||
<Icon icon={Bell} />
|
||||
Alert Settings
|
||||
</strong>
|
||||
<strong class="text-lg">Alert Settings</strong>
|
||||
{#await Badge.isSupported()}
|
||||
<!-- pass -->
|
||||
{:then { isSupported }}
|
||||
|
||||
@@ -10,12 +10,10 @@
|
||||
} from "@welshman/util"
|
||||
import {Router} from "@welshman/router"
|
||||
import {userMuteList, tagPubkey, publishThunk, userBlossomServerList} from "@welshman/app"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import InputList from "@lib/components/InputList.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
@@ -51,10 +49,7 @@
|
||||
|
||||
<form class="content column gap-4" {onsubmit}>
|
||||
<div class="card2 bg-alt col-4 shadow-md">
|
||||
<strong class="flex items-center gap-3 text-lg">
|
||||
<Icon icon={NotesMinimalistic} />
|
||||
Content Settings
|
||||
</strong>
|
||||
<strong class="text-lg">Content Settings</strong>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Hide sensitive content?</p>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ShieldMinimalistic from "@assets/icons/shield-minimalistic.svg?dataurl"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {PLATFORM_NAME, RelayAuthMode, userSettingsValues} from "@app/core/state"
|
||||
@@ -26,10 +24,7 @@
|
||||
|
||||
<form class="content column gap-4" {onsubmit}>
|
||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||
<strong class="flex items-center gap-3 text-lg">
|
||||
<Icon icon={ShieldMinimalistic} />
|
||||
Privacy Settings
|
||||
</strong>
|
||||
<strong class="text-lg">Privacy Settings</strong>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<p>Authenticate with unknown relays?</p>
|
||||
<input
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
{/if}
|
||||
<SignerStatus />
|
||||
{#if $session?.method === SessionMethod.Pomade}
|
||||
<div class="flex flex-col lg:flex-row gap-4 lg:gap-2 justify-end">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Button class="btn" onclick={startPasswordReset}>
|
||||
<Spinner {loading}>Update your password</Spinner>
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {derived} from "svelte/store"
|
||||
import {shuffle, partition, ifLet} from "@welshman/lib"
|
||||
import {
|
||||
pubkey,
|
||||
getRelayLists,
|
||||
@@ -12,182 +10,182 @@
|
||||
removeBlockedRelay,
|
||||
addMessagingRelay,
|
||||
removeMessagingRelay,
|
||||
addSearchRelay,
|
||||
removeSearchRelay,
|
||||
getRelay,
|
||||
setWriteRelays,
|
||||
setReadRelays,
|
||||
setSearchRelays,
|
||||
setMessagingRelays,
|
||||
} from "@welshman/app"
|
||||
import {RelayMode} from "@welshman/util"
|
||||
import Plane from "@assets/icons/plane.svg?dataurl"
|
||||
import Inbox from "@assets/icons/inbox.svg?dataurl"
|
||||
import Server from "@assets/icons/server.svg?dataurl"
|
||||
import Mailbox from "@assets/icons/mailbox.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import ForbiddenCircle from "@assets/icons/forbidden-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import RelaySettingsItem from "@app/components/RelaySettingsItem.svelte"
|
||||
import type {ActionItem} from "@app/components/RelaySettingsActionItem.svelte"
|
||||
import RelaySettingsActionItems from "@app/components/RelaySettingsActionItems.svelte"
|
||||
import Collapse from "@lib/components/Collapse.svelte"
|
||||
import RelayItem from "@app/components/RelayItem.svelte"
|
||||
import RelayAdd from "@app/components/RelayAdd.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {hasNip50, DEFAULT_RELAYS, DEFAULT_MESSAGING_RELAYS} from "@app/core/state"
|
||||
import {discoverRelays} from "@app/core/requests"
|
||||
import Globus from "@assets/icons/globus.svg?dataurl"
|
||||
import Inbox from "@assets/icons/inbox.svg?dataurl"
|
||||
import Mailbox from "@assets/icons/mailbox.svg?dataurl"
|
||||
import ForbiddenCircle from "@assets/icons/forbidden-circle.svg?dataurl"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
|
||||
const readRelayUrls = derivePubkeyRelays($pubkey!, RelayMode.Read)
|
||||
const writeRelayUrls = derivePubkeyRelays($pubkey!, RelayMode.Write)
|
||||
const searchRelayUrls = derivePubkeyRelays($pubkey!, RelayMode.Search)
|
||||
const blockedRelayUrls = derivePubkeyRelays($pubkey!, RelayMode.Blocked)
|
||||
const messagingRelayUrls = derivePubkeyRelays($pubkey!, RelayMode.Messaging)
|
||||
|
||||
const addReadRelay = (url: string) => addRelay(url, RelayMode.Read)
|
||||
const addReadRelays = () =>
|
||||
pushModal(RelayAdd, {
|
||||
relays: readRelayUrls,
|
||||
addRelay: (url: string) => addRelay(url, RelayMode.Read),
|
||||
})
|
||||
|
||||
const addWriteRelays = () =>
|
||||
pushModal(RelayAdd, {
|
||||
relays: writeRelayUrls,
|
||||
addRelay: (url: string) => addRelay(url, RelayMode.Write),
|
||||
})
|
||||
|
||||
const addBlockedRelays = () =>
|
||||
pushModal(RelayAdd, {relays: blockedRelayUrls, addRelay: addBlockedRelay})
|
||||
|
||||
const addMessagingRelays = () =>
|
||||
pushModal(RelayAdd, {relays: messagingRelayUrls, addRelay: addMessagingRelay})
|
||||
|
||||
const removeReadRelay = (url: string) => removeRelay(url, RelayMode.Read)
|
||||
const addWriteRelay = (url: string) => addRelay(url, RelayMode.Write)
|
||||
|
||||
const removeWriteRelay = (url: string) => removeRelay(url, RelayMode.Write)
|
||||
const showActionItems = () => pushModal(RelaySettingsActionItems, {actionItems})
|
||||
|
||||
const actionItems = derived(
|
||||
[readRelayUrls, writeRelayUrls, messagingRelayUrls, searchRelayUrls],
|
||||
([$readRelayUrls, $writeRelayUrls, $messagingRelayUrls, $searchRelayUrls]) => {
|
||||
const $actionItems: ActionItem[] = []
|
||||
|
||||
if ($readRelayUrls.length <= 1) {
|
||||
$actionItems.push({
|
||||
title: "Missing Inbox Relays",
|
||||
subtitle: "Other people aren't currently able to reliably tag you in public notes.",
|
||||
action: "Update",
|
||||
apply: () => setReadRelays(DEFAULT_RELAYS),
|
||||
})
|
||||
}
|
||||
|
||||
if ($writeRelayUrls.length <= 1) {
|
||||
$actionItems.push({
|
||||
title: "Missing Outbox Relays",
|
||||
subtitle: "Other people aren't currently able to reliably find your public notes.",
|
||||
action: "Update",
|
||||
apply: () => setWriteRelays(DEFAULT_RELAYS),
|
||||
})
|
||||
}
|
||||
|
||||
if ($messagingRelayUrls.length <= 1) {
|
||||
$actionItems.push({
|
||||
title: "Missing DM Relays",
|
||||
subtitle: "You aren't currently able to reliably send or receive direct messages.",
|
||||
action: "Update",
|
||||
apply: () => setMessagingRelays(DEFAULT_MESSAGING_RELAYS),
|
||||
})
|
||||
}
|
||||
|
||||
if ($readRelayUrls.length > 8) {
|
||||
$actionItems.push({
|
||||
title: "Too Many Inbox Relays",
|
||||
subtitle:
|
||||
"You have more inbox relays than is really necessary, which can affect resource usage.",
|
||||
action: "Prune Selections",
|
||||
apply: () => setReadRelays(shuffle($readRelayUrls).slice(0, 5)),
|
||||
})
|
||||
}
|
||||
|
||||
if ($writeRelayUrls.length > 8) {
|
||||
$actionItems.push({
|
||||
title: "Too Many Outbox Relays",
|
||||
subtitle:
|
||||
"You have more outbox relays than is really necessary, which can affect resource usage.",
|
||||
action: "Prune Selections",
|
||||
apply: () => setWriteRelays(shuffle($writeRelayUrls).slice(0, 5)),
|
||||
})
|
||||
}
|
||||
|
||||
if ($messagingRelayUrls.length > 8) {
|
||||
$actionItems.push({
|
||||
title: "Too Many DM Relays",
|
||||
subtitle:
|
||||
"You have more DM relays than is really necessary, which can affect resource usage.",
|
||||
action: "Prune Selections",
|
||||
apply: () => setMessagingRelays(shuffle($messagingRelayUrls).slice(0, 5)),
|
||||
})
|
||||
}
|
||||
|
||||
const [okSearchRelays, badSearchRelays] = partition(
|
||||
url => Boolean(ifLet(getRelay(url), hasNip50)),
|
||||
$searchRelayUrls,
|
||||
)
|
||||
|
||||
if (badSearchRelays.length > 0) {
|
||||
$actionItems.push({
|
||||
title: "Invalid Search Relays",
|
||||
subtitle: `Some of your search relays don't support search.`,
|
||||
action: "Remove Invalid",
|
||||
apply: () => setSearchRelays(okSearchRelays),
|
||||
})
|
||||
}
|
||||
|
||||
return $actionItems
|
||||
},
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
discoverRelays(getRelayLists())
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||
<div class="flex justify-between">
|
||||
<strong class="flex items-center gap-3 text-lg">
|
||||
<Icon icon={Server} />
|
||||
Your Relays
|
||||
</strong>
|
||||
{#if $actionItems.length > 0}
|
||||
<Button class="btn btn-neutral btn-sm" onclick={showActionItems}>
|
||||
<Icon icon={DangerTriangle} />
|
||||
{$actionItems.length} Issue{$actionItems.length === 1 ? "" : "s"} Detected
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="content column gap-4">
|
||||
<Collapse class="card2 bg-alt column gap-4 shadow-md">
|
||||
{#snippet title()}
|
||||
<h2 class="flex items-center gap-3 text-xl">
|
||||
<Icon icon={Globus} />
|
||||
Outbox Relays
|
||||
</h2>
|
||||
{/snippet}
|
||||
{#snippet description()}
|
||||
<p class="text-sm">
|
||||
These relays will be advertised on your profile as places where you send your public notes.
|
||||
Be sure to select relays that will accept your notes, and which will let people who follow
|
||||
you read them.
|
||||
</p>
|
||||
{/snippet}
|
||||
<div class="column gap-2">
|
||||
{#each $writeRelayUrls.sort() as url (url)}
|
||||
<RelayItem {url}>
|
||||
<Button
|
||||
class="tooltip flex items-center"
|
||||
data-tip="Stop using this relay"
|
||||
onclick={() => removeWriteRelay(url)}>
|
||||
<Icon icon={CloseCircle} />
|
||||
</Button>
|
||||
</RelayItem>
|
||||
{:else}
|
||||
<p class="text-center text-sm">No relays found</p>
|
||||
{/each}
|
||||
<Button class="btn btn-primary mt-2" onclick={addWriteRelays}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add Relay
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-sm mb-2">
|
||||
Relays are servers which store your data, or allow you to find data from across the Nostr
|
||||
network. We've set you up with some reasonable defaults, but if you're a power user, you can
|
||||
customize your relay selections below.
|
||||
</p>
|
||||
<RelaySettingsItem
|
||||
icon={Inbox}
|
||||
title="Inbox Relays"
|
||||
subtitle="Where you send your public notes. Be sure to select relays that will accept your notes, and which will let people who follow you read them."
|
||||
relays={readRelayUrls}
|
||||
addRelay={addReadRelay}
|
||||
removeRelay={removeReadRelay} />
|
||||
<RelaySettingsItem
|
||||
icon={Plane}
|
||||
title="Outbox Relays"
|
||||
subtitle="Where other people should send notes intended for you. Be sure to select relays that will accept notes that tag you."
|
||||
relays={writeRelayUrls}
|
||||
addRelay={addWriteRelay}
|
||||
removeRelay={removeWriteRelay} />
|
||||
<RelaySettingsItem
|
||||
icon={Mailbox}
|
||||
title="DM Relays"
|
||||
subtitle="Where you send and receive direct messages. Be sure to select relays that will accept your messages and messages from people you'd like to be in contact with."
|
||||
relays={messagingRelayUrls}
|
||||
addRelay={addMessagingRelay}
|
||||
removeRelay={removeMessagingRelay} />
|
||||
<RelaySettingsItem
|
||||
icon={Magnifier}
|
||||
title="Search Relays"
|
||||
subtitle="Relays that support searching for profiles and public notes."
|
||||
relays={searchRelayUrls}
|
||||
addRelay={addSearchRelay}
|
||||
removeRelay={removeSearchRelay}
|
||||
matchRelay={url => hasNip50(getRelay(url))} />
|
||||
<RelaySettingsItem
|
||||
icon={ForbiddenCircle}
|
||||
title="Blocked Relays"
|
||||
subtitle="These relays won't be used unless explicitly requested."
|
||||
relays={blockedRelayUrls}
|
||||
addRelay={addBlockedRelay}
|
||||
removeRelay={removeBlockedRelay} />
|
||||
</div>
|
||||
</Collapse>
|
||||
<Collapse class="card2 bg-alt column gap-4 shadow-md">
|
||||
{#snippet title()}
|
||||
<h2 class="flex items-center gap-3 text-xl">
|
||||
<Icon icon={Inbox} />
|
||||
Inbox Relays
|
||||
</h2>
|
||||
{/snippet}
|
||||
{#snippet description()}
|
||||
<p class="text-sm">
|
||||
These relays will be advertised on your profile as places where other people should send
|
||||
notes intended for you. Be sure to select relays that will accept notes that tag you.
|
||||
</p>
|
||||
{/snippet}
|
||||
<div class="column gap-2">
|
||||
{#each $readRelayUrls.sort() as url (url)}
|
||||
<RelayItem {url}>
|
||||
<Button
|
||||
class="tooltip flex items-center"
|
||||
data-tip="Stop using this relay"
|
||||
onclick={() => removeReadRelay(url)}>
|
||||
<Icon icon={CloseCircle} />
|
||||
</Button>
|
||||
</RelayItem>
|
||||
{:else}
|
||||
<p class="text-center text-sm">No relays found</p>
|
||||
{/each}
|
||||
<Button class="btn btn-primary mt-2" onclick={addReadRelays}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add Relay
|
||||
</Button>
|
||||
</div>
|
||||
</Collapse>
|
||||
<Collapse class="card2 bg-alt column gap-4 shadow-md">
|
||||
{#snippet title()}
|
||||
<h2 class="flex items-center gap-3 text-xl">
|
||||
<Icon icon={Mailbox} />
|
||||
Messaging Relays
|
||||
</h2>
|
||||
{/snippet}
|
||||
{#snippet description()}
|
||||
<p class="text-sm">
|
||||
These relays will be advertised on your profile as places you use to send and receive direct
|
||||
messages. Be sure to select relays that will accept your messages and messages from people
|
||||
you'd like to be in contact with.
|
||||
</p>
|
||||
{/snippet}
|
||||
<div class="column gap-2">
|
||||
{#each $messagingRelayUrls.sort() as url (url)}
|
||||
<RelayItem {url}>
|
||||
<Button
|
||||
class="tooltip flex items-center"
|
||||
data-tip="Stop using this relay"
|
||||
onclick={() => removeMessagingRelay(url)}>
|
||||
<Icon icon={CloseCircle} />
|
||||
</Button>
|
||||
</RelayItem>
|
||||
{:else}
|
||||
<p class="text-center text-sm">No relays found</p>
|
||||
{/each}
|
||||
<Button class="btn btn-primary mt-2" onclick={addMessagingRelays}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add Relay
|
||||
</Button>
|
||||
</div>
|
||||
</Collapse>
|
||||
<Collapse class="card2 bg-alt column gap-4 shadow-md">
|
||||
{#snippet title()}
|
||||
<h2 class="flex items-center gap-3 text-xl">
|
||||
<Icon icon={ForbiddenCircle} />
|
||||
Blocked Relays
|
||||
</h2>
|
||||
{/snippet}
|
||||
{#snippet description()}
|
||||
<p class="text-sm">
|
||||
These relays will never be connected to by clients supporting this policy.
|
||||
</p>
|
||||
{/snippet}
|
||||
<div class="column gap-2">
|
||||
{#each $blockedRelayUrls.sort() as url (url)}
|
||||
<RelayItem {url}>
|
||||
<Button
|
||||
class="tooltip flex items-center"
|
||||
data-tip="Stop using this relay"
|
||||
onclick={() => removeBlockedRelay(url)}>
|
||||
<Icon icon={CloseCircle} />
|
||||
</Button>
|
||||
</RelayItem>
|
||||
{:else}
|
||||
<p class="text-center text-sm">No relays found</p>
|
||||
{/each}
|
||||
<Button class="btn btn-primary mt-2" onclick={addBlockedRelays}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add Relay
|
||||
</Button>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
@@ -83,9 +83,9 @@
|
||||
<div class="content column gap-4">
|
||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-md">
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-3 text-lg">
|
||||
<strong class="flex items-center gap-3">
|
||||
<Icon icon={Wallet2} />
|
||||
Your Wallet
|
||||
Wallet
|
||||
</strong>
|
||||
{#if $session?.wallet}
|
||||
<div class={statusClass}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {insertAt, removeAt} from "@welshman/lib"
|
||||
import Planet from "@assets/icons/planet-3.svg?dataurl"
|
||||
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -96,18 +96,22 @@
|
||||
|
||||
<Page class="cw-full">
|
||||
<PageBar class="cw-full">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="ellipsize flex items-center gap-4 whitespace-nowrap">
|
||||
<Icon icon={Planet} />
|
||||
<strong>Your Spaces</strong>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon={SettingsMinimalistic} />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<strong>Your Spaces</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
{#if $userSpaceUrls.length > 0 && PLATFORM_RELAYS.length === 0}
|
||||
<Button class="btn btn-primary btn-sm" onclick={addSpace}>
|
||||
<Icon icon={AddCircle} />
|
||||
Add Space
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
<PageContent class="cw-full flex flex-col gap-2 p-2 pt-4">
|
||||
{#each PLATFORM_RELAYS as url (url)}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import SpaceAuthError from "@app/components/SpaceAuthError.svelte"
|
||||
import SpaceTrustRelay from "@app/components/SpaceTrustRelay.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {decodeRelay, relaysPendingTrust} from "@app/core/state"
|
||||
import {deriveRelayAuthError} from "@app/core/commands"
|
||||
|
||||
@@ -38,15 +37,11 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $page.url.pathname === makeSpacePath(url)}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
<SecondaryNav>
|
||||
<SpaceMenu {url} />
|
||||
</SecondaryNav>
|
||||
<Page>
|
||||
{#key $page.url.pathname}
|
||||
{@render children?.()}
|
||||
{/key}
|
||||
</Page>
|
||||
{/if}
|
||||
<SecondaryNav>
|
||||
<SpaceMenu {url} />
|
||||
</SecondaryNav>
|
||||
<Page>
|
||||
{#key $page.url.pathname}
|
||||
{@render children?.()}
|
||||
{/key}
|
||||
</Page>
|
||||
|
||||
@@ -1,31 +1,10 @@
|
||||
<script lang="ts">
|
||||
import {page} from "$app/stores"
|
||||
import theme from "tailwindcss/defaultTheme"
|
||||
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||
import {goto} from "$app/navigation"
|
||||
import {decodeRelay} from "@app/core/state"
|
||||
import {goToSpace} from "@app/util/routes"
|
||||
import PrimaryNavSpaces from "@app/components/PrimaryNavSpaces.svelte"
|
||||
import SpaceMenu from "@app/components/SpaceMenu.svelte"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
|
||||
const url = decodeRelay($page.params.relay!)
|
||||
const md = parseInt(theme.screens.md, 10)
|
||||
|
||||
let width = $state(0)
|
||||
|
||||
$effect(() => {
|
||||
if (width > md) {
|
||||
goToSpace(url)
|
||||
}
|
||||
})
|
||||
goto(makeSpacePath(url, "recent"))
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={width} />
|
||||
|
||||
{#if width <= md}
|
||||
<div class="ml-sai mt-sai mb-sai relative z-nav w-14 flex-shrink-0 bg-base-200 pt-4">
|
||||
<PrimaryNavSpaces />
|
||||
</div>
|
||||
<SecondaryNav class="!flex !min-h-0 !w-auto flex-grow pb-4">
|
||||
<SpaceMenu {url} />
|
||||
</SecondaryNav>
|
||||
{/if}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
<script>
|
||||
import {page} from "$app/stores"
|
||||
|
||||
type Props = {
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
const {children}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#key $page.url.searchParams.get("at")}
|
||||
{@render children?.()}
|
||||
<slot />
|
||||
{/key}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import {goto} from "$app/navigation"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} from "@welshman/app"
|
||||
import {now, ifLet, int, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
|
||||
import {now, int, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
|
||||
import type {MakeNonOptional} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {
|
||||
@@ -19,14 +19,13 @@
|
||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||
import InfoCircle from "@assets/icons/info-circle.svg?dataurl"
|
||||
import Login2 from "@assets/icons/login-3.svg?dataurl"
|
||||
import cx from "classnames"
|
||||
import {slide, fade, fly} from "@lib/transition"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import RoomCompose from "@app/components/RoomCompose.svelte"
|
||||
import RoomComposeParent from "@app/components/RoomComposeParent.svelte"
|
||||
import RoomImage from "@app/components/RoomImage.svelte"
|
||||
@@ -34,6 +33,7 @@
|
||||
import RoomItem from "@app/components/RoomItem.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import SpaceSearch from "@app/components/SpaceSearch.svelte"
|
||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
||||
import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
|
||||
@@ -43,16 +43,11 @@
|
||||
decodeRelay,
|
||||
deriveRoom,
|
||||
deriveUserRoomMembershipStatus,
|
||||
getRoomType,
|
||||
MESSAGE_KINDS,
|
||||
MembershipStatus,
|
||||
PROTECTED,
|
||||
RoomType,
|
||||
userSettingsValues,
|
||||
} from "@app/core/state"
|
||||
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||
import VideoCallContent from "@app/components/VideoCallContent.svelte"
|
||||
import {currentVoiceRoom, videoTileCount, voiceState} from "@app/voice"
|
||||
import {makeFeed} from "@app/core/requests"
|
||||
import {popKey} from "@lib/implicit"
|
||||
import {checked} from "@app/util/notifications"
|
||||
@@ -64,54 +59,6 @@
|
||||
const lastChecked = $checked[$page.url.pathname]
|
||||
const url = decodeRelay(relay)
|
||||
const room = deriveRoom(url, h)
|
||||
const isVoiceRoom = $derived(getRoomType($room) === RoomType.Voice)
|
||||
|
||||
const voiceConnectedHere = $derived(
|
||||
isVoiceRoom &&
|
||||
$voiceState === "connected" &&
|
||||
$currentVoiceRoom?.url === url &&
|
||||
$currentVoiceRoom?.h === h,
|
||||
)
|
||||
|
||||
let mobileRoomPanel = $state<"chat" | "video">("chat")
|
||||
let voiceDesktopPanel = $state<"chat" | "video" | "split">("split")
|
||||
|
||||
const showMobileVideoPanel = $derived(
|
||||
isVoiceRoom && $voiceState === "connected" && mobileRoomPanel === "video",
|
||||
)
|
||||
|
||||
const pageContentFrame = $derived<"default" | "split-right">(
|
||||
voiceConnectedHere && voiceDesktopPanel === "split" ? "split-right" : "default",
|
||||
)
|
||||
|
||||
const pageContentHiddenDesktopVideoOnly = $derived(
|
||||
voiceConnectedHere && voiceDesktopPanel === "video",
|
||||
)
|
||||
|
||||
let prevVideoTileCount = $state(0)
|
||||
|
||||
$effect(() => {
|
||||
if ($voiceState !== "connected") {
|
||||
mobileRoomPanel = "chat"
|
||||
voiceDesktopPanel = "chat"
|
||||
prevVideoTileCount = 0
|
||||
return
|
||||
}
|
||||
|
||||
const here = isVoiceRoom && $currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h
|
||||
const n = $videoTileCount
|
||||
|
||||
if (!here) {
|
||||
prevVideoTileCount = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (prevVideoTileCount === 0 && n >= 1) {
|
||||
voiceDesktopPanel = "video"
|
||||
mobileRoomPanel = "video"
|
||||
}
|
||||
prevVideoTileCount = n
|
||||
})
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
||||
const at = $derived(parseInt($page.url.searchParams.get("at")!))
|
||||
@@ -389,7 +336,13 @@
|
||||
eventToEdit = event
|
||||
}
|
||||
|
||||
const onEditPrevious = () => ifLet($events.toReversed().find(canEditEvent), onEditEvent)
|
||||
const onEditPrevious = () => {
|
||||
const prev = $events.toReversed().find(e => e.pubkey === $pubkey)
|
||||
|
||||
if (prev && canEditEvent(prev)) {
|
||||
onEditEvent(prev)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const observer = new ResizeObserver(() => {
|
||||
@@ -411,7 +364,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar>
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<RoomImage {url} {h} />
|
||||
{/snippet}
|
||||
@@ -419,57 +372,17 @@
|
||||
<RoomName {url} {h} />
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
{#if voiceConnectedHere}
|
||||
<div class="flex gap-1 md:hidden">
|
||||
<Button
|
||||
class={cx("btn btn-sm", mobileRoomPanel === "chat" && "btn-primary")}
|
||||
onclick={() => (mobileRoomPanel = "chat")}>
|
||||
Chat
|
||||
</Button>
|
||||
<Button
|
||||
class={cx("btn btn-sm", mobileRoomPanel === "video" && "btn-primary")}
|
||||
onclick={() => (mobileRoomPanel = "video")}>
|
||||
Video
|
||||
</Button>
|
||||
</div>
|
||||
<div class="hidden flex-wrap gap-1 md:flex">
|
||||
<Button
|
||||
data-tip="Messages only"
|
||||
class={cx("btn btn-sm", voiceDesktopPanel === "chat" && "btn-primary")}
|
||||
onclick={() => (voiceDesktopPanel = "chat")}>
|
||||
Chat
|
||||
</Button>
|
||||
<Button
|
||||
data-tip="Video only"
|
||||
class={cx("btn btn-sm", voiceDesktopPanel === "video" && "btn-primary")}
|
||||
onclick={() => (voiceDesktopPanel = "video")}>
|
||||
Video
|
||||
</Button>
|
||||
<Button
|
||||
data-tip="Video and chat side by side"
|
||||
class={cx("btn btn-sm", voiceDesktopPanel === "split" && "btn-primary")}
|
||||
onclick={() => (voiceDesktopPanel = "split")}>
|
||||
Video + Chat
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<SpaceSearch {url} {h} />
|
||||
<Button class="btn btn-neutral btn-sm btn-square" onclick={showRoomDetail}>
|
||||
<Icon size={4} icon={InfoCircle} />
|
||||
</Button>
|
||||
<div class="row-2 items-center">
|
||||
<SpaceSearch {url} {h} />
|
||||
<Button class="btn btn-neutral btn-sm btn-square" onclick={showRoomDetail}>
|
||||
<Icon size={4} icon={InfoCircle} />
|
||||
</Button>
|
||||
<SpaceMenuButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
</PageBar>
|
||||
|
||||
<PageContent
|
||||
bind:element
|
||||
onscroll={onScroll}
|
||||
contentFrame={pageContentFrame}
|
||||
class={cx(
|
||||
showMobileVideoPanel
|
||||
? "hidden flex-col-reverse pt-4 md:flex md:flex-col-reverse"
|
||||
: "flex flex-col-reverse pt-4",
|
||||
pageContentHiddenDesktopVideoOnly && "md:hidden",
|
||||
)}>
|
||||
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4">
|
||||
<div bind:this={dynamicPadding}></div>
|
||||
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
|
||||
<div class="py-20">
|
||||
@@ -541,75 +454,52 @@
|
||||
{/if}
|
||||
</PageContent>
|
||||
|
||||
{#if voiceConnectedHere}
|
||||
<VideoCallContent variant="desktop-split" {url} {h} visible={voiceDesktopPanel === "split"} />
|
||||
<VideoCallContent variant="desktop-full" {url} {h} visible={voiceDesktopPanel === "video"} />
|
||||
{/if}
|
||||
|
||||
{#if isVoiceRoom && $voiceState === "connected"}
|
||||
<VideoCallContent variant="mobile" {url} {h} visible={mobileRoomPanel === "video"} />
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class={cx(
|
||||
"chat__compose-zone flex flex-col gap-1 bg-base-200 md:flex-row md:gap-0",
|
||||
voiceConnectedHere && voiceDesktopPanel === "split" && "cw-split-chat",
|
||||
pageContentHiddenDesktopVideoOnly && "md:hidden",
|
||||
showMobileVideoPanel && "max-md:hidden",
|
||||
)}
|
||||
bind:this={chatCompose}>
|
||||
<div class="chat__compose-inner min-w-0 flex-1">
|
||||
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
|
||||
<!-- pass -->
|
||||
{:else if $room.isRestricted && $membershipStatus !== MembershipStatus.Granted}
|
||||
<div class="bg-alt card m-4 flex flex-row items-center justify-between px-4 py-3">
|
||||
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
||||
{#if !$room.isClosed}
|
||||
{#if $membershipStatus === MembershipStatus.Pending}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
||||
<Icon icon={ClockCircle} />
|
||||
Access Pending
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
||||
{#if joining}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<Icon icon={Login2} />
|
||||
{/if}
|
||||
Ask to Join
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
||||
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
|
||||
<!-- pass -->
|
||||
{:else if $room.isRestricted && $membershipStatus !== MembershipStatus.Granted}
|
||||
<div class="bg-alt card m-4 flex flex-row items-center justify-between px-4 py-3">
|
||||
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
||||
{#if !$room.isClosed}
|
||||
{#if $membershipStatus === MembershipStatus.Pending}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
||||
<Icon icon={ClockCircle} />
|
||||
Access Pending
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
||||
{#if joining}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<Icon icon={Login2} />
|
||||
{/if}
|
||||
Ask to Join
|
||||
</Button>
|
||||
{/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 />
|
||||
{/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>
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
import {EVENT_TIME, getTagValue} from "@welshman/util"
|
||||
import {fly} from "@lib/transition"
|
||||
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
|
||||
import Add from "@assets/icons/add.svg?dataurl"
|
||||
import CalendarAdd from "@assets/icons/calendar-add.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||
import CalendarEventItem from "@app/components/CalendarEventItem.svelte"
|
||||
import CalendarEventCreate from "@app/components/CalendarEventCreate.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
@@ -110,18 +111,25 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar>
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon={CalendarMinimalistic} />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<Icon icon={CalendarMinimalistic} />
|
||||
<strong>Calendar</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Button class="btn btn-primary btn-sm" onclick={makeEvent}>
|
||||
<Icon icon={Add} />
|
||||
Create
|
||||
</Button>
|
||||
<div class="row-2">
|
||||
<Button class="btn btn-primary btn-sm" onclick={makeEvent}>
|
||||
<Icon icon={CalendarAdd} />
|
||||
Create an Event
|
||||
</Button>
|
||||
<SpaceMenuButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
</PageBar>
|
||||
|
||||
<PageContent bind:element class="flex flex-col gap-2 p-2 pt-4">
|
||||
{#each items as { event, dateDisplay, isFirstFutureEvent }, i (event.id)}
|
||||
|
||||
@@ -7,16 +7,18 @@
|
||||
import {request} from "@welshman/net"
|
||||
import {repository} from "@welshman/app"
|
||||
import {deriveEventsById, deriveEventsAsc} from "@welshman/store"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import SortVertical from "@assets/icons/sort-vertical.svg?dataurl"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
import CalendarEventMeta from "@app/components/CalendarEventMeta.svelte"
|
||||
@@ -58,11 +60,22 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar {back}>
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
<Button class="btn btn-neutral btn-sm flex-nowrap whitespace-nowrap" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
<span class="hidden sm:inline">Go back</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<h1 class="text-xl">{getTagValue("title", $event?.tags || []) || ""}</h1>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
{#snippet action()}
|
||||
<SpaceMenuButton {url} />
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
|
||||
<PageContent class="flex flex-col gap-3 p-2 pt-4">
|
||||
{#if $event}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {goto} from "$app/navigation"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {readable} from "svelte/store"
|
||||
import {now, int, ifLet, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib"
|
||||
import {now, int, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER} from "@welshman/util"
|
||||
import {pubkey, publishThunk} from "@welshman/app"
|
||||
@@ -14,11 +14,12 @@
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import SpaceSearch from "@app/components/SpaceSearch.svelte"
|
||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||
import RoomItem from "@app/components/RoomItem.svelte"
|
||||
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
||||
import RoomItemRemoveMember from "@src/app/components/RoomItemRemoveMember.svelte"
|
||||
@@ -271,7 +272,13 @@
|
||||
eventToEdit = event
|
||||
}
|
||||
|
||||
const onEditPrevious = () => ifLet($events.toReversed().find(canEditEvent), onEditEvent)
|
||||
const onEditPrevious = () => {
|
||||
const prev = $events.toReversed().find(e => e.pubkey === $pubkey)
|
||||
|
||||
if (prev && canEditEvent(prev)) {
|
||||
onEditEvent(prev)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const controller = new AbortController()
|
||||
@@ -295,15 +302,22 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar>
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon={ChatRound} />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<Icon icon={ChatRound} />
|
||||
<strong>Chat</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<SpaceSearch {url} />
|
||||
<div class="row-2 items-center">
|
||||
<SpaceSearch {url} />
|
||||
<SpaceMenuButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
</PageBar>
|
||||
|
||||
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4">
|
||||
<div bind:this={dynamicPadding}></div>
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {CLASSIFIED, getTagValue} from "@welshman/util"
|
||||
import {fly} from "@lib/transition"
|
||||
import CaseMinimalistic from "@assets/icons/case-minimalistic.svg?dataurl"
|
||||
import Add from "@assets/icons/add.svg?dataurl"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||
import ClassifiedItem from "@app/components/ClassifiedItem.svelte"
|
||||
import ClassifiedCreate from "@app/components/ClassifiedCreate.svelte"
|
||||
import {decodeRelay} from "@app/core/state"
|
||||
@@ -62,18 +62,25 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar>
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon={NotesMinimalistic} />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<Icon icon={CaseMinimalistic} />
|
||||
<strong>Classifieds</strong>
|
||||
<strong>Classified Listings</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Button class="btn btn-primary btn-sm" onclick={createClassified}>
|
||||
<Icon icon={Add} />
|
||||
Create
|
||||
</Button>
|
||||
<div class="row-2">
|
||||
<Button class="btn btn-primary btn-sm" onclick={createClassified}>
|
||||
<Icon icon={NotesMinimalistic} />
|
||||
Create a Listing
|
||||
</Button>
|
||||
<SpaceMenuButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
</PageBar>
|
||||
|
||||
<PageContent bind:element class="flex flex-col gap-2 p-2 pt-4">
|
||||
{#each items as event (event.id)}
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
import {repository} from "@welshman/app"
|
||||
import {request} from "@welshman/net"
|
||||
import {deriveEventsById, deriveEventsAsc} from "@welshman/store"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import SortVertical from "@assets/icons/sort-vertical.svg?dataurl"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
|
||||
import CommentActions from "@app/components/CommentActions.svelte"
|
||||
import EventReply from "@app/components/EventReply.svelte"
|
||||
@@ -55,11 +57,24 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar {back}>
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
<Button class="btn btn-neutral btn-sm flex-nowrap whitespace-nowrap" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
<span class="hidden sm:inline">Go back</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<h1 class="text-xl">{getTagValue("title", $event?.tags || []) || ""}</h1>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
{#snippet action()}
|
||||
<div>
|
||||
<SpaceMenuButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
|
||||
<PageContent class="flex flex-col p-2 pt-4">
|
||||
{#if $event}
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {ZAP_GOAL, getTagValue} from "@welshman/util"
|
||||
import {fly} from "@lib/transition"
|
||||
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic.svg?dataurl"
|
||||
import Add from "@assets/icons/add.svg?dataurl"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||
import GoalItem from "@app/components/GoalItem.svelte"
|
||||
import GoalCreate from "@app/components/GoalCreate.svelte"
|
||||
import {decodeRelay, makeCommentFilter} from "@app/core/state"
|
||||
@@ -61,18 +61,25 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar>
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon={NotesMinimalistic} />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<Icon icon={StarFallMinimalistic} />
|
||||
<strong>Goals</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Button class="btn btn-primary btn-sm" onclick={createGoal}>
|
||||
<Icon icon={Add} />
|
||||
Create
|
||||
</Button>
|
||||
<div class="row-2">
|
||||
<Button class="btn btn-primary btn-sm" onclick={createGoal}>
|
||||
<Icon icon={NotesMinimalistic} />
|
||||
Create a Goal
|
||||
</Button>
|
||||
<SpaceMenuButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
</PageBar>
|
||||
|
||||
<PageContent bind:element class="flex flex-col gap-2 p-2 pt-4">
|
||||
{#each items as event (event.id)}
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
import {repository} from "@welshman/app"
|
||||
import {request} from "@welshman/net"
|
||||
import {deriveEventsById, deriveEventsAsc} from "@welshman/store"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import SortVertical from "@assets/icons/sort-vertical.svg?dataurl"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||
import GoalActions from "@app/components/GoalActions.svelte"
|
||||
import CommentActions from "@app/components/CommentActions.svelte"
|
||||
import EventReply from "@app/components/EventReply.svelte"
|
||||
@@ -56,11 +58,24 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar {back}>
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
<Button class="btn btn-neutral btn-sm flex-nowrap whitespace-nowrap" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
<span class="hidden sm:inline">Go back</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<h1 class="text-xl">{$event?.content}</h1>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
{#snippet action()}
|
||||
<div>
|
||||
<SpaceMenuButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
|
||||
<PageContent class="flex flex-col p-2 pt-4">
|
||||
{#if $event}
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
import History from "@assets/icons/history.svg?dataurl"
|
||||
import {createScroller} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||
import NoteItem from "@app/components/NoteItem.svelte"
|
||||
import ThreadItem from "@app/components/ThreadItem.svelte"
|
||||
import ClassifiedItem from "@app/components/ClassifiedItem.svelte"
|
||||
@@ -30,7 +31,7 @@
|
||||
import {decodeRelay, deriveEventsForUrl, CONTENT_KINDS} from "@app/core/state"
|
||||
|
||||
const url = decodeRelay($page.params.relay!)
|
||||
const since = ago(3, MONTH)
|
||||
const since = ago(MONTH)
|
||||
|
||||
const messages = deriveEventsForUrl(url, [{kinds: [MESSAGE], since}])
|
||||
const content = deriveEventsForUrl(url, [{kinds: CONTENT_KINDS, since}])
|
||||
@@ -103,12 +104,21 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar>
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon={History} />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<Icon icon={History} />
|
||||
<strong>Recent Activity</strong>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
{#snippet action()}
|
||||
<div class="row-2">
|
||||
<SpaceMenuButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
|
||||
<div bind:this={element}>
|
||||
<PageContent class="flex flex-col gap-2 p-2 pt-4">
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
import {THREAD, getTagValue} from "@welshman/util"
|
||||
import {fly} from "@lib/transition"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
import Add from "@assets/icons/add.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||
import ThreadItem from "@app/components/ThreadItem.svelte"
|
||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||
import {decodeRelay} from "@app/core/state"
|
||||
@@ -62,18 +62,25 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar>
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div class="center">
|
||||
<Icon icon={NotesMinimalistic} />
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<Icon icon={NotesMinimalistic} />
|
||||
<strong>Threads</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Button class="btn btn-sm btn-primary" onclick={createThread}>
|
||||
<Icon icon={Add} />
|
||||
Create
|
||||
</Button>
|
||||
<div class="row-2">
|
||||
<Button class="btn btn-primary btn-sm" onclick={createThread}>
|
||||
<Icon icon={NotesMinimalistic} />
|
||||
Create a Thread
|
||||
</Button>
|
||||
<SpaceMenuButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
</PageBar>
|
||||
|
||||
<PageContent bind:element class="flex flex-col gap-2 p-2 pt-4">
|
||||
{#each items as event (event.id)}
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
import {repository} from "@welshman/app"
|
||||
import {request} from "@welshman/net"
|
||||
import {deriveEventsById, deriveEventsAsc} from "@welshman/store"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import SortVertical from "@assets/icons/sort-vertical.svg?dataurl"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||
import ThreadActions from "@app/components/ThreadActions.svelte"
|
||||
import CommentActions from "@app/components/CommentActions.svelte"
|
||||
import EventReply from "@app/components/EventReply.svelte"
|
||||
@@ -55,11 +57,24 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar {back}>
|
||||
<PageBar>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
<Button class="btn btn-neutral btn-sm flex-nowrap whitespace-nowrap" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
<span class="hidden sm:inline">Go back</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<h1 class="text-xl">{getTagValue("title", $event?.tags || []) || ""}</h1>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
{#snippet action()}
|
||||
<div>
|
||||
<SpaceMenuButton {url} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
|
||||
<PageContent class="flex flex-col p-2 pt-4">
|
||||
{#if $event}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+11
-11
@@ -1,13 +1,13 @@
|
||||
identifier: social.flotilla
|
||||
name: Flotilla
|
||||
tags: nostr nip29 community chat group
|
||||
changelog: CHANGELOG.md
|
||||
homepage: https://flotilla.social
|
||||
description: Self-hosted community chat and threads built on the nostr protocol.
|
||||
tags:
|
||||
- nostr
|
||||
- nip29
|
||||
- community
|
||||
- chat
|
||||
- group
|
||||
license: MIT
|
||||
website: https://flotilla.social
|
||||
repository: https://gitea.coracle.social/coracle/flotilla
|
||||
release_source: ./android/app/build/outputs/apk/release/app-release-signed.apk
|
||||
release_notes: ./CHANGELOG.md
|
||||
repository: https://github.com/coracle-social/flotilla
|
||||
blossom_servers:
|
||||
- https://cdn.zapstore.dev
|
||||
- https://hbr.coracle.social
|
||||
assets:
|
||||
- app-release-signed.apk
|
||||
|
||||
|
||||
Reference in New Issue
Block a user