Compare commits

..

69 Commits

Author SHA1 Message Date
Jon Staab c691400630 Update version 2026-03-09 16:46:47 -07:00
Jon Staab 6acbfb1181 Fix classified images 2026-03-06 15:01:15 -08:00
Jon Staab 5f7474140f Add StringMultiInput for OTPs 2026-03-06 15:00:17 -08:00
Jon Staab c56d6f4c75 Fix up edit 2026-03-06 14:46:04 -08:00
Jon Staab c874ae50e5 Handle prompt-with-rationale 2026-03-06 14:40:58 -08:00
Jon Staab 25e7cc97f9 Handle profile update errors 2026-03-06 14:18:05 -08:00
Jon Staab 837e4bc537 Move from .env to .env.local 2026-03-06 13:58:03 -08:00
Jon Staab 65fa93d853 Tweak some error messages 2026-03-06 13:37:16 -08:00
Jon Staab 28b0276f17 Refactor pomade, add password reset flow 2026-03-06 11:48:57 -08:00
Jon Staab 10ac15f8a2 Add LogInSelect step for disambiguating pomade sessions 2026-03-06 09:38:36 -08:00
Jon Staab a45633e214 Update pomade implementation 2026-03-05 17:12:40 -08:00
mplorentz a42ba5446a Fix a docker rebuild issue (#88)
The Docker build wasn't making use of docker's cache because the .git directory was being copied into the build context. This means that even if the app did not change, if anything in git changed then docker would rebuild the entire app.

This excludes the .git folder from the docker build, instead relying on the user to pass in the build hash at build time. Which is annoying but I don't think there's a better way around it.

This was annoying me because I am deploying a self-hosted version of flotilla from a git branch via ansible and it was rebuilding flotilla every time.

Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social>
Reviewed-on: coracle/flotilla#88
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-03-03 19:52:22 +00:00
Jon Staab ccfe1bded5 Bring back some notification badges 2026-02-27 12:25:16 -08:00
Jon Staab dfedf4e879 Make sync logic more robust 2026-02-26 14:51:31 -08:00
Jon Staab 0682c404f2 Bump welshman 2026-02-26 13:43:31 -08:00
Jon Staab 80ece70450 Support unban/unallow 2026-02-26 13:35:13 -08:00
Jon Staab f6245c712d Fix WalletPay 2026-02-26 13:06:34 -08:00
Jon Staab e0c5f0d4f1 Bump welshman 2026-02-25 15:50:14 -08:00
Jon Staab b616e2ea33 Blobify images so users can open them easier 2026-02-25 14:52:05 -08:00
Jon Staab 6fb6995103 Add manual invoice payment 2026-02-25 14:25:59 -08:00
Jon Staab 47bc0c2382 Update link_deps 2026-02-25 13:14:19 -08:00
Jon Staab 59a919d888 Fix enter selecting an option when there is no term. Closes #84 2026-02-25 10:01:08 -08:00
triesap 17d673c288 Bootstrap Tauri desktop shell for evaluation (#66)
Adds a minimal Tauri desktop bootstrap. Run with: pnpm run tauri:dev

Reviewed-on: coracle/flotilla#66
Co-authored-by: triesap <tyson@radroots.org>
Co-committed-by: triesap <tyson@radroots.org>
2026-02-25 00:23:16 +00:00
triesap 985fd46243 Classifieds tags (#18) (#65)
Closes #18

Reviewed-on: coracle/flotilla#65
Co-authored-by: triesap <tyson@radroots.org>
Co-committed-by: triesap <tyson@radroots.org>
2026-02-25 00:09:50 +00:00
triesap 6d99e296e4 Show wallet status when wallet is unreachable 2026-02-24 15:45:29 -08:00
Jon Staab 21c34efb6a Remove capacitor plugin from overrides 2026-02-24 10:45:43 -08:00
Jon Staab 9c2f923c26 Bump welshman and pomade 2026-02-23 15:38:45 -08:00
Jon Staab 52d2d70838 Update nostr signer capacitor plugin 2026-02-23 15:15:59 -08:00
Jon Staab edd8824c5e Fix safe area inset for modal footer 2026-02-23 14:40:00 -08:00
Jon Staab 4f12ad9533 Bump nip55 signer 2026-02-21 11:18:38 -08:00
Jon Staab 2a5850e67f Update pomade version 2026-02-20 14:24:15 -08:00
Jon Staab 15341edece Fix svgs with 302 redirects on safari 2026-02-19 12:59:03 -08:00
Jon Staab 30f8b4160e Fix mask-repeat property 2026-02-19 12:26:18 -08:00
Jon Staab 937ca5ecf6 Refine space join dialogs and discover page 2026-02-19 11:20:46 -08:00
mplorentz ba1757d4f1 Reopen the last DM that was open when navigating back to chat (#81)
#60

Co-authored-by: mplorentz <mplorentz@users.noreply.github.com>
Reviewed-on: coracle/flotilla#81
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-02-19 18:31:54 +00:00
Jon Staab 5a2b5f43b8 Add content-visibility class 2026-02-18 16:37:55 -08:00
Jon Staab 2f487705c3 Get rid of ChatEnable, automatically enable unwrapping when the user first visits the dms page. Closes #72 2026-02-18 16:02:49 -08:00
Jon Staab 558d59ce88 Enable auth for relays we're publishing to 2026-02-18 15:54:14 -08:00
triesap 1030edd322 Drag and drop space icons (#17) (#78)
Closes #17

Co-authored-by: Jon Staab <shtaab@gmail.com>
Reviewed-on: coracle/flotilla#78
Co-authored-by: triesap <tyson@radroots.org>
Co-committed-by: triesap <tyson@radroots.org>
2026-02-18 23:03:08 +00:00
Jon Staab 981c8fd706 re-order some menu items 2026-02-18 14:50:39 -08:00
Jon Staab 45ade602b5 Fix iOS zoom bug 2026-02-18 12:49:20 -08:00
Jon Staab ef8a8682cd Reset to old home page 2026-02-18 11:02:39 -08:00
Jon Staab 112ac4b6d5 Continue working on feed page 2026-02-18 11:01:28 -08:00
Jon Staab 3a26d2cb0b Add better muting, add EventReducer 2026-02-18 10:22:23 -08:00
mplorentz a678bf42f1 Add back button to settings menu 2026-02-18 17:06:07 +00:00
Jon Staab dc314a1d1b Work on feed page 2026-02-17 17:37:19 -08:00
Jon Staab 3af56f6bb1 Prevent error loop on images 2026-02-17 13:45:00 -08:00
triesap a996664e6c Page titles (#16) (#62)
Closes #16

Reviewed-on: coracle/flotilla#62
Co-authored-by: triesap <tyson@radroots.org>
Co-committed-by: triesap <tyson@radroots.org>
2026-02-17 20:39:08 +00:00
Jon Staab 6e865fef06 tweak how at works 2026-02-17 10:45:50 -08:00
Jon Staab 588bd0f341 Fix scroll to event behavior 2026-02-17 10:45:50 -08:00
Jon Staab 69f6abf4b6 Pin scroll position to at'd event until user scrolls 2026-02-17 10:45:50 -08:00
Jon Staab c8eb4ac31a Simplify goToEvent 2026-02-17 10:45:50 -08:00
Jon Staab e3e69390ce Add forward scrolling to makeMakeFeed 2026-02-17 10:45:49 -08:00
Jon Staab d0b34dfdf8 Make createScroller honor reverse param 2026-02-17 10:45:49 -08:00
Jon Staab bcdb3dc351 Fix duplicate ids in chat 2026-02-17 10:45:49 -08:00
Jon Staab a7b0031b8d Use compressorjs-next 2026-02-17 10:45:49 -08:00
Jon Staab 2c05bc6961 Refactor SpaceSearch into its own component 2026-02-17 10:45:49 -08:00
Ben c2d0ec92bf Space search 2026-02-17 10:45:49 -08:00
Jon Staab 407b4dce94 Fix editing messages with html tags 2026-02-17 10:45:49 -08:00
Jon Staab 796157384f Fix DM media detection 2026-02-17 10:45:49 -08:00
Jon Staab 3446977df6 Make hover target for menu button more reasonable 2026-02-17 10:45:49 -08:00
Jon Staab f8016aba99 Watch tracker in feed utils 2026-02-17 10:45:49 -08:00
Jon Staab 56d8527ed9 Revert makeFeed changes 2026-02-17 10:45:49 -08:00
Jon Staab 302788bcba Clean up report item design, bad/restore user actions, space description input, add feed to home page 2026-02-17 10:45:49 -08:00
Jon Staab db075e602a Tweak wallet page 2026-02-17 10:45:49 -08:00
Jon Staab 67011d4740 Fix makeFeed (maybe) 2026-02-17 10:45:49 -08:00
Jon Staab a35d867b34 Tweak room detail 2026-02-17 10:45:49 -08:00
Jon Staab 23b59e54d7 Fix scroll to bottom button safe insets 2026-02-17 10:45:49 -08:00
Jon Staab da2665d2bc Disable wallet on ios 2026-02-17 10:45:49 -08:00
97 changed files with 1259 additions and 3030 deletions
+2 -2
View File
@@ -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=
+1 -1
View File
@@ -6,7 +6,7 @@ on:
env:
REGISTRY: ghcr.io
IMAGE_NAME: coracle-social/flotilla
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
-1
View File
@@ -25,7 +25,6 @@ android/app/src/main/assets/public/
# Web/JavaScript
node_modules/
.pnpm-store/
build/
.svelte-kit/
-5
View File
@@ -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
View File
@@ -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"]
+2 -2
View File
@@ -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:
-2
View File
@@ -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>
-3
View File
@@ -4,9 +4,6 @@ const config: CapacitorConfig = {
appId: "social.flotilla",
appName: "Flotilla",
webDir: "build",
ios: {
scheme: "Flotilla Chat",
},
android: {
adjustMarginsForEdgeToEdge: true,
},
+6 -8
View File
@@ -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
View File
@@ -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",
+111 -199
View File
@@ -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
View File
@@ -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;
}
+3 -2
View File
@@ -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
View File
@@ -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 -29
View File
@@ -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)}>
-21
View File
@@ -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>
+4 -7
View File
@@ -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">
+1 -8
View File
@@ -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
-17
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+108 -54
View File
@@ -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>
+13 -21
View File
@@ -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},
},
+1 -1
View File
@@ -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>
+40 -6
View File
@@ -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>
+2 -7
View File
@@ -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>
+1 -1
View File
@@ -6,7 +6,7 @@
import ImageIcon from "@lib/components/ImageIcon.svelte"
type Props = {
pubkey?: string
pubkey: string
class?: string
size?: number
url?: string
+13 -65
View File
@@ -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>
-85
View File
@@ -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>
+2 -2
View File
@@ -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
+2 -34
View File
@@ -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} />
+3 -22
View File
@@ -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}
+1 -1
View File
@@ -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>
-44
View File
@@ -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>
+2 -2
View File
@@ -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>
+4 -6
View File
@@ -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>
+15 -27
View File
@@ -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>
+27
View File
@@ -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>
+12 -19
View File
@@ -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>
-247
View File
@@ -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}
-31
View File
@@ -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>
-91
View File
@@ -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>
-100
View File
@@ -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}
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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,
}),
),
)
})
-7
View File
@@ -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()
}
-1
View File
@@ -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
View File
@@ -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")
}
+6 -4
View File
@@ -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) {
+5 -5
View File
@@ -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
View File
@@ -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,
]
-442
View File
@@ -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"})
}
}
-8
View File
@@ -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

+36
View File
@@ -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>
-9
View File
@@ -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>
+18 -6
View File
@@ -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}
+20 -4
View File
@@ -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>
+4 -9
View File
@@ -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>
+3 -10
View File
@@ -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>
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
-23
View File
@@ -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)
-30
View File
@@ -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)
+4 -5
View File
@@ -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())
+2 -2
View File
@@ -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}}>
+2 -7
View File
@@ -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 }}
+1 -6
View File
@@ -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 -6
View File
@@ -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
+1 -1
View File
@@ -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>
+157 -159
View File
@@ -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>
+2 -2
View File
@@ -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}>
+10 -6
View File
@@ -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 -13
View File
@@ -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>
+3 -24
View File
@@ -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}
+2 -9
View File
@@ -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}
+64 -174
View File
@@ -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}
+21 -7
View File
@@ -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}
+17 -10
View File
@@ -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}
+15 -5
View File
@@ -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">
+16 -9
View File
@@ -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
View File
@@ -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