forked from coracle/flotilla
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bae956ffa | |||
| a2f59a5b1b | |||
| df56af9b0e | |||
| 83f7f9584f | |||
| a2d440e54f | |||
| 4132e8449b | |||
| ee444416e4 | |||
| 10c12c3c48 | |||
| db3775ae99 | |||
| 393acce884 | |||
| 68fe663730 | |||
| f65a4b0db0 | |||
| cdfb502e6e | |||
| 1a2c83e49b | |||
| e6c7a675a9 | |||
| 69c04f29f4 | |||
| 04c6f9b4fe | |||
| 86ec12a9db | |||
| 72b3111c64 | |||
| 6709c91779 | |||
| bb6e7495f5 | |||
| df17929681 | |||
| e083719ceb | |||
| bfdc69f18c | |||
| e7ae20afb7 | |||
| 229d92055f | |||
| 64c77cfd13 | |||
| 3a63894562 | |||
| 1d272f8b37 | |||
| bac433b640 | |||
| 62f573eac0 | |||
| b3ea62c53c | |||
| b0731503a8 | |||
| 2421c02c24 | |||
| 25e868118d | |||
| 2880044e0e | |||
| 5300404b46 | |||
| d949d58076 | |||
| 997b223e95 | |||
| ba52a97e26 | |||
| cc4c7b5fe9 | |||
| 8e2ebd11fc | |||
| 9cae4da9f4 | |||
| c05d7e99e2 | |||
| 2390599e8f | |||
| 1a4d45fa9c | |||
| 57447e5bf4 | |||
| 8e411daaef | |||
| 183aebf841 | |||
| e3e500ccc2 | |||
| e7a2535ece | |||
| 761e369313 | |||
| 5248275d73 | |||
| cb033279dd | |||
| 41d50d8c28 | |||
| a52c2b4c3c | |||
| b5917cb184 | |||
| 57348472f8 | |||
| 4b6223dc00 |
@@ -1,5 +1,50 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 1.6.2
|
||||||
|
|
||||||
|
* Fix modal scrolling and style
|
||||||
|
|
||||||
|
# 1.6.1
|
||||||
|
|
||||||
|
* Fix skinny profile images
|
||||||
|
* Custom handler for relay urls
|
||||||
|
* Improve time based chat partitioning
|
||||||
|
* Improve authenticated image access interop
|
||||||
|
* Fix image detail dialog
|
||||||
|
* Fix zapper loading
|
||||||
|
* Fix recent events missing in feeds
|
||||||
|
|
||||||
|
# 1.6.0
|
||||||
|
|
||||||
|
* Switch back to indexeddb to fix memory and performance
|
||||||
|
* Add pay invoice functionality
|
||||||
|
* Add space membership management and bans
|
||||||
|
* Add event info to profile dialog
|
||||||
|
* Add better room membership management
|
||||||
|
* Refactor stores for performance
|
||||||
|
* Hide nav when keyboard is open
|
||||||
|
* Handle flotilla links in-app
|
||||||
|
* Fix new messages indicator z-index
|
||||||
|
* Fix some display bugs
|
||||||
|
* Add date to chat items
|
||||||
|
* Refine data synchronization
|
||||||
|
* Hide nav when keyboard is open on mobile
|
||||||
|
|
||||||
|
# 1.5.3
|
||||||
|
|
||||||
|
* Add space edit form
|
||||||
|
* Improve room syncing
|
||||||
|
* Return better blossom errors
|
||||||
|
* Fix access restricted bugs
|
||||||
|
* Add room detail dialog
|
||||||
|
* Fix broken link to self hosting
|
||||||
|
* Tweak shadows
|
||||||
|
* Always join spaces when visiting them
|
||||||
|
|
||||||
|
# 1.5.2
|
||||||
|
|
||||||
|
* Fix negentropy room syncing
|
||||||
|
|
||||||
# 1.5.1
|
# 1.5.1
|
||||||
|
|
||||||
* Fix chat path link
|
* Fix chat path link
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
# Flotilla - AI Assistant Context
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Flotilla is a Discord-like Nostr client based on the concept of "relays as groups". It's built with SvelteKit, TypeScript, and Capacitor for cross-platform support (web, Android, iOS).
|
||||||
|
|
||||||
|
On boot, please run `tree -I assets src` to get an idea of the project structure.
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
|
||||||
|
`@welshman/*` libraries contain the majority of nostr-related functionality.
|
||||||
|
`@app/core/*` contains additional app-specific data stores and commands.
|
||||||
|
|
||||||
|
When creating an import statement, first identify what functionality you need. Search the codebase for components with similar functionality, and imitate their imports.
|
||||||
|
|
||||||
|
## Dependency Graph (Acyclic)
|
||||||
|
|
||||||
|
The project follows a strict dependency hierarchy:
|
||||||
|
1. **External libraries** (bottom layer)
|
||||||
|
2. **`lib/`** - Only depends on external libraries
|
||||||
|
3. **`app/core/`** and **`app/util/`** - Can depend on `lib` only
|
||||||
|
4. **`app/components/`** - Can depend on anything in `app` or `lib`
|
||||||
|
5. **`routes/`** - Can depend on anything (top layer)
|
||||||
|
|
||||||
|
**Import Ordering Convention:** Always sort imports by dependency level:
|
||||||
|
1. Third-party libraries first
|
||||||
|
2. Then `lib` imports
|
||||||
|
3. Then `app` imports
|
||||||
|
|
||||||
|
## Development Conventions
|
||||||
|
|
||||||
|
When creating components related to a given space or room, parameterize them only with the entity's identifier (i.e., `url` and `h`). Only pass additional props if they can't be derived from the identifiers. For example, a room's `members` should be derived inside the child component, not passed in by the parent.
|
||||||
|
|
||||||
|
Do not use null, only undefined.
|
||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "social.flotilla"
|
applicationId "social.flotilla"
|
||||||
minSdk rootProject.ext.minSdkVersion
|
minSdk rootProject.ext.minSdkVersion
|
||||||
targetSdk rootProject.ext.targetSdkVersion
|
targetSdk rootProject.ext.targetSdkVersion
|
||||||
versionCode 33
|
versionCode 38
|
||||||
versionName "1.5.2"
|
versionName "1.6.2"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -358,14 +358,14 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 24;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.5.2;
|
MARKETING_VERSION = 1.6.2;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -384,14 +384,14 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 24;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.5.2;
|
MARKETING_VERSION = 1.6.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
+12
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "1.5.2",
|
"version": "1.6.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"@capacitor/push-notifications": "^7.0.3",
|
"@capacitor/push-notifications": "^7.0.3",
|
||||||
"@capawesome/capacitor-android-dark-mode-support": "^7.0.0",
|
"@capawesome/capacitor-android-dark-mode-support": "^7.0.0",
|
||||||
"@capawesome/capacitor-badge": "^7.0.1",
|
"@capawesome/capacitor-badge": "^7.0.1",
|
||||||
|
"@getalby/lightning-tools": "^6.0.0",
|
||||||
"@getalby/sdk": "^5.1.2",
|
"@getalby/sdk": "^5.1.2",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@sentry/browser": "^8.55.0",
|
"@sentry/browser": "^8.55.0",
|
||||||
@@ -60,16 +61,16 @@
|
|||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.8",
|
"@vite-pwa/sveltekit": "^0.6.8",
|
||||||
"@welshman/app": "^0.6.5",
|
"@welshman/app": "^0.7.1",
|
||||||
"@welshman/content": "^0.6.5",
|
"@welshman/content": "^0.7.1",
|
||||||
"@welshman/editor": "^0.6.5",
|
"@welshman/editor": "^0.7.1",
|
||||||
"@welshman/feeds": "^0.6.5",
|
"@welshman/feeds": "^0.7.1",
|
||||||
"@welshman/lib": "^0.6.5",
|
"@welshman/lib": "^0.7.1",
|
||||||
"@welshman/net": "^0.6.5",
|
"@welshman/net": "^0.7.1",
|
||||||
"@welshman/router": "^0.6.5",
|
"@welshman/router": "^0.7.1",
|
||||||
"@welshman/signer": "^0.6.5",
|
"@welshman/signer": "^0.7.1",
|
||||||
"@welshman/store": "^0.6.5",
|
"@welshman/store": "^0.7.1",
|
||||||
"@welshman/util": "^0.6.5",
|
"@welshman/util": "^0.7.1",
|
||||||
"compressorjs": "^1.2.1",
|
"compressorjs": "^1.2.1",
|
||||||
"daisyui": "^4.12.24",
|
"daisyui": "^4.12.24",
|
||||||
"date-picker-svelte": "^2.16.0",
|
"date-picker-svelte": "^2.16.0",
|
||||||
|
|||||||
Generated
+142
-78
@@ -44,6 +44,9 @@ importers:
|
|||||||
'@capawesome/capacitor-badge':
|
'@capawesome/capacitor-badge':
|
||||||
specifier: ^7.0.1
|
specifier: ^7.0.1
|
||||||
version: 7.0.1(@capacitor/core@7.4.3)
|
version: 7.0.1(@capacitor/core@7.4.3)
|
||||||
|
'@getalby/lightning-tools':
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
'@getalby/sdk':
|
'@getalby/sdk':
|
||||||
specifier: ^5.1.2
|
specifier: ^5.1.2
|
||||||
version: 5.1.2(typescript@5.9.3)
|
version: 5.1.2(typescript@5.9.3)
|
||||||
@@ -72,35 +75,35 @@ importers:
|
|||||||
specifier: ^0.6.8
|
specifier: ^0.6.8
|
||||||
version: 0.6.8(@sveltejs/kit@2.46.5(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0))
|
version: 0.6.8(@sveltejs/kit@2.46.5(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(svelte@5.39.12)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0))(workbox-build@7.3.0)(workbox-window@7.3.0))
|
||||||
'@welshman/app':
|
'@welshman/app':
|
||||||
specifier: ^0.6.5
|
specifier: ^0.7.1
|
||||||
version: 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
version: 0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/content':
|
'@welshman/content':
|
||||||
specifier: ^0.6.5
|
specifier: ^0.7.1
|
||||||
version: 0.6.5(typescript@5.9.3)
|
version: 0.7.1(typescript@5.9.3)
|
||||||
'@welshman/editor':
|
'@welshman/editor':
|
||||||
specifier: ^0.6.5
|
specifier: ^0.7.1
|
||||||
version: 0.6.5(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)
|
version: 0.7.1(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)
|
||||||
'@welshman/feeds':
|
'@welshman/feeds':
|
||||||
specifier: ^0.6.5
|
specifier: ^0.7.1
|
||||||
version: 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
version: 0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/lib':
|
'@welshman/lib':
|
||||||
specifier: ^0.6.5
|
specifier: ^0.7.1
|
||||||
version: 0.6.5
|
version: 0.7.1
|
||||||
'@welshman/net':
|
'@welshman/net':
|
||||||
specifier: ^0.6.5
|
specifier: ^0.7.1
|
||||||
version: 0.6.5(typescript@5.9.3)(ws@8.18.3)
|
version: 0.7.1(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/router':
|
'@welshman/router':
|
||||||
specifier: ^0.6.5
|
specifier: ^0.7.1
|
||||||
version: 0.6.5(typescript@5.9.3)(ws@8.18.3)
|
version: 0.7.1(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/signer':
|
'@welshman/signer':
|
||||||
specifier: ^0.6.5
|
specifier: ^0.7.1
|
||||||
version: 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
version: 0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/store':
|
'@welshman/store':
|
||||||
specifier: ^0.6.5
|
specifier: ^0.7.1
|
||||||
version: 0.6.5(typescript@5.9.3)(ws@8.18.3)
|
version: 0.7.1(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/util':
|
'@welshman/util':
|
||||||
specifier: ^0.6.5
|
specifier: ^0.7.1
|
||||||
version: 0.6.5(typescript@5.9.3)
|
version: 0.7.1(typescript@5.9.3)
|
||||||
compressorjs:
|
compressorjs:
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
@@ -986,6 +989,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-dxOmJLJAh6qJ8rsbA5/Bwj7MSI9X3RkxxqmCedl5rfP+yKwNSdfu8i4EiCZN/tk2hNBJb8GHSCcPRNZfwfmEHg==}
|
resolution: {integrity: sha512-dxOmJLJAh6qJ8rsbA5/Bwj7MSI9X3RkxxqmCedl5rfP+yKwNSdfu8i4EiCZN/tk2hNBJb8GHSCcPRNZfwfmEHg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@getalby/lightning-tools@6.0.0':
|
||||||
|
resolution: {integrity: sha512-jpTO+7o1N1KhV5qT6qetPK+et6ZQshCzUMCRV8+Ek1NVlVU4ITIqOWRQ3kOrb0PhSxkbGN5G3d60HCi535hbDw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
'@getalby/sdk@5.1.2':
|
'@getalby/sdk@5.1.2':
|
||||||
resolution: {integrity: sha512-yUF9LhuvdIFOwjV1aG0ryzfwDiGBFk/CRLkRvrrM9dsE38SUjKsf1FDga5jxsKMu80nWcPZR9TiGGASWedoYPA==}
|
resolution: {integrity: sha512-yUF9LhuvdIFOwjV1aG0ryzfwDiGBFk/CRLkRvrrM9dsE38SUjKsf1FDga5jxsKMu80nWcPZR9TiGGASWedoYPA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -1092,6 +1099,15 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
|
'@jsr/fiatjaf__promenade-trusted-dealer@0.4.1':
|
||||||
|
resolution: {integrity: sha512-K9WjpDkQGyLl5gUZBLr3Gb+b5b1r8miZmDOo4+ZlzGQgoXD2TaqT+dkBjL/yLj/pYwBcd1Bschv0xuNpguL2ZQ==, tarball: https://npm.jsr.io/~/11/@jsr/fiatjaf__promenade-trusted-dealer/0.4.1.tgz}
|
||||||
|
|
||||||
|
'@jsr/henrygd__semaphore@0.0.2':
|
||||||
|
resolution: {integrity: sha512-nrwZ8HaqU1Agb2ij8omIxaOCAsKkDHWcwS9hTRumPhZuptwh6/0BJExBd8+eClTYM7jBnZxK+cP4WJRTcHBvCA==, tarball: https://npm.jsr.io/~/11/@jsr/henrygd__semaphore/0.0.2.tgz}
|
||||||
|
|
||||||
|
'@jsr/nostr__tools@2.16.2':
|
||||||
|
resolution: {integrity: sha512-QK1XwHvAnqEwbimD+ywbLQ3T2iI+/qE/zrRgOhmtjoEGlCWgtbPTNJ6Y/MEunXr6H/MnuHV+s400i/Yk4suvGQ==, tarball: https://npm.jsr.io/~/11/@jsr/nostr__tools/2.16.2.tgz}
|
||||||
|
|
||||||
'@noble/ciphers@0.5.3':
|
'@noble/ciphers@0.5.3':
|
||||||
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
|
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
|
||||||
|
|
||||||
@@ -1117,6 +1133,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
||||||
engines: {node: ^14.21.3 || >=16}
|
engines: {node: ^14.21.3 || >=16}
|
||||||
|
|
||||||
|
'@noble/hashes@2.0.1':
|
||||||
|
resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
|
||||||
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -1692,38 +1712,38 @@ packages:
|
|||||||
'@vite-pwa/assets-generator':
|
'@vite-pwa/assets-generator':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@welshman/app@0.6.5':
|
'@welshman/app@0.7.1':
|
||||||
resolution: {integrity: sha512-hk39kKzptldZxtFbYzgrEK8Y151o75GwV6P2sK5LlkyafWlhx3SwteAcuNelcJZitoAPXi7w06W34bbwRYPx+Q==}
|
resolution: {integrity: sha512-gHXuUVplKEtV2J7BDXxz9r6Gv9PwIfhXFEhjOraPW9/BEYS1zK0KneCe87jwZe5B/zmMk3dwMhkaUx4H3WphIA==}
|
||||||
|
|
||||||
'@welshman/content@0.6.5':
|
'@welshman/content@0.7.1':
|
||||||
resolution: {integrity: sha512-QSlkuko+2r72q3VFlOXpnnoJ6GioCgan1ysHMlKqKarKNFTL4kfqdq1yxYrFRJdQou7WuB+f9iULO0AFWkXmXg==}
|
resolution: {integrity: sha512-AHSwpodzQ9zjgbKy7CRIoQg7Irni8PUNyqlvcj4RYbY19bgaGcSoozwjbDat0wY4ULBnVsX1y2DE3+rm5R0T2A==}
|
||||||
|
|
||||||
'@welshman/editor@0.6.5':
|
'@welshman/editor@0.7.1':
|
||||||
resolution: {integrity: sha512-3sUnUFBeaVJbJgnkZlIqFqXv/NtnxXt3Pr6BkMYi2ocDMxHsMOIsOrCcyoXg8G5IYz7FzkWHtUtM3mhaDU7YVg==}
|
resolution: {integrity: sha512-fsCm+W8AQbygoN2+fm1LS6xkxdanB7v5FfhQKFsa8L1B9eYEYCAhwvrxy+nZsBEK/dt8zelk7qKQwq/CJ9sppQ==}
|
||||||
|
|
||||||
'@welshman/feeds@0.6.5':
|
'@welshman/feeds@0.7.1':
|
||||||
resolution: {integrity: sha512-IT1kSN+Xf/MaoHAOHJftORDwJZxl3UCLizc+mvJ4ktvOT/oVu9YX5zcb0YMwiJN2N4C4FpK/BIBjxivS6QIaRQ==}
|
resolution: {integrity: sha512-i9SCE1jlVIBjM1pfPVW+5axQ0BSNBmOYeo9lKdFOjeTx1sHityb/Q3kK9lgie6IDgXhK/SshEH6bKdYSnOkVSg==}
|
||||||
|
|
||||||
'@welshman/lib@0.6.5':
|
'@welshman/lib@0.7.1':
|
||||||
resolution: {integrity: sha512-L6NQm1QNBOTQ+ymiSFPfL+TDiW1AP64AEp633Fh1ciopaU73JFbV0P6xpLxt3qJkQFZfJRxk7gWDMaVDo3Y24w==}
|
resolution: {integrity: sha512-NQkxPwnAoUY4uSroQcfvR4YPG63j7Ke0R9YrLNXF9SQn2t2p6iAQ6A3GEOVu/koUQiVBseYn514lS7X1XkCP3A==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
'@welshman/net@0.6.5':
|
'@welshman/net@0.7.1':
|
||||||
resolution: {integrity: sha512-JcmPdWzT0aUaOWytw4EJpl9EooOstm4LcJczc9pJYk1hQE4gDix1AfW6bqBiDlTnJ5fplOW/KLgXuV1rhsKEWA==}
|
resolution: {integrity: sha512-S3dFH73Cy4phLy5I2KKEeefkRmNBYWB2qONK8txUVDhx1u7ezpALzZEMSPVqVIZk/vCQU3KJ0CyagvbuGF+F9Q==}
|
||||||
|
|
||||||
'@welshman/router@0.6.5':
|
'@welshman/router@0.7.1':
|
||||||
resolution: {integrity: sha512-7ZxAkCg09ZIeYh49LUlL7nFRvU4880DsMstEu2KRQQIO/wg6VZuMJh8+uKGQq5arul09rtNl1bhg0/YUFiAc/A==}
|
resolution: {integrity: sha512-PZnbGHtbnVbsY+b+FqQHNlyY2+MrEAJ3arFiO3fouayb/sWHdBfSd9EL5UM1FQd1q0fjoZIncTmffRcvQfeBqQ==}
|
||||||
|
|
||||||
'@welshman/signer@0.6.5':
|
'@welshman/signer@0.7.1':
|
||||||
resolution: {integrity: sha512-SgQCtb0du3vpyaRVGXM43CM5S0fTh+1WWLnZEWUBkEpyRzRtI9DegTowL6+vhbNxsWB6oHn/FqY7O3HLS5rIEw==}
|
resolution: {integrity: sha512-/WNEgXZemQ36A07lmrEy78Yn7kEngBjySmXW+xYmHc3OLhQ9XEq3FBCTR+vxsmp1w/t+7IEScPTKn/wvAQ/cSw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
nostr-signer-capacitor-plugin: ~0.0.4
|
nostr-signer-capacitor-plugin: ~0.0.4
|
||||||
|
|
||||||
'@welshman/store@0.6.5':
|
'@welshman/store@0.7.1':
|
||||||
resolution: {integrity: sha512-Fdl8ygK2/pZRxbLGSWtJJGtf2wTm46RDrCF2zURDJL6e80NGmTXl7LhqpSeRKmR1sTQiwEhsRvD1lqnKAWB3xQ==}
|
resolution: {integrity: sha512-EE+vlMdUeVgQhzJqzhAkbLnnOL22gXW8afJzR377n+CvHABLV7/zY9aW0Hmgm1RnyI7fSfWF2YEa6l6VP8x4pw==}
|
||||||
|
|
||||||
'@welshman/util@0.6.5':
|
'@welshman/util@0.7.1':
|
||||||
resolution: {integrity: sha512-BmKgDtgSk0RSnw3YyExN8Mm25TJuJnjvE/7foTENpf2bMo2+PTXwVERNmgDEHRq9MCbmTkPv4h7kJTbpywLMVA==}
|
resolution: {integrity: sha512-UGryq1jfwRHFS7mjGa4fmuqN851iwKeR+616LmUpTJQHAfhGU7ifer2+JLdDLYBU/neI5iKHdRDO5hg92U6k8Q==}
|
||||||
|
|
||||||
'@xml-tools/parser@1.0.11':
|
'@xml-tools/parser@1.0.11':
|
||||||
resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==}
|
resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==}
|
||||||
@@ -3526,6 +3546,14 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
nostr-tools@2.19.1:
|
||||||
|
resolution: {integrity: sha512-iEHSzRxD1gCMohtna5Jx6Cm90gGK4mrJD2+2VYMu346/EucSlz9gsUFubQ3B7f3SMsnQnh1Srm5nCcPfy2NsNw==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
nostr-wasm@0.1.0:
|
nostr-wasm@0.1.0:
|
||||||
resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==}
|
resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==}
|
||||||
|
|
||||||
@@ -5839,6 +5867,8 @@ snapshots:
|
|||||||
|
|
||||||
'@getalby/lightning-tools@5.2.1': {}
|
'@getalby/lightning-tools@5.2.1': {}
|
||||||
|
|
||||||
|
'@getalby/lightning-tools@6.0.0': {}
|
||||||
|
|
||||||
'@getalby/sdk@5.1.2(typescript@5.9.3)':
|
'@getalby/sdk@5.1.2(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@getalby/lightning-tools': 5.2.1
|
'@getalby/lightning-tools': 5.2.1
|
||||||
@@ -6024,6 +6054,24 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@jsr/fiatjaf__promenade-trusted-dealer@0.4.1':
|
||||||
|
dependencies:
|
||||||
|
'@jsr/henrygd__semaphore': 0.0.2
|
||||||
|
'@jsr/nostr__tools': 2.16.2
|
||||||
|
'@noble/curves': 1.9.7
|
||||||
|
|
||||||
|
'@jsr/henrygd__semaphore@0.0.2': {}
|
||||||
|
|
||||||
|
'@jsr/nostr__tools@2.16.2':
|
||||||
|
dependencies:
|
||||||
|
'@noble/ciphers': 0.5.3
|
||||||
|
'@noble/curves': 1.2.0
|
||||||
|
'@noble/hashes': 1.3.1
|
||||||
|
'@scure/base': 1.1.1
|
||||||
|
'@scure/bip32': 1.3.1
|
||||||
|
'@scure/bip39': 1.2.1
|
||||||
|
nostr-wasm: 0.1.0
|
||||||
|
|
||||||
'@noble/ciphers@0.5.3': {}
|
'@noble/ciphers@0.5.3': {}
|
||||||
|
|
||||||
'@noble/curves@1.1.0':
|
'@noble/curves@1.1.0':
|
||||||
@@ -6044,6 +6092,8 @@ snapshots:
|
|||||||
|
|
||||||
'@noble/hashes@1.8.0': {}
|
'@noble/hashes@1.8.0': {}
|
||||||
|
|
||||||
|
'@noble/hashes@2.0.1': {}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@@ -6651,16 +6701,16 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@vite-pwa/assets-generator': 0.2.6
|
'@vite-pwa/assets-generator': 0.2.6
|
||||||
|
|
||||||
'@welshman/app@0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/app@0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/throttle-debounce': 5.0.2
|
'@types/throttle-debounce': 5.0.2
|
||||||
'@welshman/feeds': 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/feeds': 0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/lib': 0.6.5
|
'@welshman/lib': 0.7.1
|
||||||
'@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/net': 0.7.1(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/router': 0.6.5(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/router': 0.7.1(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/signer': 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/signer': 0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/store': 0.6.5(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/store': 0.7.1(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/util': 0.6.5(typescript@5.9.3)
|
'@welshman/util': 0.7.1(typescript@5.9.3)
|
||||||
fuse.js: 7.1.0
|
fuse.js: 7.1.0
|
||||||
svelte: 4.2.20
|
svelte: 4.2.20
|
||||||
throttle-debounce: 5.0.2
|
throttle-debounce: 5.0.2
|
||||||
@@ -6669,14 +6719,14 @@ snapshots:
|
|||||||
- typescript
|
- typescript
|
||||||
- ws
|
- ws
|
||||||
|
|
||||||
'@welshman/content@0.6.5(typescript@5.9.3)':
|
'@welshman/content@0.7.1(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@braintree/sanitize-url': 7.1.1
|
'@braintree/sanitize-url': 7.1.1
|
||||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
nostr-tools: 2.17.0(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@welshman/editor@0.6.5(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)':
|
'@welshman/editor@0.7.1(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(linkifyjs@4.3.2)(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
||||||
'@tiptap/extension-code': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
'@tiptap/extension-code': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||||
@@ -6691,8 +6741,8 @@ snapshots:
|
|||||||
'@tiptap/extension-text': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
'@tiptap/extension-text': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||||
'@tiptap/pm': 2.26.3
|
'@tiptap/pm': 2.26.3
|
||||||
'@tiptap/suggestion': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
'@tiptap/suggestion': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)
|
||||||
'@welshman/lib': 0.6.5
|
'@welshman/lib': 0.7.1
|
||||||
'@welshman/util': 0.6.5(typescript@5.9.3)
|
'@welshman/util': 0.7.1(typescript@5.9.3)
|
||||||
nostr-editor: 1.0.2(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))
|
nostr-editor: 1.0.2(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/extension-image@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(linkifyjs@4.3.2)(nostr-tools@2.17.0(typescript@5.9.3))(prosemirror-markdown@1.13.2)(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3)(tiptap-markdown@0.8.10(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)))
|
||||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
nostr-tools: 2.17.0(typescript@5.9.3)
|
||||||
tippy.js: 6.3.7
|
tippy.js: 6.3.7
|
||||||
@@ -6707,71 +6757,73 @@ snapshots:
|
|||||||
- tiptap-markdown
|
- tiptap-markdown
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
'@welshman/feeds@0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/feeds@0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.6.5
|
'@welshman/lib': 0.7.1
|
||||||
'@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/net': 0.7.1(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/router': 0.6.5(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/router': 0.7.1(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/signer': 0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/signer': 0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/util': 0.6.5(typescript@5.9.3)
|
'@welshman/util': 0.7.1(typescript@5.9.3)
|
||||||
trava: 1.2.1
|
trava: 1.2.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- nostr-signer-capacitor-plugin
|
- nostr-signer-capacitor-plugin
|
||||||
- typescript
|
- typescript
|
||||||
- ws
|
- ws
|
||||||
|
|
||||||
'@welshman/lib@0.6.5':
|
'@welshman/lib@0.7.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@scure/base': 1.2.6
|
'@scure/base': 1.2.6
|
||||||
'@types/events': 3.0.3
|
'@types/events': 3.0.3
|
||||||
events: 3.3.0
|
events: 3.3.0
|
||||||
|
|
||||||
'@welshman/net@0.6.5(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/net@0.7.1(typescript@5.9.3)(ws@8.18.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.6.5
|
'@welshman/lib': 0.7.1
|
||||||
'@welshman/util': 0.6.5(typescript@5.9.3)
|
'@welshman/util': 0.7.1(typescript@5.9.3)
|
||||||
events: 3.3.0
|
events: 3.3.0
|
||||||
isomorphic-ws: 5.0.0(ws@8.18.3)
|
isomorphic-ws: 5.0.0(ws@8.18.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
- ws
|
- ws
|
||||||
|
|
||||||
'@welshman/router@0.6.5(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/router@0.7.1(typescript@5.9.3)(ws@8.18.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.6.5
|
'@welshman/lib': 0.7.1
|
||||||
'@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/net': 0.7.1(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/util': 0.6.5(typescript@5.9.3)
|
'@welshman/util': 0.7.1(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
- ws
|
- ws
|
||||||
|
|
||||||
'@welshman/signer@0.6.5(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/signer@0.7.1(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@jsr/fiatjaf__promenade-trusted-dealer': 0.4.1
|
||||||
'@noble/curves': 1.9.7
|
'@noble/curves': 1.9.7
|
||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 2.0.1
|
||||||
'@welshman/lib': 0.6.5
|
'@welshman/lib': 0.7.1
|
||||||
'@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/net': 0.7.1(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/util': 0.6.5(typescript@5.9.3)
|
'@welshman/util': 0.7.1(typescript@5.9.3)
|
||||||
nostr-signer-capacitor-plugin: 0.0.4(@capacitor/core@7.4.3)
|
nostr-signer-capacitor-plugin: 0.0.4(@capacitor/core@7.4.3)
|
||||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
nostr-tools: 2.19.1(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
- ws
|
- ws
|
||||||
|
|
||||||
'@welshman/store@0.6.5(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/store@0.7.1(typescript@5.9.3)(ws@8.18.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.6.5
|
'@welshman/lib': 0.7.1
|
||||||
'@welshman/net': 0.6.5(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/net': 0.7.1(typescript@5.9.3)(ws@8.18.3)
|
||||||
'@welshman/util': 0.6.5(typescript@5.9.3)
|
'@welshman/util': 0.7.1(typescript@5.9.3)
|
||||||
svelte: 4.2.20
|
svelte: 4.2.20
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
- ws
|
- ws
|
||||||
|
|
||||||
'@welshman/util@0.6.5(typescript@5.9.3)':
|
'@welshman/util@0.7.1(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@noble/curves': 1.9.7
|
||||||
'@types/ws': 8.18.1
|
'@types/ws': 8.18.1
|
||||||
'@welshman/lib': 0.6.5
|
'@welshman/lib': 0.7.1
|
||||||
js-base64: 3.7.8
|
js-base64: 3.7.8
|
||||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
nostr-tools: 2.17.0(typescript@5.9.3)
|
||||||
nostr-wasm: 0.1.0
|
nostr-wasm: 0.1.0
|
||||||
@@ -8669,6 +8721,18 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
nostr-tools@2.19.1(typescript@5.9.3):
|
||||||
|
dependencies:
|
||||||
|
'@noble/ciphers': 0.5.3
|
||||||
|
'@noble/curves': 1.2.0
|
||||||
|
'@noble/hashes': 1.3.1
|
||||||
|
'@scure/base': 1.1.1
|
||||||
|
'@scure/bip32': 1.3.1
|
||||||
|
'@scure/bip39': 1.2.1
|
||||||
|
nostr-wasm: 0.1.0
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
nostr-wasm@0.1.0: {}
|
nostr-wasm@0.1.0: {}
|
||||||
|
|
||||||
nth-check@2.1.1:
|
nth-check@2.1.1:
|
||||||
|
|||||||
+10
@@ -392,6 +392,16 @@ progress[value]::-webkit-progress-value {
|
|||||||
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
|
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keyboard open state adjustments */
|
||||||
|
|
||||||
|
body.keyboard-open .cb {
|
||||||
|
@apply bottom-sai;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.keyboard-open .hide-on-keyboard {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* chat view */
|
/* chat view */
|
||||||
|
|
||||||
.chat__compose {
|
.chat__compose {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib"
|
import {randomInt, map, displayList, identity, TIMEZONE} from "@welshman/lib"
|
||||||
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
||||||
import type {Filter} from "@welshman/util"
|
import type {Filter} from "@welshman/util"
|
||||||
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
|
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {alerts, userSpaceUrls} from "@app/core/state"
|
import {alertsById, userSpaceUrls} from "@app/core/state"
|
||||||
import {requestRelayClaim} from "@app/core/requests"
|
import {requestRelayClaim} from "@app/core/requests"
|
||||||
import {createAlert} from "@app/core/commands"
|
import {createAlert} from "@app/core/commands"
|
||||||
import {canSendPushNotifications} from "@app/util/push"
|
import {canSendPushNotifications} from "@app/util/push"
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
hideSpaceField = false,
|
hideSpaceField = false,
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
const timezoneOffset = parseInt(TIMEZONE.slice(3)) / 100
|
const timezoneOffset = parseInt(TIMEZONE.split(":")?.[0] || "00")
|
||||||
const minute = randomInt(0, 59)
|
const minute = randomInt(0, 59)
|
||||||
const hour = (17 - timezoneOffset) % 24
|
const hour = (17 - timezoneOffset) % 24
|
||||||
const WEEKLY = `0 ${minute} ${hour} * * 1`
|
const WEEKLY = `0 ${minute} ${hour} * * 1`
|
||||||
@@ -45,7 +45,9 @@
|
|||||||
|
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
let cron = $state(WEEKLY)
|
let cron = $state(WEEKLY)
|
||||||
let email = $state($alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || "")
|
let email = $state(
|
||||||
|
map(a => getTagValue("email", a.tags), $alertsById.values()).filter(identity)[0] || "",
|
||||||
|
)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {sleep} from "@welshman/lib"
|
import {sleep, filter} from "@welshman/lib"
|
||||||
import {getTagValue, getAddress} from "@welshman/util"
|
import {getTagValue, getAddress, RelayMode} from "@welshman/util"
|
||||||
import {isRelayFeed, findFeed} from "@welshman/feeds"
|
import {isRelayFeed, findFeed} from "@welshman/feeds"
|
||||||
|
import {getPubkeyRelays, pubkey} from "@welshman/app"
|
||||||
import Inbox from "@assets/icons/inbox.svg?dataurl"
|
import Inbox from "@assets/icons/inbox.svg?dataurl"
|
||||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
@@ -12,10 +13,9 @@
|
|||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {
|
import {
|
||||||
alerts,
|
|
||||||
dmAlert,
|
dmAlert,
|
||||||
|
alertsById,
|
||||||
deriveAlertStatus,
|
deriveAlertStatus,
|
||||||
userInboxRelays,
|
|
||||||
getAlertFeed,
|
getAlertFeed,
|
||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
const dmStatus = $derived($dmAlert ? deriveAlertStatus(getAddress($dmAlert.event)) : undefined)
|
const dmStatus = $derived($dmAlert ? deriveAlertStatus(getAddress($dmAlert.event)) : undefined)
|
||||||
|
|
||||||
const filteredAlerts = $derived(
|
const filteredAlerts = $derived(
|
||||||
$alerts.filter(alert => {
|
filter(alert => {
|
||||||
const feed = getAlertFeed(alert)
|
const feed = getAlertFeed(alert)
|
||||||
|
|
||||||
// Skip non-feeds and DM alerts
|
// Skip non-feeds and DM alerts
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
if (url) return findFeed(feed, f => isRelayFeed(f) && f.includes(url))
|
if (url) return findFeed(feed, f => isRelayFeed(f) && f.includes(url))
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}),
|
}, $alertsById.values()),
|
||||||
)
|
)
|
||||||
|
|
||||||
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
|
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
if ($dmAlert) {
|
if ($dmAlert) {
|
||||||
deleteAlert($dmAlert)
|
deleteAlert($dmAlert)
|
||||||
} else {
|
} else {
|
||||||
if ($userInboxRelays.length === 0) {
|
if (getPubkeyRelays($pubkey!, RelayMode.Messaging).length === 0) {
|
||||||
return uncheckDmAlert("Please set up your messaging relays before enabling alerts.")
|
return uncheckDmAlert("Please set up your messaging relays before enabling alerts.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
<div class="card2 bg-alt flex flex-col gap-6 shadow-md">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<strong class="flex items-center gap-3">
|
<strong class="flex items-center gap-3">
|
||||||
<Icon icon={Inbox} />
|
<Icon icon={Inbox} />
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl">
|
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<strong class="flex items-center gap-3">
|
<strong class="flex items-center gap-3">
|
||||||
<Icon icon={Bell} />
|
<Icon icon={Bell} />
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
class="col-3 card2 bg-alt w-full cursor-pointer shadow-xl"
|
class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
|
||||||
href={makeCalendarPath(url, event.id)}>
|
href={makeCalendarPath(url, event.id)}>
|
||||||
<CalendarEventHeader {event} />
|
<CalendarEventHeader {event} />
|
||||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
tagPubkey,
|
tagPubkey,
|
||||||
sendWrapped,
|
sendWrapped,
|
||||||
mergeThunks,
|
mergeThunks,
|
||||||
loadInboxRelaySelections,
|
loadMessagingRelayList,
|
||||||
inboxRelaySelectionsByPubkey,
|
messagingRelayListsByPubkey,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -43,36 +43,31 @@
|
|||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ProfileList from "@app/components/ProfileList.svelte"
|
import ChatMembers from "@app/components/ChatMembers.svelte"
|
||||||
import ChatMessage from "@app/components/ChatMessage.svelte"
|
import ChatMessage from "@app/components/ChatMessage.svelte"
|
||||||
import ChatCompose from "@app/components/ChatCompose.svelte"
|
import ChatCompose from "@app/components/ChatCompose.svelte"
|
||||||
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import {
|
import {INDEXER_RELAYS, userSettingsValues, PLATFORM_NAME, deriveChat} from "@app/core/state"
|
||||||
INDEXER_RELAYS,
|
|
||||||
userSettingsValues,
|
|
||||||
deriveChat,
|
|
||||||
splitChatId,
|
|
||||||
PLATFORM_NAME,
|
|
||||||
} from "@app/core/state"
|
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {prependParent} from "@app/core/commands"
|
import {prependParent} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string
|
pubkeys: string[]
|
||||||
info?: Snippet
|
info?: Snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
const {id, info}: Props = $props()
|
const {pubkeys, info}: Props = $props()
|
||||||
|
|
||||||
const chat = deriveChat(id)
|
const chat = deriveChat(pubkeys)
|
||||||
const pubkeys = splitChatId(id)
|
|
||||||
const others = remove($pubkey!, pubkeys)
|
const others = remove($pubkey!, pubkeys)
|
||||||
const missingInboxes = $derived(pubkeys.filter(pk => !$inboxRelaySelectionsByPubkey.has(pk)))
|
const missingRelayLists = $derived(pubkeys.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
|
||||||
|
|
||||||
const showMembers = () =>
|
const showMembers = () =>
|
||||||
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
|
others.length === 1
|
||||||
|
? pushModal(ProfileDetail, {pubkey: others[0]})
|
||||||
|
: pushModal(ChatMembers, {pubkeys: others})
|
||||||
|
|
||||||
const replyTo = (event: TrustedEvent) => {
|
const replyTo = (event: TrustedEvent) => {
|
||||||
parent = event
|
parent = event
|
||||||
@@ -183,7 +178,7 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
for (const pubkey of others) {
|
for (const pubkey of others) {
|
||||||
loadInboxRelaySelections(pubkey, INDEXER_RELAYS, true)
|
loadMessagingRelayList(pubkey, INDEXER_RELAYS, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
@@ -208,19 +203,17 @@
|
|||||||
|
|
||||||
<PageBar>
|
<PageBar>
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<div class="flex flex-col gap-1 sm:flex-row sm:gap-2">
|
<Button class="flex flex-col gap-1 sm:flex-row sm:gap-2" onclick={showMembers}>
|
||||||
{#if others.length === 0}
|
{#if others.length === 0}
|
||||||
<div class="row-2">
|
<div class="row-2">
|
||||||
<ProfileCircle pubkey={$pubkey!} size={5} />
|
<ProfileCircle pubkey={$pubkey!} size={5} />
|
||||||
<ProfileName pubkey={$pubkey!} />
|
<ProfileName pubkey={$pubkey!} />
|
||||||
</div>
|
</div>
|
||||||
{:else if others.length === 1}
|
{:else if others.length === 1}
|
||||||
{@const pubkey = others[0]}
|
<div class="row-2">
|
||||||
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
|
<ProfileCircle pubkey={others[0]} size={5} />
|
||||||
<Button onclick={onClick} class="row-2">
|
<ProfileName pubkey={others[0]} />
|
||||||
<ProfileCircle {pubkey} size={5} />
|
</div>
|
||||||
<ProfileName {pubkey} />
|
|
||||||
</Button>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ProfileCircles pubkeys={others} size={5} />
|
<ProfileCircles pubkeys={others} size={5} />
|
||||||
@@ -235,55 +228,49 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{#if others.length > 2}
|
|
||||||
<Button onclick={showMembers} class="btn btn-link hidden sm:block"
|
|
||||||
>Show all members</Button>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
<div>
|
{#if remove($pubkey, missingRelayLists).length > 0}
|
||||||
{#if remove($pubkey, missingInboxes).length > 0}
|
{@const count = remove($pubkey, missingRelayLists).length}
|
||||||
{@const count = remove($pubkey, missingInboxes).length}
|
{@const label = count > 1 ? "lists are" : "list is"}
|
||||||
{@const label = count > 1 ? "inboxes are" : "inbox is"}
|
<div
|
||||||
<div
|
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
||||||
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
data-tip="{count} messaging {label} not configured.">
|
||||||
data-tip="{count} {label} not configured.">
|
<Icon icon={Danger} />
|
||||||
<Icon icon={Danger} />
|
{count}
|
||||||
{count}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</PageBar>
|
</PageBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col-reverse gap-2 pt-4">
|
<PageContent class="flex flex-col-reverse gap-2 pt-4">
|
||||||
<div bind:this={dynamicPadding}></div>
|
<div bind:this={dynamicPadding}></div>
|
||||||
{#if missingInboxes.includes($pubkey!)}
|
{#if missingRelayLists.includes($pubkey!)}
|
||||||
<div class="py-12">
|
<div class="py-12">
|
||||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||||
<p class="row-2 text-lg text-error">
|
<p class="row-2 text-lg text-error">
|
||||||
<Icon icon={Danger} />
|
<Icon icon={Danger} />
|
||||||
Your inbox is not configured.
|
Your messaging relays are not configured.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
|
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
|
||||||
your <Link class="link" href="/settings/relays">relay settings page</Link> to set up your inbox.
|
your <Link class="link" href="/settings/relays">relay settings page</Link> to receive messages.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if missingInboxes.length > 0}
|
{:else if missingRelayLists.length > 0}
|
||||||
<div class="py-12">
|
<div class="py-12">
|
||||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||||
<p class="row-2 text-lg text-error">
|
<p class="row-2 text-lg text-error">
|
||||||
<Icon icon={Danger} />
|
<Icon icon={Danger} />
|
||||||
{missingInboxes.length}
|
{missingRelayLists.length} messaging
|
||||||
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured.
|
{missingRelayLists.length > 1 ? "lists are" : "list is"} not configured.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
|
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
|
||||||
sure everyone in this conversation has set up their inbox relays.
|
sure everyone in this conversation has set up their messaging relays.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {remove} from "@welshman/lib"
|
import {remove, formatTimestamp} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey, loadInboxRelaySelections} from "@welshman/app"
|
import {pubkey, loadMessagingRelayList} from "@welshman/app"
|
||||||
import {fade} from "@lib/transition"
|
import {fade} from "@lib/transition"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
for (const pk of others) {
|
for (const pk of others) {
|
||||||
loadInboxRelaySelections(pk)
|
loadMessagingRelayList(pk)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -59,13 +59,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
||||||
<span class="opacity-50">
|
<span class="opacity-70">
|
||||||
{#if props.messages[0].pubkey === $pubkey}
|
{#if props.messages[0].pubkey === $pubkey}
|
||||||
You:
|
You:
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{props.messages[0].content}
|
{props.messages[0].content}
|
||||||
</p>
|
</p>
|
||||||
|
<p class="text-xs opacity-70">
|
||||||
|
{formatTimestamp(props.messages[0].created_at)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pubkeys: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const {pubkeys}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>People in this conversation</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
{#each pubkeys as pubkey (pubkey)}
|
||||||
|
<div class="card2 bg-alt">
|
||||||
|
<Profile {pubkey} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<Button class="btn btn-primary" onclick={() => history.back()}>Got it</Button>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {waitForThunkCompletion} from "@welshman/app"
|
import {RelayMode} from "@welshman/util"
|
||||||
|
import {waitForThunkCompletion, getPubkeyRelays, pubkey} from "@welshman/app"
|
||||||
import ChatSquare from "@assets/icons/chat-square.svg?dataurl"
|
import ChatSquare from "@assets/icons/chat-square.svg?dataurl"
|
||||||
import Check from "@assets/icons/check.svg?dataurl"
|
import Check from "@assets/icons/check.svg?dataurl"
|
||||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
import {setChecked} from "@app/util/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {dmAlert, userInboxRelays} from "@app/core/state"
|
import {dmAlert} from "@app/core/state"
|
||||||
import {deleteAlert, createDmAlert} from "@app/core/commands"
|
import {deleteAlert, createDmAlert} from "@app/core/commands"
|
||||||
|
|
||||||
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const enableAlerts = async () => {
|
const enableAlerts = async () => {
|
||||||
if ($userInboxRelays.length === 0) {
|
if (getPubkeyRelays($pubkey!, RelayMode.Messaging).length === 0) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Please set up your messaging relays before enabling alerts.",
|
message: "Please set up your messaging relays before enabling alerts.",
|
||||||
|
|||||||
@@ -2,21 +2,14 @@
|
|||||||
import {type Instance} from "tippy.js"
|
import {type Instance} from "tippy.js"
|
||||||
import {hash, formatTimestampAsTime} from "@welshman/lib"
|
import {hash, formatTimestampAsTime} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {
|
import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
|
||||||
thunks,
|
|
||||||
mergeThunks,
|
|
||||||
pubkey,
|
|
||||||
deriveProfile,
|
|
||||||
deriveProfileDisplay,
|
|
||||||
sendWrapped,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
import TapTarget from "@lib/components/TapTarget.svelte"
|
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||||
@@ -37,7 +30,6 @@
|
|||||||
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
||||||
|
|
||||||
const isOwn = event.pubkey === $pubkey
|
const isOwn = event.pubkey === $pubkey
|
||||||
const profile = deriveProfile(event.pubkey)
|
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||||
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
||||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||||
@@ -107,8 +99,8 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if !isOwn}
|
{#if !isOwn}
|
||||||
<Button onclick={openProfile} class="flex items-center gap-1">
|
<Button onclick={openProfile} class="flex items-center gap-1">
|
||||||
<Avatar
|
<ProfileCircle
|
||||||
src={$profile?.picture}
|
pubkey={event.pubkey}
|
||||||
class="border border-solid border-base-content"
|
class="border border-solid border-base-content"
|
||||||
size={4} />
|
size={4} />
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
@@ -46,20 +46,20 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
<Button class="btn btn-neutral" onclick={showInfo}>
|
||||||
<Icon size={4} icon={SmileCircle} />
|
<Icon size={4} icon={Code2} />
|
||||||
Send Reaction
|
Message Info
|
||||||
</Button>
|
|
||||||
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
|
||||||
<Icon size={4} icon={Reply} />
|
|
||||||
Send Reply
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-neutral w-full" onclick={copyText}>
|
<Button class="btn btn-neutral w-full" onclick={copyText}>
|
||||||
<Icon size={4} icon={Copy} />
|
<Icon size={4} icon={Copy} />
|
||||||
Copy Text
|
Copy Text
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-neutral" onclick={showInfo}>
|
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
||||||
<Icon size={4} icon={Code2} />
|
<Icon size={4} icon={Reply} />
|
||||||
Message Details
|
Send Reply
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
||||||
|
<Icon size={4} icon={SmileCircle} />
|
||||||
|
Send Reaction
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl" bind:this={ul}>
|
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md" bind:this={ul}>
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={createGoal}>
|
<Button onclick={createGoal}>
|
||||||
<Icon size={4} icon={StarFallMinimalistic} />
|
<Icon size={4} icon={StarFallMinimalistic} />
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {ellipsize, displayUrl, postJson} from "@welshman/lib"
|
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
|
||||||
import {dufflepud} from "@app/core/state"
|
import {isRelayUrl} from "@welshman/util"
|
||||||
import {preventDefault, stopPropagation} from "@lib/html"
|
import {preventDefault, stopPropagation} from "@lib/html"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||||
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {dufflepud, PLATFORM_URL} from "@app/core/state"
|
||||||
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
const {value, event} = $props()
|
const {value, event} = $props()
|
||||||
|
|
||||||
let hideImage = $state(false)
|
let hideImage = $state(false)
|
||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
const [href, external] = call(() => {
|
||||||
|
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
||||||
|
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
||||||
|
|
||||||
|
return [url, true]
|
||||||
|
})
|
||||||
|
|
||||||
const loadPreview = async () => {
|
const loadPreview = async () => {
|
||||||
const json = await postJson(dufflepud("link/preview"), {url})
|
const json = await postJson(dufflepud("link/preview"), {url})
|
||||||
@@ -30,7 +38,7 @@
|
|||||||
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
|
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link external href={url} class="my-2 block">
|
<Link {external} {href} class="my-2 block">
|
||||||
<div class="overflow-hidden rounded-box">
|
<div class="overflow-hidden rounded-box">
|
||||||
{#if url.match(/\.(mov|webm|mp4)$/)}
|
{#if url.match(/\.(mov|webm|mp4)$/)}
|
||||||
<video controls src={url} class="max-h-96 object-contain object-center">
|
<video controls src={url} class="max-h-96 object-contain object-center">
|
||||||
@@ -49,7 +57,7 @@
|
|||||||
<div class="bg-alt flex max-w-xl flex-col leading-normal">
|
<div class="bg-alt flex max-w-xl flex-col leading-normal">
|
||||||
{#if preview.image && !hideImage}
|
{#if preview.image && !hideImage}
|
||||||
<img
|
<img
|
||||||
alt="Link preview"
|
alt=""
|
||||||
onerror={onError}
|
onerror={onError}
|
||||||
src={preview.image}
|
src={preview.image}
|
||||||
class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
|
class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
.map(tagsFromIMeta)
|
.map(tagsFromIMeta)
|
||||||
.find(meta => getTagValue("url", meta) === url) || event.tags
|
.find(meta => getTagValue("url", meta) === url) || event.tags
|
||||||
|
|
||||||
const hash = getTagValue("x", meta)
|
// Fallback to filename if hash was omitted from the message for interoperability
|
||||||
|
const hash = getTagValue("x", meta) || url.split(/[\/\.]/).slice(-2)[0]
|
||||||
const key = getTagValue("decryption-key", meta)
|
const key = getTagValue("decryption-key", meta)
|
||||||
const nonce = getTagValue("decryption-nonce", meta)
|
const nonce = getTagValue("decryption-nonce", meta)
|
||||||
const algorithm = getTagValue("encryption-algorithm", meta)
|
const algorithm = getTagValue("encryption-algorithm", meta)
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayUrl} from "@welshman/lib"
|
import {call, displayUrl} from "@welshman/lib"
|
||||||
|
import {isRelayUrl} from "@welshman/util"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {PLATFORM_URL} from "@app/core/state"
|
||||||
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
const {value} = $props()
|
const {value} = $props()
|
||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
const [href, external] = call(() => {
|
||||||
|
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
||||||
|
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
||||||
|
|
||||||
|
return [url, true]
|
||||||
|
})
|
||||||
|
|
||||||
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
||||||
</script>
|
</script>
|
||||||
@@ -21,7 +30,7 @@
|
|||||||
{displayUrl(url)}
|
{displayUrl(url)}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<Link external href={url} class="link-content whitespace-nowrap">
|
<Link {external} {href} class="link-content whitespace-nowrap">
|
||||||
<Icon icon={LinkRound} size={3} class="inline-block" />
|
<Icon icon={LinkRound} size={3} class="inline-block" />
|
||||||
{displayUrl(url)}
|
{displayUrl(url)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {removeNil} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import type {ProfilePointer} from "@welshman/content"
|
import type {ProfilePointer} from "@welshman/content"
|
||||||
import {deriveProfileDisplay} from "@welshman/app"
|
import {deriveProfileDisplay} from "@welshman/app"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
const {value, url}: Props = $props()
|
const {value, url}: Props = $props()
|
||||||
|
|
||||||
const display = deriveProfileDisplay(value.pubkey, removeNil([url]))
|
const display = deriveProfileDisplay(value.pubkey, removeUndefined([url]))
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import {max, formatTimestampRelative} from "@welshman/lib"
|
import {max, formatTimestampRelative} from "@welshman/lib"
|
||||||
import {COMMENT} from "@welshman/util"
|
import {COMMENT} from "@welshman/util"
|
||||||
import {load} from "@welshman/net"
|
import {load} from "@welshman/net"
|
||||||
import {deriveEvents} from "@welshman/store"
|
import {deriveArray, deriveEventsById} from "@welshman/store"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {repository} from "@welshman/app"
|
import {repository} from "@welshman/app"
|
||||||
import {notifications} from "@app/util/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
const {url, path, event}: {url: string; path: string; event: TrustedEvent} = $props()
|
const {url, path, event}: {url: string; path: string; event: TrustedEvent} = $props()
|
||||||
|
|
||||||
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
|
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
|
||||||
const replies = deriveEvents(repository, {filters})
|
const replies = deriveArray(deriveEventsById({repository, filters}))
|
||||||
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import {LOCALE, secondsToDate} from "@welshman/lib"
|
import {LOCALE, secondsToDate} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
import {tracker} from "@welshman/app"
|
||||||
import FileText from "@assets/icons/file-text.svg?dataurl"
|
import FileText from "@assets/icons/file-text.svg?dataurl"
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||||
@@ -11,7 +12,6 @@
|
|||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import {trackerStore} from "@app/core/state"
|
|
||||||
import {clip} from "@app/util/toast"
|
import {clip} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
const relays = url ? [url] : Router.get().Event(event).getUrls()
|
const relays = url ? [url] : Router.get().Event(event).getUrls()
|
||||||
const nevent1 = nip19.neventEncode({...event, relays})
|
const nevent1 = nip19.neventEncode({...event, relays})
|
||||||
const seenOn = $trackerStore.getRelays(event.id)
|
const seenOn = tracker.getRelays(event.id)
|
||||||
const npub1 = nip19.npubEncode(event.pubkey)
|
const npub1 = nip19.npubEncode(event.pubkey)
|
||||||
const json = JSON.stringify(event, null, 2)
|
const json = JSON.stringify(event, null, 2)
|
||||||
const copyLink = () => clip(nevent1)
|
const copyLink = () => clip(nevent1)
|
||||||
|
|||||||
@@ -3,21 +3,23 @@
|
|||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {COMMENT} from "@welshman/util"
|
import {COMMENT, ManagementMethod} from "@welshman/util"
|
||||||
import {pubkey, relaysByUrl} from "@welshman/app"
|
import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
|
||||||
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
import Danger from "@assets/icons/danger.svg?dataurl"
|
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||||
import {setKey} from "@lib/implicit"
|
import {setKey} from "@lib/implicit"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import EventReport from "@app/components/EventReport.svelte"
|
import Report from "@app/components/Report.svelte"
|
||||||
import EventShare from "@app/components/EventShare.svelte"
|
import EventShare from "@app/components/EventShare.svelte"
|
||||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {hasNip29} from "@app/core/state"
|
import {hasNip29, deriveUserIsSpaceAdmin} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
import {makeSpaceChatPath} from "@app/util/routes"
|
import {makeSpaceChatPath} from "@app/util/routes"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -31,8 +33,9 @@
|
|||||||
const {url, noun, event, onClick, customActions}: Props = $props()
|
const {url, noun, event, onClick, customActions}: Props = $props()
|
||||||
|
|
||||||
const isRoot = event.kind !== COMMENT
|
const isRoot = event.kind !== COMMENT
|
||||||
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
const report = () => pushModal(EventReport, {url, event})
|
const report = () => pushModal(Report, {url, event})
|
||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {url, event})
|
const showInfo = () => pushModal(EventInfo, {url, event})
|
||||||
|
|
||||||
@@ -47,6 +50,26 @@
|
|||||||
|
|
||||||
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||||
|
|
||||||
|
const showAdminDelete = () =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: `Delete ${noun}`,
|
||||||
|
message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.BanEvent,
|
||||||
|
params: [event.id],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Event has successfully been deleted!"})
|
||||||
|
repository.removeEvent(event.id)
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
let ul: Element
|
let ul: Element
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -54,7 +77,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl" bind:this={ul}>
|
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md" bind:this={ul}>
|
||||||
{#if isRoot}
|
{#if isRoot}
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={share}>
|
<Button onclick={share}>
|
||||||
@@ -84,5 +107,13 @@
|
|||||||
Report Content
|
Report Content
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
|
{#if $userIsAdmin}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={showAdminDelete}>
|
||||||
|
<Icon size={4} icon={TrashBin2} />
|
||||||
|
Delete {noun}
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {getTag, REPORT} from "@welshman/util"
|
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
|
||||||
import {deriveEvents} from "@welshman/store"
|
|
||||||
import {pubkey, repository} from "@welshman/app"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Profile from "@app/components/Profile.svelte"
|
|
||||||
import {publishDelete, canEnforceNip70} from "@app/core/commands"
|
|
||||||
|
|
||||||
const {url, event} = $props()
|
|
||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
|
||||||
|
|
||||||
const reports = deriveEvents(repository, {
|
|
||||||
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
|
||||||
})
|
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
|
|
||||||
const deleteReport = async (report: TrustedEvent) => {
|
|
||||||
publishDelete({event: report, relays: [url], protect: await shouldProtect})
|
|
||||||
|
|
||||||
if ($reports.length === 0) {
|
|
||||||
history.back()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getReason = (tags: string[][]) => getTag("e", tags)?.[2] || "other"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="column gap-4">
|
|
||||||
<ModalHeader>
|
|
||||||
{#snippet title()}
|
|
||||||
<div>Report Details</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<div>All reports for this event are shown below.</div>
|
|
||||||
{/snippet}
|
|
||||||
</ModalHeader>
|
|
||||||
{#each $reports as report (report.id)}
|
|
||||||
{@const reason = getReason(report.tags)}
|
|
||||||
{@const remove = () => deleteReport(report)}
|
|
||||||
<div class="column gap-2">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div>
|
|
||||||
<Profile pubkey={report.pubkey} {url} />
|
|
||||||
<span>Reported this event as "{reason}"</span>
|
|
||||||
</div>
|
|
||||||
{#if report.pubkey === $pubkey}
|
|
||||||
<Button class="btn-default btn" onclick={remove}>Delete Report</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if report.content}
|
|
||||||
<p>"{report.content}"</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<Button class="btn btn-primary" onclick={back}>Got it</Button>
|
|
||||||
</div>
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
const h = getTagValue("h", event.tags)
|
const h = getTagValue("h", event.tags)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl" href={makeGoalPath(url, event.id)}>
|
<Link class="col-2 card2 bg-alt w-full cursor-pointer shadow-md" href={makeGoalPath(url, event.id)}>
|
||||||
<p class="text-2xl">{event.content}</p>
|
<p class="text-2xl">{event.content}</p>
|
||||||
<Content
|
<Content
|
||||||
event={{content: summary, tags: event.tags}}
|
event={{content: summary, tags: event.tags}}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {now, DAY, uniq, sum} from "@welshman/lib"
|
import {now, DAY, uniq, sum} from "@welshman/lib"
|
||||||
import type {Zap, TrustedEvent} from "@welshman/util"
|
import type {Zap, TrustedEvent} from "@welshman/util"
|
||||||
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
|
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
|
||||||
import {deriveEventsMapped} from "@welshman/store"
|
import {deriveItemsByKey, deriveArray} from "@welshman/store"
|
||||||
import {repository, getValidZap} from "@welshman/app"
|
import {repository, getValidZap} from "@welshman/app"
|
||||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -16,11 +16,14 @@
|
|||||||
|
|
||||||
const {url, event, ...props}: Props = $props()
|
const {url, event, ...props}: Props = $props()
|
||||||
|
|
||||||
const zaps = deriveEventsMapped<Zap>(repository, {
|
const zaps = deriveArray(
|
||||||
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
deriveItemsByKey<Zap>({
|
||||||
itemToEvent: item => item.response,
|
repository,
|
||||||
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
|
getKey: zap => zap.response.id,
|
||||||
})
|
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
||||||
|
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
|
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
|
||||||
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
|
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-96 rounded-box bg-base-100 p-4 shadow-lg">
|
<div class="w-96 rounded-box bg-base-100 p-4 shadow-2xl">
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
<Icon icon={Magnifier} />
|
<Icon icon={Magnifier} />
|
||||||
<input bind:value={searchTerm} class="grow" type="text" placeholder="Search icons..." />
|
<input bind:value={searchTerm} class="grow" type="text" placeholder="Search icons..." />
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount, onDestroy} from "svelte"
|
import {onMount, onDestroy} from "svelte"
|
||||||
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
||||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
import {Nip46Broker} from "@welshman/signer"
|
||||||
|
import {makeSecret} from "@welshman/util"
|
||||||
import {loginWithNip01, loginWithNip46} from "@welshman/app"
|
import {loginWithNip01, loginWithNip46} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount, onDestroy} from "svelte"
|
import {onMount, onDestroy} from "svelte"
|
||||||
import {postJson, stripProtocol} from "@welshman/lib"
|
import {postJson, stripProtocol} from "@welshman/lib"
|
||||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
import {Nip46Broker} from "@welshman/signer"
|
||||||
import {normalizeRelayUrl} from "@welshman/util"
|
import {normalizeRelayUrl, makeSecret} from "@welshman/util"
|
||||||
import {addSession, makeNip46Session} from "@welshman/app"
|
import {addSession, makeNip46Session} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import CardButton from "@lib/components/CardButton.svelte"
|
import CardButton from "@lib/components/CardButton.svelte"
|
||||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link replaceState href={path}>
|
<Link replaceState href={path}>
|
||||||
<CardButton class="btn-neutral">
|
<CardButton class="btn-neutral shadow-md">
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<div><SpaceAvatar {url} /></div>
|
<RelayIcon {url} size={12} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
target: element,
|
target: element,
|
||||||
props: {
|
props: {
|
||||||
onClose: closeModals,
|
onClose: closeModals,
|
||||||
|
fullscreen: options.fullscreen,
|
||||||
children: createRawSnippet(() => ({
|
children: createRawSnippet(() => ({
|
||||||
render: () => "<div></div>",
|
render: () => "<div></div>",
|
||||||
setup: (target: Element) => {
|
setup: (target: Element) => {
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
|
||||||
import {formatTimestamp} from "@welshman/lib"
|
import {formatTimestamp} from "@welshman/lib"
|
||||||
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {Router} from "@welshman/router"
|
import {userMuteList} from "@welshman/app"
|
||||||
import {userMutes} from "@welshman/app"
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import {entityLink} from "@app/core/state"
|
import {goToEvent} from "@app/util/routes"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
event,
|
event,
|
||||||
@@ -31,14 +28,11 @@
|
|||||||
class?: string
|
class?: string
|
||||||
} = $props()
|
} = $props()
|
||||||
|
|
||||||
const relays = Router.get().Event(event).getUrls()
|
|
||||||
const nevent = nip19.neventEncode({id: event.id, relays})
|
|
||||||
|
|
||||||
const ignoreMute = () => {
|
const ignoreMute = () => {
|
||||||
muted = false
|
muted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let muted = $state(getPubkeyTagValues(getListTags($userMutes)).includes(event.pubkey))
|
let muted = $state(getPubkeyTagValues(getListTags($userMuteList)).includes(event.pubkey))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 {restProps.class}">
|
<div class="flex flex-col gap-2 {restProps.class}">
|
||||||
@@ -59,12 +53,11 @@
|
|||||||
<Profile pubkey={event.pubkey} {url} />
|
<Profile pubkey={event.pubkey} {url} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<Link
|
<Button
|
||||||
external
|
class={cx("text-sm opacity-75", {"text-xs": minimal})}
|
||||||
href={entityLink(nevent)}
|
onclick={() => goToEvent(event)}>
|
||||||
class={cx("text-sm opacity-75", {"text-xs": minimal})}>
|
|
||||||
{formatTimestamp(event.created_at)}
|
{formatTimestamp(event.created_at)}
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import {sum} from "@welshman/lib"
|
import {sum} from "@welshman/lib"
|
||||||
import type {Zap, TrustedEvent} from "@welshman/util"
|
import type {Zap, TrustedEvent} from "@welshman/util"
|
||||||
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
|
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
|
||||||
import {deriveEventsMapped} from "@welshman/store"
|
import {deriveItemsByKey, deriveArray} from "@welshman/store"
|
||||||
import {repository, getValidZap} from "@welshman/app"
|
import {repository, getValidZap} from "@welshman/app"
|
||||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -14,11 +14,14 @@
|
|||||||
const content = getTagValue("summary", props.event.tags)
|
const content = getTagValue("summary", props.event.tags)
|
||||||
const fakeEvent = {content, tags: props.event.tags}
|
const fakeEvent = {content, tags: props.event.tags}
|
||||||
|
|
||||||
const zaps = deriveEventsMapped<Zap>(repository, {
|
const zaps = deriveArray(
|
||||||
filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
|
deriveItemsByKey<Zap>({
|
||||||
itemToEvent: item => item.response,
|
repository,
|
||||||
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
|
getKey: zap => zap.response.id,
|
||||||
})
|
filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
|
||||||
|
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
|
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
|
||||||
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
|
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl">
|
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<Profile {pubkey} {url} />
|
<Profile {pubkey} {url} />
|
||||||
<Button onclick={openProfile} class="btn btn-primary hidden sm:flex">
|
<Button onclick={openProfile} class="btn btn-primary hidden sm:flex">
|
||||||
|
|||||||
@@ -4,7 +4,15 @@
|
|||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {splitAt} from "@welshman/lib"
|
import {splitAt} from "@welshman/lib"
|
||||||
import {userProfile, shouldUnwrap} from "@welshman/app"
|
import {userProfile, shouldUnwrap} from "@welshman/app"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
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 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 Divider from "@lib/components/Divider.svelte"
|
||||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
import ChatEnable from "@app/components/ChatEnable.svelte"
|
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||||
@@ -15,13 +23,6 @@
|
|||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
import {notifications} from "@app/util/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
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 SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
|
||||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: Snippet
|
children?: Snippet
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
<PrimaryNavItemSpace {url} />
|
<PrimaryNavItemSpace {url} />
|
||||||
{:else}
|
{:else}
|
||||||
<PrimaryNavItem title="Home" href="/home" class="tooltip-right">
|
<PrimaryNavItem title="Home" href="/home" class="tooltip-right">
|
||||||
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" />
|
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#each primarySpaceUrls as url (url)}
|
{#each primarySpaceUrls as url (url)}
|
||||||
@@ -73,11 +74,11 @@
|
|||||||
class="tooltip-right"
|
class="tooltip-right"
|
||||||
onclick={showOtherSpacesMenu}
|
onclick={showOtherSpacesMenu}
|
||||||
notification={otherSpaceNotifications}>
|
notification={otherSpaceNotifications}>
|
||||||
<Avatar icon={Widget} class="!h-10 !w-10" />
|
<ImageIcon alt="Other Spaces" src={Widget} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
|
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
|
||||||
<Avatar icon={Compass} class="!h-10 !w-10" />
|
<ImageIcon alt="Add a Space" src={Compass} size={7} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -90,17 +91,17 @@
|
|||||||
href="/settings/profile"
|
href="/settings/profile"
|
||||||
prefix="/settings"
|
prefix="/settings"
|
||||||
class="tooltip-right">
|
class="tooltip-right">
|
||||||
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
|
<ImageIcon alt="Settings" src={$userProfile?.picture || UserRounded} class="rounded-full" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
title="Messages"
|
title="Messages"
|
||||||
onclick={openChat}
|
onclick={openChat}
|
||||||
class="tooltip-right"
|
class="tooltip-right"
|
||||||
notification={$notifications.has("/chat")}>
|
notification={$notifications.has("/chat")}>
|
||||||
<Avatar icon={Letter} class="!h-10 !w-10" />
|
<ImageIcon alt="Messages" src={Letter} size={7} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
|
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
|
||||||
<Avatar icon={Magnifier} class="!h-10 !w-10" />
|
<ImageIcon alt="Search" src={Magnifier} size={7} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,28 +110,34 @@
|
|||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|
||||||
<!-- a little extra something for ios -->
|
<!-- a little extra something for ios -->
|
||||||
<div class="fixed bottom-0 left-0 right-0 z-nav h-[var(--saib)] bg-base-100 md:hidden"></div>
|
|
||||||
<div
|
<div
|
||||||
class="border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
class="bottom-nav hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-[var(--saib)] bg-base-100 md:hidden">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bottom-nav hide-on-keyboard border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
||||||
<div class="content-padding-x content-sizing flex justify-between px-2">
|
<div class="content-padding-x content-sizing flex justify-between px-2">
|
||||||
<div class="flex gap-2 sm:gap-6">
|
<div class="flex gap-2 sm:gap-6">
|
||||||
<PrimaryNavItem title="Home" href="/home">
|
<PrimaryNavItem title="Home" href="/home">
|
||||||
<Avatar icon={HomeSmile} class="!h-10 !w-10" />
|
<ImageIcon alt="Home" src={HomeSmile} size={7} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
title="Messages"
|
title="Messages"
|
||||||
onclick={openChat}
|
onclick={openChat}
|
||||||
notification={$notifications.has("/chat")}>
|
notification={$notifications.has("/chat")}>
|
||||||
<Avatar icon={Letter} class="!h-10 !w-10" />
|
<ImageIcon alt="Messages" src={Letter} size={7} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
{#if PLATFORM_RELAYS.length !== 1}
|
{#if PLATFORM_RELAYS.length !== 1}
|
||||||
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
|
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
|
||||||
<Avatar icon={SettingsMinimalistic} class="!h-10 !w-10" />
|
<ImageIcon alt="Spaces" src={SettingsMinimalistic} size={7} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
|
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
|
||||||
<Avatar icon={Settings} src={$userProfile?.picture} class="!h-10 !w-10" />
|
<ImageIcon
|
||||||
|
alt="Settings"
|
||||||
|
src={$userProfile?.picture || Settings}
|
||||||
|
size={7}
|
||||||
|
class="rounded-full" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||||
import {makeSpacePath, goToSpace} from "@app/util/routes"
|
import {makeSpacePath, goToSpace} from "@app/util/routes"
|
||||||
import {notifications} from "@app/util/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
|
|
||||||
const {url} = $props()
|
type Props = {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
const onClick = () => goToSpace(url)
|
const onClick = () => goToSpace(url)
|
||||||
</script>
|
</script>
|
||||||
@@ -15,5 +19,5 @@
|
|||||||
title={displayRelayUrl(url)}
|
title={displayRelayUrl(url)}
|
||||||
class="tooltip-right"
|
class="tooltip-right"
|
||||||
notification={$notifications.has(makeSpacePath(url))}>
|
notification={$notifications.has(makeSpacePath(url))}>
|
||||||
<SpaceAvatar {url} />
|
<RelayIcon {url} size={7} class="rounded-full" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {removeNil} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {displayPubkey} from "@welshman/util"
|
import {displayPubkey} from "@welshman/util"
|
||||||
import {
|
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
|
||||||
deriveHandleForPubkey,
|
|
||||||
displayHandle,
|
|
||||||
deriveProfile,
|
|
||||||
deriveProfileDisplay,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import WotScore from "@app/components/WotScore.svelte"
|
import WotScore from "@app/components/WotScore.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
@@ -26,8 +21,7 @@
|
|||||||
|
|
||||||
const {pubkey, url, showPubkey, avatarSize = 10}: Props = $props()
|
const {pubkey, url, showPubkey, avatarSize = 10}: Props = $props()
|
||||||
|
|
||||||
const relays = removeNil([url])
|
const relays = removeUndefined([url])
|
||||||
const profile = deriveProfile(pubkey, relays)
|
|
||||||
const profileDisplay = deriveProfileDisplay(pubkey, relays)
|
const profileDisplay = deriveProfileDisplay(pubkey, relays)
|
||||||
const handle = deriveHandleForPubkey(pubkey)
|
const handle = deriveHandleForPubkey(pubkey)
|
||||||
|
|
||||||
@@ -38,7 +32,7 @@
|
|||||||
|
|
||||||
<div class="flex max-w-full items-start gap-3">
|
<div class="flex max-w-full items-start gap-3">
|
||||||
<Button onclick={openProfile} class="py-1">
|
<Button onclick={openProfile} class="py-1">
|
||||||
<Avatar src={$profile?.picture} size={avatarSize} />
|
<ProfileCircle {pubkey} size={avatarSize} />
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex min-w-0 flex-col">
|
<div class="flex min-w-0 flex-col">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
@@ -3,17 +3,13 @@
|
|||||||
import {load} from "@welshman/net"
|
import {load} from "@welshman/net"
|
||||||
import {Router} from "@welshman/router"
|
import {Router} from "@welshman/router"
|
||||||
import type {Filter} from "@welshman/util"
|
import type {Filter} from "@welshman/util"
|
||||||
import {deriveEvents} from "@welshman/store"
|
import {deriveArray, deriveEventsById} from "@welshman/store"
|
||||||
import {formatTimestampRelative} from "@welshman/lib"
|
import {formatTimestampRelative} from "@welshman/lib"
|
||||||
import {NOTE, ROOMS, COMMENT} from "@welshman/util"
|
import {NOTE, ROOMS, COMMENT} from "@welshman/util"
|
||||||
import {repository, loadRelaySelections} from "@welshman/app"
|
import {repository, loadRelayList} from "@welshman/app"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
|
import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
|
||||||
import {
|
import {deriveGroupList, getSpaceUrlsFromGroupList, MESSAGE_KINDS} from "@app/core/state"
|
||||||
deriveGroupSelections,
|
|
||||||
getSpaceUrlsFromGroupSelections,
|
|
||||||
MESSAGE_KINDS,
|
|
||||||
} from "@app/core/state"
|
|
||||||
import {goToEvent} from "@app/util/routes"
|
import {goToEvent} from "@app/util/routes"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
@@ -24,9 +20,9 @@
|
|||||||
|
|
||||||
const {pubkey, url}: Props = $props()
|
const {pubkey, url}: Props = $props()
|
||||||
const filters: Filter[] = [{authors: [pubkey], limit: 1}]
|
const filters: Filter[] = [{authors: [pubkey], limit: 1}]
|
||||||
const events = deriveEvents(repository, {filters})
|
const events = deriveArray(deriveEventsById({repository, filters}))
|
||||||
const selections = deriveGroupSelections(pubkey)
|
const groupList = deriveGroupList(pubkey)
|
||||||
const spaceUrls = $derived(getSpaceUrlsFromGroupSelections($selections))
|
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
|
||||||
|
|
||||||
const viewEvent = () => goToEvent($events[0]!)
|
const viewEvent = () => goToEvent($events[0]!)
|
||||||
|
|
||||||
@@ -34,7 +30,7 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Make sure we have their relay selections before we load their posts
|
// Make sure we have their relay selections before we load their posts
|
||||||
await loadRelaySelections(pubkey)
|
await loadRelayList(pubkey)
|
||||||
|
|
||||||
// Load groups and at least one note, regardless of time frame
|
// Load groups and at least one note, regardless of time frame
|
||||||
load({
|
load({
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import cx from "classnames"
|
||||||
import {removeNil} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {deriveProfile} from "@welshman/app"
|
import {deriveProfile} from "@welshman/app"
|
||||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||||
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
|
class?: string
|
||||||
|
size?: number
|
||||||
url?: string
|
url?: string
|
||||||
} & Record<string, any>
|
}
|
||||||
|
|
||||||
const {pubkey, url, ...props}: Props = $props()
|
const {pubkey, url, size = 7, ...props}: Props = $props()
|
||||||
|
|
||||||
const profile = deriveProfile(pubkey, removeNil([url]))
|
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Avatar src={$profile?.picture} icon={UserCircle} {...props} />
|
<ImageIcon
|
||||||
|
{size}
|
||||||
|
alt=""
|
||||||
|
class={cx(props.class, "rounded-full")}
|
||||||
|
src={$profile?.picture || UserRounded} />
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {getProfile} from "@welshman/app"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
|
|
||||||
const {...props} = $props()
|
type Props = {
|
||||||
|
pubkeys: string[]
|
||||||
|
size?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const {pubkeys, size = 7}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex pr-3">
|
<div class="flex pr-3">
|
||||||
{#each props.pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
|
{#each pubkeys
|
||||||
|
.filter(p => getProfile(p)?.picture)
|
||||||
|
.toSorted()
|
||||||
|
.slice(0, 15) as pubkey (pubkey)}
|
||||||
<div class="z-feature -mr-3 inline-block">
|
<div class="z-feature -mr-3 inline-block">
|
||||||
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {...props} />
|
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {size} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
DELETE,
|
DELETE,
|
||||||
isReplaceable,
|
isReplaceable,
|
||||||
getAddress,
|
getAddress,
|
||||||
|
RelayMode,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {pubkey, publishThunk, repository} from "@welshman/app"
|
import {pubkey, publishThunk, repository, derivePubkeyRelays} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
@@ -19,12 +20,13 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {logout} from "@app/core/commands"
|
import {logout} from "@app/core/commands"
|
||||||
import {INDEXER_RELAYS, PLATFORM_NAME, userSpaceUrls, userWriteRelays} from "@app/core/state"
|
import {INDEXER_RELAYS, PLATFORM_NAME, userSpaceUrls} from "@app/core/state"
|
||||||
|
|
||||||
let progress: number | undefined = $state(undefined)
|
let progress: number | undefined = $state(undefined)
|
||||||
let confirmText = $state("")
|
let confirmText = $state("")
|
||||||
|
|
||||||
const CONFIRM_TEXT = "permanently delete my nostr account"
|
const CONFIRM_TEXT = "permanently delete my nostr account"
|
||||||
|
const userWriteRelays = derivePubkeyRelays($pubkey!, RelayMode.Write)
|
||||||
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
|
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
|
||||||
const showProgress = $derived(progress !== undefined)
|
const showProgress = $derived(progress !== undefined)
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {shouldUnwrap} from "@welshman/app"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
|
import {ManagementMethod} from "@welshman/util"
|
||||||
|
import {shouldUnwrap, manageRelay, deriveProfile, displayProfileByPubkey} from "@welshman/app"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
||||||
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
||||||
import ChatEnable from "@app/components/ChatEnable.svelte"
|
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||||
import {pubkeyLink} from "@app/core/state"
|
import {pubkeyLink, deriveUserIsSpaceAdmin} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
import {makeChatPath} from "@app/util/routes"
|
import {makeChatPath} from "@app/util/routes"
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
@@ -23,15 +33,83 @@
|
|||||||
|
|
||||||
const {pubkey, url}: Props = $props()
|
const {pubkey, url}: Props = $props()
|
||||||
|
|
||||||
|
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
||||||
|
|
||||||
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const chatPath = makeChatPath([pubkey])
|
const chatPath = makeChatPath([pubkey])
|
||||||
|
|
||||||
|
const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event})
|
||||||
|
|
||||||
const openChat = () => ($shouldUnwrap ? goto(chatPath) : pushModal(ChatEnable, {next: chatPath}))
|
const openChat = () => ($shouldUnwrap ? goto(chatPath) : pushModal(ChatEnable, {next: chatPath}))
|
||||||
|
|
||||||
|
const toggleMenu = (pubkey: string) => {
|
||||||
|
showMenu = !showMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
showMenu = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const banMember = () =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Ban User",
|
||||||
|
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const {error} = await manageRelay(url!, {
|
||||||
|
method: ManagementMethod.BanPubkey,
|
||||||
|
params: [pubkey],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "User has successfully been banned!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let showMenu = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<Profile showPubkey avatarSize={14} {pubkey} {url} />
|
<div class="flex justify-between">
|
||||||
|
<Profile showPubkey avatarSize={14} {pubkey} {url} />
|
||||||
|
{#if $profile || $userIsAdmin}
|
||||||
|
<div class="relative">
|
||||||
|
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
||||||
|
<Icon icon={MenuDots} />
|
||||||
|
</Button>
|
||||||
|
{#if showMenu}
|
||||||
|
<Popover hideOnClick onClose={closeMenu}>
|
||||||
|
<ul
|
||||||
|
transition:fly
|
||||||
|
class="bg-alt menu absolute right-0 z-popover w-48 gap-1 rounded-box p-2 shadow-md">
|
||||||
|
{#if $profile}
|
||||||
|
<li>
|
||||||
|
<Button onclick={showInfo}>
|
||||||
|
<Icon icon={Code2} />
|
||||||
|
User Details
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if $userIsAdmin}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={banMember}>
|
||||||
|
<Icon icon={MinusCircle} />
|
||||||
|
Ban User
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<ProfileInfo {pubkey} {url} />
|
<ProfileInfo {pubkey} {url} />
|
||||||
<ProfileBadges {pubkey} {url} />
|
<ProfileBadges {pubkey} {url} />
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
@@ -41,7 +119,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Link external href={pubkeyLink(pubkey)} class="btn btn-neutral">
|
<Link external href={pubkeyLink(pubkey)} class="btn btn-neutral">
|
||||||
<Avatar src="/coracle.png" />
|
<ImageIcon alt="" src="/coracle.png" />
|
||||||
Open in Coracle
|
Open in Coracle
|
||||||
</Link>
|
</Link>
|
||||||
<Button onclick={openChat} class="btn btn-primary">
|
<Button onclick={openChat} class="btn btn-primary">
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
success = true
|
success = true
|
||||||
pushToast({message: "Success! Please check your inbox and continue when you're ready."})
|
pushToast({message: "Success! Please check your messages and continue when you're ready."})
|
||||||
|
|
||||||
await logout()
|
await logout()
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {removeNil} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {deriveProfile} from "@welshman/app"
|
import {deriveProfile} from "@welshman/app"
|
||||||
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
const {pubkey, url}: Props = $props()
|
const {pubkey, url}: Props = $props()
|
||||||
|
|
||||||
const profile = deriveProfile(pubkey, removeNil([url]))
|
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $profile}
|
{#if $profile}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {load} from "@welshman/net"
|
import {load} from "@welshman/net"
|
||||||
import {NOTE} from "@welshman/util"
|
import {NOTE} from "@welshman/util"
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import NoteItem from "@app/components/NoteItem.svelte"
|
import NoteItem from "@app/components/NoteItem.svelte"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -24,16 +22,16 @@
|
|||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#await events}
|
{#await events}
|
||||||
<p class="center my-12 flex">
|
<p class="center flex min-h-6">
|
||||||
<Spinner loading />
|
<span class="loading loading-spinner"></span>
|
||||||
</p>
|
</p>
|
||||||
{:then events}
|
{:then events}
|
||||||
{#each events as event (event.id)}
|
{#each events as event (event.id)}
|
||||||
<div in:fly>
|
<NoteItem {url} {event} />
|
||||||
<NoteItem {url} {event} />
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
{@render fallback?.()}
|
<div class="min-h-6">
|
||||||
|
{@render fallback?.()}
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {removeNil} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {deriveProfileDisplay} from "@welshman/app"
|
import {deriveProfileDisplay} from "@welshman/app"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
const {pubkey, url}: Props = $props()
|
const {pubkey, url}: Props = $props()
|
||||||
|
|
||||||
const profileDisplay = deriveProfileDisplay(pubkey, removeNil([url]))
|
const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{$profileDisplay}
|
{$profileDisplay}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
import {deriveGroupSelections, getSpaceUrlsFromGroupSelections} from "@app/core/state"
|
import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/core/state"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
const {pubkey}: Props = $props()
|
const {pubkey}: Props = $props()
|
||||||
|
|
||||||
const selections = deriveGroupSelections(pubkey)
|
const groupList = deriveGroupList(pubkey)
|
||||||
const spaceUrls = $derived(getSpaceUrlsFromGroupSelections($selections))
|
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
</script>
|
</script>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
{#each spaceUrls as url (url)}
|
{#each spaceUrls as url (url)}
|
||||||
<div class="card2 bg-alt flex flex-row items-center gap-2">
|
<div class="card2 bg-alt flex flex-row items-center gap-2">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<SpaceAvatar {url} />
|
<RelayIcon {url} size={12} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-grow flex-col">
|
<div class="flex flex-grow flex-col">
|
||||||
<RelayName {url} />
|
<RelayName {url} />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {groupBy, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib"
|
import {groupBy, map, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
REPORT,
|
REPORT,
|
||||||
REACTION,
|
REACTION,
|
||||||
@@ -15,14 +15,14 @@
|
|||||||
DELETE,
|
DELETE,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
|
import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
|
||||||
import {deriveEvents, deriveEventsMapped} from "@welshman/store"
|
import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store"
|
||||||
import {load} from "@welshman/net"
|
import {load} from "@welshman/net"
|
||||||
import {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app"
|
import {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app"
|
||||||
import {isMobile, preventDefault, stopPropagation} from "@lib/html"
|
import {isMobile, preventDefault, stopPropagation} from "@lib/html"
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Reaction from "@app/components/Reaction.svelte"
|
import Reaction from "@app/components/Reaction.svelte"
|
||||||
import EventReportDetails from "@app/components/EventReportDetails.svelte"
|
import ReportDetails from "@app/components/ReportDetails.svelte"
|
||||||
import {REACTION_KINDS} from "@app/core/state"
|
import {REACTION_KINDS} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
@@ -46,19 +46,22 @@
|
|||||||
children,
|
children,
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
const reports = deriveEvents(repository, {
|
const reports = deriveArray(
|
||||||
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
deriveEventsById({repository, filters: [{kinds: [REPORT], "#e": [event.id]}]}),
|
||||||
})
|
)
|
||||||
|
|
||||||
const reactions = deriveEvents(repository, {
|
const reactions = deriveArray(
|
||||||
filters: [{kinds: [REACTION], "#e": [event.id]}],
|
deriveEventsById({repository, filters: [{kinds: [REACTION], "#e": [event.id]}]}),
|
||||||
})
|
)
|
||||||
|
|
||||||
const zaps = deriveEventsMapped<Zap>(repository, {
|
const zaps = deriveArray(
|
||||||
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
deriveItemsByKey<Zap>({
|
||||||
itemToEvent: item => item.response,
|
repository,
|
||||||
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
|
getKey: zap => zap.response.id,
|
||||||
})
|
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
||||||
|
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const onReactionClick = (events: TrustedEvent[]) => {
|
const onReactionClick = (events: TrustedEvent[]) => {
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
const reaction = events.find(e => e.pubkey === $pubkey)
|
||||||
@@ -75,20 +78,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onReportClick = () => pushModal(EventReportDetails, {url, event})
|
const onReportClick = () => pushModal(ReportDetails, {url, event})
|
||||||
|
|
||||||
const reportReasons = $derived(uniq($reports.map(e => getTag("e", e.tags)?.[2])))
|
const reportReasons = $derived(uniq(map(e => getTag("e", e.tags)?.[2], $reports.values())))
|
||||||
|
|
||||||
const getReactionKey = (e: TrustedEvent) => getEmojiTag(e.content, e.tags)?.join("") || e.content
|
const getReactionKey = (e: TrustedEvent) => getEmojiTag(e.content, e.tags)?.join("") || e.content
|
||||||
|
|
||||||
const groupedReactions = $derived(
|
const groupedReactions = $derived(
|
||||||
groupBy(
|
groupBy(
|
||||||
getReactionKey,
|
getReactionKey,
|
||||||
uniqBy(e => `${e.pubkey}${getReactionKey(e)}`, $reactions),
|
uniqBy(e => `${e.pubkey}${getReactionKey(e)}`, $reactions.values()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const groupedZaps = $derived(groupBy(e => getReactionKey(e.request), $zaps))
|
const groupedZaps = $derived(groupBy(e => getReactionKey(e.request), $zaps.values()))
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
@@ -137,7 +140,7 @@
|
|||||||
data-tip={tooltip}
|
data-tip={tooltip}
|
||||||
class={cx(
|
class={cx(
|
||||||
reactionClass,
|
reactionClass,
|
||||||
"flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full text-xs font-normal",
|
"flex-inline btn btn-outline btn-neutral btn-xs flex items-center gap-1 rounded-full text-xs font-normal",
|
||||||
{
|
{
|
||||||
tooltip: !noTooltip && !isMobile,
|
tooltip: !noTooltip && !isMobile,
|
||||||
"border-neutral-content/20": !isOwn,
|
"border-neutral-content/20": !isOwn,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {deriveRelay} from "@welshman/app"
|
||||||
|
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
|
||||||
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
size?: number
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, size = 7, ...props}: Props = $props()
|
||||||
|
|
||||||
|
const relay = deriveRelay(url)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ImageIcon {size} alt="" src={$relay?.icon || RemoteControllerMinimalistic} class={props.class} />
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {deriveRelayDisplay} from "@welshman/app"
|
import {deriveRelayDisplay} from "@welshman/app"
|
||||||
|
|
||||||
const {url} = $props()
|
type Props = {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
const display = $derived(deriveRelayDisplay(url))
|
const display = $derived(deriveRelayDisplay(url))
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {deriveRelay} from "@welshman/app"
|
|
||||||
import Ghost from "@assets/icons/ghost-smile.svg?dataurl"
|
|
||||||
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
|
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import {deriveSpaceMembers, deriveUserRooms} from "@app/core/state"
|
import {deriveGroupListPubkeys, deriveUserRooms} from "@app/core/state"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url}: Props = $props()
|
const {url}: Props = $props()
|
||||||
const relay = deriveRelay(url)
|
|
||||||
const rooms = deriveUserRooms(url)
|
const rooms = deriveUserRooms(url)
|
||||||
const members = deriveSpaceMembers(url)
|
const favorited = deriveGroupListPubkeys(url)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="col-4 text-left">
|
<div class="col-4 text-left">
|
||||||
@@ -25,11 +23,7 @@
|
|||||||
<div class="avatar relative">
|
<div class="avatar relative">
|
||||||
<div
|
<div
|
||||||
class="center !flex h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
|
class="center !flex h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
|
||||||
{#if $relay?.icon}
|
<RelayIcon {url} />
|
||||||
<img alt="" src={$relay.icon} />
|
|
||||||
{:else}
|
|
||||||
<Icon icon={Ghost} size={5} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $rooms.includes(url)}
|
{#if $rooms.includes(url)}
|
||||||
@@ -49,10 +43,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<RelayDescription {url} />
|
<RelayDescription {url} />
|
||||||
</div>
|
</div>
|
||||||
{#if $members.length > 0}
|
{#if $favorited.size > 0}
|
||||||
<div class="row-2 card2 card2-sm bg-alt">
|
<div class="row-2 card2 card2-sm bg-alt">
|
||||||
Members:
|
Favorited By:
|
||||||
<ProfileCircles pubkeys={$members} />
|
<ProfileCircles pubkeys={Array.from($favorited)} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {REPORT} from "@welshman/util"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {deriveEventsById} from "@welshman/store"
|
||||||
|
import {repository} from "@welshman/app"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ReportItem from "@app/components/ReportItem.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const reports = deriveEventsById({
|
||||||
|
repository,
|
||||||
|
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
||||||
|
})
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const onDelete = () => {
|
||||||
|
if ($reports.size === 0) {
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Report Details</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>All reports for this event are shown below.</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
{#each $reports.values() as report (report.id)}
|
||||||
|
<div class="card2 card2-sm bg-alt">
|
||||||
|
<ReportItem {url} event={report} {onDelete} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<Button class="btn btn-primary" onclick={back}>Got it</Button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {formatTimestamp} from "@welshman/lib"
|
||||||
|
import {getTag, getIdFilters} from "@welshman/util"
|
||||||
|
import {load, LOCAL_RELAY_URL} from "@welshman/net"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {pubkey} from "@welshman/app"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
|
import NoteContent from "@app/components/NoteContent.svelte"
|
||||||
|
import ReportMenu from "@app/components/ReportMenu.svelte"
|
||||||
|
import {publishDelete, canEnforceNip70} from "@app/core/commands"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {goToEvent} from "@app/util/routes"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
onDelete?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event, onDelete}: Props = $props()
|
||||||
|
|
||||||
|
const etag = getTag("e", event.tags)
|
||||||
|
const ptag = getTag("p", event.tags)
|
||||||
|
const reason = etag?.[2] || ptag?.[2]
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
|
const onClick = (e: Event, event: TrustedEvent) => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (e.target?.classList.contains("profile-name")) {
|
||||||
|
pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||||
|
} else {
|
||||||
|
goToEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteReport = async () => {
|
||||||
|
publishDelete({event, relays: [url], protect: await shouldProtect})
|
||||||
|
onDelete?.()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<Profile pubkey={event.pubkey} {url} avatarSize={5} />
|
||||||
|
<span>
|
||||||
|
Reported this event
|
||||||
|
{#if reason}
|
||||||
|
as "{reason}"
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{#if event.pubkey === $pubkey}
|
||||||
|
<Button class="btn-default btn" onclick={deleteReport}>Delete Report</Button>
|
||||||
|
{:else}
|
||||||
|
<ReportMenu {url} {event} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if event.content}
|
||||||
|
<div class="border-l-2 border-primary pl-3">
|
||||||
|
<NoteContent {event} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="card2 card2-sm bg-alt">
|
||||||
|
{#if etag}
|
||||||
|
{#await load({relays: [url, LOCAL_RELAY_URL], filters: getIdFilters([etag[1]])})}
|
||||||
|
<p>Loading</p>
|
||||||
|
{:then reportedEvents}
|
||||||
|
{#if reportedEvents.length === 0}
|
||||||
|
<p>Unable to find reported note.</p>
|
||||||
|
{:else}
|
||||||
|
{@const event = reportedEvents[0]}
|
||||||
|
<Button class="col-2 w-full" onclick={(e: Event) => onClick(e, event)}>
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<span class="profile-name">
|
||||||
|
@<ProfileName pubkey={event.pubkey} {url} />
|
||||||
|
</span>
|
||||||
|
<span class="text-xs opacity-75">
|
||||||
|
{formatTimestamp(event.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<NoteContent {event} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{/await}
|
||||||
|
{:else if ptag}
|
||||||
|
<Profile pubkey={ptag[1]} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {getTag, ManagementMethod} from "@welshman/util"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {manageRelay, repository, displayProfileByPubkey} from "@welshman/app"
|
||||||
|
import InboxOut from "@assets/icons/inbox-out.svg?dataurl"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const etag = getTag("e", event.tags)
|
||||||
|
const ptag = getTag("p", event.tags)
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
isOpen = !isOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
isOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const dismissReport = async () => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.BanEvent,
|
||||||
|
params: [event.id, "Dismissed by admin"],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Content has successfully been deleted!"})
|
||||||
|
repository.removeEvent(event.id)
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const banContent = () => {
|
||||||
|
const [_, id, reason = ""] = etag!
|
||||||
|
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: `Delete Content`,
|
||||||
|
message: `Are you sure you want to delete this content from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.BanEvent,
|
||||||
|
params: [id, reason],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Content has successfully been deleted!"})
|
||||||
|
repository.removeEvent(id)
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const banMember = () => {
|
||||||
|
const [pubkey, reason = ""] = ptag!
|
||||||
|
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Ban User",
|
||||||
|
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.BanPubkey,
|
||||||
|
params: [pubkey, reason],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "User has successfully been banned!"})
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let isOpen = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<Button class="btn btn-circle btn-ghost btn-sm" onclick={toggleMenu}>
|
||||||
|
<Icon icon={MenuDots} />
|
||||||
|
</Button>
|
||||||
|
{#if isOpen}
|
||||||
|
<Popover hideOnClick onClose={closeMenu}>
|
||||||
|
<ul
|
||||||
|
transition:fly
|
||||||
|
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||||
|
<li>
|
||||||
|
<Button onclick={dismissReport}>
|
||||||
|
<Icon icon={InboxOut} />
|
||||||
|
Dismiss Report
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{#if etag}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={banContent}>
|
||||||
|
<Icon icon={TrashBin2} />
|
||||||
|
Remove Content
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if ptag}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={banMember}>
|
||||||
|
<Icon icon={MinusCircle} />
|
||||||
|
Ban User
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import EyeClosed from "@assets/icons/eye-closed.svg?dataurl"
|
||||||
|
import Lock from "@assets/icons/lock.svg?dataurl"
|
||||||
|
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import {deriveRoom} from "@app/core/state"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
h: any
|
||||||
|
url: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $room.isHidden}
|
||||||
|
<Button
|
||||||
|
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||||
|
data-tip="This room is not visible to non-members.">
|
||||||
|
<Icon size={4} icon={EyeClosed} />
|
||||||
|
</Button>
|
||||||
|
{:else if $room.isPrivate}
|
||||||
|
<Button
|
||||||
|
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||||
|
data-tip="Only members can view messages.">
|
||||||
|
<Icon size={4} icon={Lock} />
|
||||||
|
</Button>
|
||||||
|
{:else if $room.isRestricted}
|
||||||
|
<Button
|
||||||
|
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||||
|
data-tip="Only members can send messages.">
|
||||||
|
<Icon size={4} icon={Microphone} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
|
import type {RoomMeta} from "@welshman/util"
|
||||||
|
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
|
||||||
|
import type {Thunk} from "@welshman/app"
|
||||||
|
import {deleteRoom, waitForThunkError, repository, joinRoom, leaveRoom} from "@welshman/app"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
|
import Login3 from "@assets/icons/login-3.svg?dataurl"
|
||||||
|
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import EyeClosed from "@assets/icons/eye-closed.svg?dataurl"
|
||||||
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
|
import Lock from "@assets/icons/lock.svg?dataurl"
|
||||||
|
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
|
import RoomMembers from "@app/components/RoomMembers.svelte"
|
||||||
|
import RoomEdit from "@app/components/RoomEdit.svelte"
|
||||||
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
|
import RoomImage from "@app/components/RoomImage.svelte"
|
||||||
|
import {
|
||||||
|
deriveRoom,
|
||||||
|
deriveRoomMembers,
|
||||||
|
deriveUserIsRoomAdmin,
|
||||||
|
deriveUserRoomMembershipStatus,
|
||||||
|
MembershipStatus,
|
||||||
|
} from "@app/core/state"
|
||||||
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
const members = deriveRoomMembers(url, h)
|
||||||
|
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
|
||||||
|
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const startEdit = () => pushModal(RoomEdit, {url, h})
|
||||||
|
|
||||||
|
const handleLoading = async (f: (url: string, room: RoomMeta) => Thunk) => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await waitForThunkError(f(url, makeRoomMeta({h})))
|
||||||
|
|
||||||
|
if (message && !message.startsWith("duplicate:")) {
|
||||||
|
pushToast({theme: "error", message})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const join = () => handleLoading(joinRoom)
|
||||||
|
|
||||||
|
const leave = () => handleLoading(leaveRoom)
|
||||||
|
|
||||||
|
const showMembers = () => pushModal(RoomMembers, {url, h})
|
||||||
|
|
||||||
|
const startDelete = () =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Are you sure you want to delete this room?",
|
||||||
|
message:
|
||||||
|
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
|
||||||
|
confirm: async () => {
|
||||||
|
const thunk = deleteRoom(url, $room)
|
||||||
|
const message = await waitForThunkError(thunk)
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
repository.removeEvent(thunk.event.id)
|
||||||
|
pushToast({theme: "error", message})
|
||||||
|
} else {
|
||||||
|
goto(makeSpacePath(url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<RoomImage {url} {h} size={8} />
|
||||||
|
<div class="flex min-w-0 flex-col">
|
||||||
|
<RoomName {url} {h} class="text-2xl" />
|
||||||
|
<span class="text-primary">{displayRelayUrl(url)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
{#if $room?.isRestricted}
|
||||||
|
<Button
|
||||||
|
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||||
|
data-tip="Only members can send messages.">
|
||||||
|
<Icon size={4} icon={Microphone} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if $room?.isPrivate}
|
||||||
|
<Button
|
||||||
|
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||||
|
data-tip="Only members can view messages.">
|
||||||
|
<Icon size={4} icon={Lock} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if $room?.isHidden}
|
||||||
|
<Button
|
||||||
|
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||||
|
data-tip="This room is not visible to non-members.">
|
||||||
|
<Icon size={4} icon={EyeClosed} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if $room?.isClosed}
|
||||||
|
<Button
|
||||||
|
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
||||||
|
data-tip="Requests to join this room will be ignored.">
|
||||||
|
<Icon size={4} icon={MinusCircle} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if $room?.about}
|
||||||
|
<p>{$room.about}</p>
|
||||||
|
{/if}
|
||||||
|
{#if $members.length > 0}
|
||||||
|
<div class="card2 card2-sm bg-alt flex items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span>Members:</span>
|
||||||
|
<ProfileCircles pubkeys={$members} />
|
||||||
|
</div>
|
||||||
|
<Button class="btn btn-neutral btn-sm" onclick={showMembers}>View All</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{#if $userIsAdmin}
|
||||||
|
<Button class="btn btn-outline btn-error" onclick={startDelete}>
|
||||||
|
<Icon icon={TrashBin2} />
|
||||||
|
<span class="hidden md:inline">Delete Room</span>
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={startEdit}>
|
||||||
|
<Icon icon={Pen} />
|
||||||
|
Edit Room
|
||||||
|
</Button>
|
||||||
|
{:else if $membershipStatus === MembershipStatus.Initial}
|
||||||
|
<Button class="btn btn-neutral" disabled={loading} onclick={join}>
|
||||||
|
{#if loading}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon={Login3} />
|
||||||
|
{/if}
|
||||||
|
Join member list
|
||||||
|
</Button>
|
||||||
|
{:else if $membershipStatus === MembershipStatus.Pending}
|
||||||
|
<Button class="btn btn-neutral">
|
||||||
|
<Icon icon={ClockCircle} />
|
||||||
|
Membership pending
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button class="btn btn-neutral" disabled={loading} onclick={leave}>
|
||||||
|
{#if loading}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon={Login3} />
|
||||||
|
{/if}
|
||||||
|
Leave member list
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
@@ -2,20 +2,15 @@
|
|||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import type {RoomMeta} from "@welshman/util"
|
import type {RoomMeta} from "@welshman/util"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {deleteRoom, waitForThunkError, repository} from "@welshman/app"
|
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import RoomForm from "@app/components/RoomForm.svelte"
|
import RoomForm from "@app/components/RoomForm.svelte"
|
||||||
import {deriveRoom} from "@app/core/state"
|
import {deriveRoom} from "@app/core/state"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
import {pushModal} from "@app/util/modal"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -29,24 +24,6 @@
|
|||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, h))
|
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, h))
|
||||||
|
|
||||||
const startDelete = () =>
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: "Are you sure you want to delete this room?",
|
|
||||||
message:
|
|
||||||
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
|
|
||||||
confirm: async () => {
|
|
||||||
const thunk = deleteRoom(url, $room)
|
|
||||||
const message = await waitForThunkError(thunk)
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
repository.removeEvent(thunk.event.id)
|
|
||||||
pushToast({theme: "error", message})
|
|
||||||
} else {
|
|
||||||
goto(makeSpacePath(url))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<RoomForm {url} {onsubmit} initialValues={$room}>
|
<RoomForm {url} {onsubmit} initialValues={$room}>
|
||||||
@@ -68,15 +45,9 @@
|
|||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex gap-2">
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
<Button class="btn btn-outline btn-error" onclick={startDelete}>
|
<Spinner {loading}>Save Changes</Spinner>
|
||||||
<Icon icon={TrashBin2} />
|
</Button>
|
||||||
<span class="hidden md:inline">Delete Room</span>
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
|
||||||
<Spinner {loading}>Save Changes</Spinner>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</RoomForm>
|
</RoomForm>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||||
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
|
||||||
import {preventDefault, compressFile} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
const room = $state.snapshot(values)
|
const room = $state.snapshot(values)
|
||||||
|
|
||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
const {error, result} = await uploadFile(imageFile)
|
const {error, result} = await uploadFile(imageFile, {maxWidth: 128, maxHeight: 128})
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return pushToast({theme: "error", message: error})
|
return pushToast({theme: "error", message: error})
|
||||||
@@ -38,8 +38,6 @@
|
|||||||
|
|
||||||
room.picture = result.url
|
room.picture = result.url
|
||||||
room.pictureMeta = result.tags
|
room.pictureMeta = result.tags
|
||||||
} else if (selectedIcon) {
|
|
||||||
room.picture = selectedIcon
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createMessage = await waitForThunkError(createRoom(url, room))
|
const createMessage = await waitForThunkError(createRoom(url, room))
|
||||||
@@ -76,29 +74,34 @@
|
|||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
let imageFile = $state<File | undefined>()
|
let imageFile = $state<File | undefined>()
|
||||||
let imagePreview = $state(initialValues.picture)
|
let imagePreview = $state(initialValues.picture)
|
||||||
let selectedIcon = $state<string | undefined>()
|
|
||||||
|
|
||||||
const handleImageUpload = async (event: Event) => {
|
const handleImageUpload = async (event: Event) => {
|
||||||
const file = (event.target as HTMLInputElement).files?.[0]
|
const file = (event.target as HTMLInputElement).files?.[0]
|
||||||
|
|
||||||
if (file && file.type.startsWith("image/")) {
|
if (file && file.type.startsWith("image/")) {
|
||||||
selectedIcon = undefined
|
|
||||||
imageFile = await compressFile(file, {maxWidth: 64, maxHeight: 64})
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
|
|
||||||
reader.onload = e => {
|
reader.onload = e => {
|
||||||
|
imageFile = file
|
||||||
imagePreview = e.target?.result as string
|
imagePreview = e.target?.result as string
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.readAsDataURL(imageFile)
|
reader.readAsDataURL(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIconSelect = (iconUrl: string) => {
|
const handleIconSelect = (iconUrl: string) => {
|
||||||
imageFile = undefined
|
imagePreview = iconUrl
|
||||||
imagePreview = undefined
|
|
||||||
selectedIcon = iconUrl
|
const parts = iconUrl.split(",")
|
||||||
|
const imageData = atob(parts[1])
|
||||||
|
const result = new Uint8Array(imageData.length)
|
||||||
|
|
||||||
|
for (let n = 0; n < imageData.length; n++) {
|
||||||
|
result[n] = imageData.charCodeAt(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFile = new File([result], `icon.svg`, {type: "image/svg+xml"})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -114,12 +117,7 @@
|
|||||||
{#if imagePreview}
|
{#if imagePreview}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-sm opacity-75">Selected:</span>
|
<span class="text-sm opacity-75">Selected:</span>
|
||||||
<ImageIcon src={imagePreview} alt="Room icon preview" />
|
<ImageIcon src={imagePreview} alt="" class="rounded-lg" />
|
||||||
</div>
|
|
||||||
{:else if selectedIcon}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-sm opacity-75">Selected:</span>
|
|
||||||
<Icon icon={selectedIcon} class="h-8 w-8" />
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-sm opacity-75">No icon selected</span>
|
<span class="text-sm opacity-75">No icon selected</span>
|
||||||
@@ -146,9 +144,7 @@
|
|||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
{#if imagePreview}
|
{#if imagePreview}
|
||||||
<ImageIcon src={imagePreview} alt="Room icon preview" />
|
<ImageIcon src={imagePreview} alt="" class="rounded-lg" />
|
||||||
{:else if selectedIcon}
|
|
||||||
<Icon icon={selectedIcon} class="h-8 w-8" />
|
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon={Hashtag} />
|
<Icon icon={Hashtag} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
|
import {deriveRoom} from "@app/core/state"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
h: string
|
||||||
|
url: string
|
||||||
|
size?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h, size = 5}: Props = $props()
|
||||||
|
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $room.picture}
|
||||||
|
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
||||||
|
{:else}
|
||||||
|
<Icon icon={Hashtag} {size} />
|
||||||
|
{/if}
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
thunks,
|
thunks,
|
||||||
pubkey,
|
pubkey,
|
||||||
mergeThunks,
|
mergeThunks,
|
||||||
deriveProfile,
|
|
||||||
deriveProfileDisplay,
|
deriveProfileDisplay,
|
||||||
displayProfileByPubkey,
|
displayProfileByPubkey,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
@@ -16,12 +15,12 @@
|
|||||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||||
import ReplyAlt from "@assets/icons/reply.svg?dataurl"
|
import ReplyAlt from "@assets/icons/reply.svg?dataurl"
|
||||||
import TapTarget from "@lib/components/TapTarget.svelte"
|
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import RoomItemZapButton from "@app/components/RoomItemZapButton.svelte"
|
import RoomItemZapButton from "@app/components/RoomItemZapButton.svelte"
|
||||||
import RoomItemEmojiButton from "@app/components/RoomItemEmojiButton.svelte"
|
import RoomItemEmojiButton from "@app/components/RoomItemEmojiButton.svelte"
|
||||||
@@ -56,7 +55,6 @@
|
|||||||
const path = getRoomItemPath(url, event)
|
const path = getRoomItemPath(url, event)
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
const today = formatTimestampAsDate(now())
|
const today = formatTimestampAsDate(now())
|
||||||
const profile = deriveProfile(event.pubkey, [url])
|
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
||||||
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
||||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||||
@@ -83,7 +81,10 @@
|
|||||||
<div class="flex w-full gap-3 overflow-auto">
|
<div class="flex w-full gap-3 overflow-auto">
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<Button onclick={openProfile} class="flex items-start">
|
<Button onclick={openProfile} class="flex items-start">
|
||||||
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={8} />
|
<ProfileCircle
|
||||||
|
pubkey={event.pubkey}
|
||||||
|
class="border border-solid border-base-content"
|
||||||
|
size={8} />
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="w-8 min-w-8 max-w-8"></div>
|
<div class="w-8 min-w-8 max-w-8"></div>
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {ManagementMethod} from "@welshman/util"
|
||||||
|
import {pubkey, manageRelay, repository} from "@welshman/app"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
import Danger from "@assets/icons/danger.svg?dataurl"
|
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import EventReport from "@app/components/EventReport.svelte"
|
import Report from "@app/components/Report.svelte"
|
||||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {deriveUserIsSpaceAdmin} from "@app/core/state"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -19,9 +23,11 @@
|
|||||||
|
|
||||||
const {url, event, onClick}: Props = $props()
|
const {url, event, onClick}: Props = $props()
|
||||||
|
|
||||||
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
const report = () => {
|
const report = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(EventReport, {url, event})
|
pushModal(Report, {url, event})
|
||||||
}
|
}
|
||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
@@ -33,9 +39,29 @@
|
|||||||
onClick()
|
onClick()
|
||||||
pushModal(EventDeleteConfirm, {url, event})
|
pushModal(EventDeleteConfirm, {url, event})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showAdminDelete = () =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: `Delete Message`,
|
||||||
|
message: `Are you sure you want to delete this message from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.BanEvent,
|
||||||
|
params: [event.id],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Event has successfully been deleted!"})
|
||||||
|
repository.removeEvent(event.id)
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl">
|
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md">
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={showInfo}>
|
<Button onclick={showInfo}>
|
||||||
<Icon size={4} icon={Code2} />
|
<Icon size={4} icon={Code2} />
|
||||||
@@ -56,5 +82,13 @@
|
|||||||
Report Content
|
Report Content
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
|
{#if $userIsAdmin}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={showAdminDelete}>
|
||||||
|
<Icon size={4} icon={TrashBin2} />
|
||||||
|
Delete Message
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -58,12 +58,12 @@
|
|||||||
{#if event.pubkey === $pubkey}
|
{#if event.pubkey === $pubkey}
|
||||||
<Button class="btn btn-neutral text-error" onclick={showDelete}>
|
<Button class="btn btn-neutral text-error" onclick={showDelete}>
|
||||||
<Icon size={4} icon={TrashBin2} />
|
<Icon size={4} icon={TrashBin2} />
|
||||||
Delete
|
Delete Message
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button class="btn btn-neutral" onclick={showInfo}>
|
<Button class="btn btn-neutral" onclick={showInfo}>
|
||||||
<Icon size={4} icon={Code2} />
|
<Icon size={4} icon={Code2} />
|
||||||
Show JSON
|
Message Info
|
||||||
</Button>
|
</Button>
|
||||||
{#if path}
|
{#if path}
|
||||||
<Link class="btn btn-neutral" href={path}>
|
<Link class="btn btn-neutral" href={path}>
|
||||||
@@ -71,18 +71,18 @@
|
|||||||
View Details
|
View Details
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
<Button class="btn btn-outline btn-neutral w-full" onclick={sendReply}>
|
|
||||||
<Icon size={4} icon={Reply} />
|
|
||||||
Reply
|
|
||||||
</Button>
|
|
||||||
<Button class="btn btn-secondary w-full" onclick={showEmojiPicker}>
|
|
||||||
<Icon size={4} icon={SmileCircle} />
|
|
||||||
React
|
|
||||||
</Button>
|
|
||||||
{#if ENABLE_ZAPS}
|
{#if ENABLE_ZAPS}
|
||||||
<ZapButton replaceState {url} {event} class="btn btn-primary w-full">
|
<ZapButton replaceState {url} {event} class="btn btn-neutral w-full">
|
||||||
<Icon size={4} icon={Bolt} />
|
<Icon size={4} icon={Bolt} />
|
||||||
Zap
|
Send Zap
|
||||||
</ZapButton>
|
</ZapButton>
|
||||||
{/if}
|
{/if}
|
||||||
|
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
||||||
|
<Icon size={4} icon={Reply} />
|
||||||
|
Send Reply
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-neutral w-full" onclick={showEmojiPicker}>
|
||||||
|
<Icon size={4} icon={SmileCircle} />
|
||||||
|
Send Reaction
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {waitForThunkError, removeRoomMember} from "@welshman/app"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
|
import RoomMembersAdd from "@app/components/RoomMembersAdd.svelte"
|
||||||
|
import {deriveRoom, deriveRoomMembers, deriveUserIsRoomAdmin} from "@app/core/state"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
const members = deriveRoomMembers(url, h)
|
||||||
|
const userIsAdmin = deriveUserIsRoomAdmin(url, h)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const toggleMenu = (pubkey: string) => {
|
||||||
|
menuPubkey = menuPubkey === pubkey ? undefined : pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
menuPubkey = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const addMember = () => pushModal(RoomMembersAdd, {url, h})
|
||||||
|
|
||||||
|
const removeMember = (pubkey: string) =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Remove Member",
|
||||||
|
message: "Are you sure you want to remove this user from the room?",
|
||||||
|
confirm: async () => {
|
||||||
|
const error = await waitForThunkError(removeRoomMember(url, $room, pubkey))
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Member has successfully been removed!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let menuPubkey = $state<string | undefined>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<div class="flex min-w-0 flex-col gap-1">
|
||||||
|
<h1 class="ellipsize whitespace-nowrap text-2xl font-bold">Members</h1>
|
||||||
|
<p class="ellipsize text-sm opacity-75">of <RoomName {url} {h} /></p>
|
||||||
|
</div>
|
||||||
|
{#if $userIsAdmin}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button class="btn btn-primary" onclick={addMember}>
|
||||||
|
<Icon icon={AddCircle} />
|
||||||
|
Add members
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#each $members as pubkey (pubkey)}
|
||||||
|
<div class="card2 bg-alt relative">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<Profile {pubkey} {url} />
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
||||||
|
<Icon icon={MenuDots} />
|
||||||
|
</Button>
|
||||||
|
{#if menuPubkey === pubkey}
|
||||||
|
<Popover hideOnClick onClose={closeMenu}>
|
||||||
|
<ul
|
||||||
|
transition:fly
|
||||||
|
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={() => removeMember(pubkey)}>
|
||||||
|
<Icon icon={MinusCircle} />
|
||||||
|
Remove Member
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {addRoomMember, waitForThunkError} from "@welshman/app"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
|
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {deriveRoom} from "@app/core/state"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const addMember = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const errors = await Promise.all(
|
||||||
|
pubkeys.map(pubkey => waitForThunkError(addRoomMember(url, $room, pubkey))),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const error of errors) {
|
||||||
|
if (error) {
|
||||||
|
return pushToast({theme: "error", message: errors[0]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushToast({message: "Members have successfully been added!"})
|
||||||
|
back()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let pubkeys: string[] = $state([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Add Members</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>to <RoomName {url} {h} /></div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Search for People</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<ProfileMultiSelect bind:value={pubkeys} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={addMember} disabled={loading}>
|
||||||
|
<Spinner {loading}>Save changes</Spinner>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {roomsById, makeRoomId} from "@app/core/state"
|
import {deriveRoom} from "@app/core/state"
|
||||||
|
|
||||||
const {url, h} = $props()
|
type Props = {
|
||||||
|
url: string
|
||||||
|
h: string
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h, ...props}: Props = $props()
|
||||||
|
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{$roomsById.get(makeRoomId(url, h))?.name || h}
|
<span class="ellipsize {props.class}">
|
||||||
|
{$room?.name || h}
|
||||||
|
</span>
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
import {deriveRoom} from "@app/core/state"
|
import RoomImage from "@app/components/RoomImage.svelte"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: any
|
h: string
|
||||||
h: any
|
url: string
|
||||||
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, h}: Props = $props()
|
const {url, h, ...props}: Props = $props()
|
||||||
|
|
||||||
const room = deriveRoom(url, h)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $room.picture}
|
<div class="flex flex-grow items-center justify-between gap-4 {props.class}">
|
||||||
{@const src = $room.picture}
|
<div class="flex items-center gap-3">
|
||||||
<ImageIcon {src} alt="Room icon" />
|
<RoomImage {url} {h} />
|
||||||
{:else}
|
<div class="min-w-0 overflow-hidden text-ellipsis">
|
||||||
<Icon icon={Hashtag} />
|
<RoomName {url} {h} />
|
||||||
{/if}
|
</div>
|
||||||
<div class="min-w-0 overflow-hidden text-ellipsis">
|
</div>
|
||||||
<RoomName {url} {h} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {nsecEncode} from "nostr-tools/nip19"
|
import {nsecEncode} from "nostr-tools/nip19"
|
||||||
import {encrypt} from "nostr-tools/nip49"
|
import {encrypt} from "nostr-tools/nip49"
|
||||||
import {hexToBytes} from "@welshman/lib"
|
import {hexToBytes} from "@welshman/lib"
|
||||||
import {makeSecret} from "@welshman/signer"
|
import {makeSecret} from "@welshman/util"
|
||||||
import type {Profile} from "@welshman/util"
|
import type {Profile} from "@welshman/util"
|
||||||
import {preventDefault, downloadText} from "@lib/html"
|
import {preventDefault, downloadText} from "@lib/html"
|
||||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||||
|
|||||||
@@ -12,12 +12,29 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
|
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {removeSpaceMembership, publishLeaveRequest, removeTrustedRelay} from "@app/core/commands"
|
||||||
|
|
||||||
const {url, error} = $props()
|
const {url, error} = $props()
|
||||||
|
|
||||||
const back = () => goto("/home")
|
const back = () => goto("/home")
|
||||||
|
|
||||||
const requestAccess = () => pushModal(SpaceAccessRequest, {url})
|
const requestAccess = () => pushModal(SpaceAccessRequest, {url})
|
||||||
|
|
||||||
|
const leaveSpace = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeSpaceMembership(url)
|
||||||
|
await publishLeaveRequest({url})
|
||||||
|
await removeTrustedRelay(url)
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
goto("/home")
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(requestAccess)}>
|
<form class="column gap-4" onsubmit={preventDefault(requestAccess)}>
|
||||||
@@ -40,9 +57,14 @@
|
|||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go Home
|
Go Home
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary">
|
<div class="flex gap-2">
|
||||||
Request Access
|
<Button class="btn btn-outline btn-error" onclick={leaveSpace} disabled={loading}>
|
||||||
<Icon icon={AltArrowRight} />
|
Leave Space
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
Request Access
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
|
||||||
import {deriveRelay} from "@welshman/app"
|
|
||||||
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
url?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {url = ""}: Props = $props()
|
|
||||||
|
|
||||||
const relay = deriveRelay(url)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Avatar
|
|
||||||
icon={RemoteControllerMinimalistic}
|
|
||||||
class="!h-10 !w-10"
|
|
||||||
alt={displayRelayUrl(url)}
|
|
||||||
src={$relay?.icon} />
|
|
||||||
@@ -10,19 +10,24 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import SpaceVisitConfirm, {confirmSpaceVisit} from "@app/components/SpaceVisitConfirm.svelte"
|
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
|
||||||
import {attemptRelayAccess} from "@app/core/commands"
|
import {attemptRelayAccess} from "@app/core/commands"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const next = () => {
|
const next = async () => {
|
||||||
if (!error && Pool.get().get(url).auth.status === AuthStatus.None) {
|
if (error) {
|
||||||
pushModal(SpaceVisitConfirm, {url}, {replaceState: true})
|
return pushToast({theme: "error", message: error, timeout: 30_000})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Pool.get().get(url).auth.status === AuthStatus.None) {
|
||||||
|
pushModal(SpaceJoinConfirm, {url}, {replaceState: true})
|
||||||
} else {
|
} else {
|
||||||
confirmSpaceVisit(url)
|
await confirmSpaceJoin(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +77,7 @@
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
Go to Space
|
Join Space
|
||||||
<Icon icon={AltArrowRight} />
|
<Icon icon={AltArrowRight} />
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|||||||
@@ -2,16 +2,22 @@
|
|||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {deriveRelay} from "@welshman/app"
|
import {deriveRelay} from "@welshman/app"
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
|
import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
|
||||||
import BillList from "@assets/icons/bill-list.svg?dataurl"
|
import BillList from "@assets/icons/bill-list.svg?dataurl"
|
||||||
import Ghost from "@assets/icons/ghost-smile.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
|
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||||
|
import SpaceEdit from "@app/components/SpaceEdit.svelte"
|
||||||
import SpaceRelayStatus from "@app/components/SpaceRelayStatus.svelte"
|
import SpaceRelayStatus from "@app/components/SpaceRelayStatus.svelte"
|
||||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||||
import ProfileLatest from "@app/components/ProfileLatest.svelte"
|
import ProfileLatest from "@app/components/ProfileLatest.svelte"
|
||||||
|
import {deriveUserIsSpaceAdmin} from "@app/core/state"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -20,30 +26,37 @@
|
|||||||
const {url}: Props = $props()
|
const {url}: Props = $props()
|
||||||
const relay = deriveRelay(url)
|
const relay = deriveRelay(url)
|
||||||
const owner = $derived($relay?.pubkey)
|
const owner = $derived($relay?.pubkey)
|
||||||
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const startEdit = () => pushModal(SpaceEdit, {url, initialValues: $relay})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
<div class="relative flex gap-4">
|
<div class="flex justify-between">
|
||||||
<div class="relative">
|
<div class="relative flex gap-4">
|
||||||
<div class="avatar relative">
|
<div class="relative">
|
||||||
<div
|
<div class="avatar relative">
|
||||||
class="center !flex h-16 w-16 min-w-16 rounded-full border-2 border-solid border-base-300 bg-base-300">
|
<div
|
||||||
{#if $relay?.icon}
|
class="center !flex h-16 w-16 min-w-16 rounded-full border-2 border-solid border-base-300 bg-base-300">
|
||||||
<img alt="" src={$relay.icon} />
|
<RelayIcon {url} size={10} />
|
||||||
{:else}
|
</div>
|
||||||
<Icon icon={Ghost} size={6} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex min-w-0 flex-col gap-1">
|
||||||
|
<h1 class="ellipsize whitespace-nowrap text-2xl font-bold">
|
||||||
|
<RelayName {url} />
|
||||||
|
</h1>
|
||||||
|
<p class="ellipsize text-sm opacity-75">{displayRelayUrl(url)}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex min-w-0 flex-col gap-1">
|
{#if $userIsAdmin}
|
||||||
<h1 class="ellipsize whitespace-nowrap text-2xl font-bold">
|
<Button class="btn btn-primary" onclick={startEdit}>
|
||||||
<RelayName {url} />
|
<Icon icon={Pen} />
|
||||||
</h1>
|
Edit
|
||||||
<p class="ellipsize text-sm opacity-75">{displayRelayUrl(url)}</p>
|
</Button>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<RelayDescription {url} />
|
<RelayDescription {url} />
|
||||||
{#if $relay?.terms_of_service || $relay?.privacy_policy}
|
{#if $relay?.terms_of_service || $relay?.privacy_policy}
|
||||||
@@ -78,5 +91,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Button class="btn btn-primary" onclick={back}>Got it</Button>
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {ifLet} from "@welshman/lib"
|
||||||
|
import type {RelayProfile} from "@welshman/util"
|
||||||
|
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
||||||
|
import {manageRelay, relaysByUrl, notifyRelay, fetchRelayDirectly} from "@welshman/app"
|
||||||
|
import StickerSmileSquare from "@assets/icons/sticker-smile-square.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"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
|
import IconPickerButton from "@lib/components/IconPickerButton.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {clearModals} from "@app/util/modal"
|
||||||
|
import {uploadFile} from "@app/core/commands"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
initialValues: RelayProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, initialValues}: Props = $props()
|
||||||
|
|
||||||
|
const values = $state(initialValues)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (values.name != initialValues.name) {
|
||||||
|
const res = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.ChangeRelayName,
|
||||||
|
params: [values.name || ""],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
return pushToast({theme: "error", message: res.error})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.description != initialValues.description) {
|
||||||
|
const res = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.ChangeRelayDescription,
|
||||||
|
params: [values.description || ""],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
return pushToast({theme: "error", message: res.error})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageFile) {
|
||||||
|
const {error, result} = await uploadFile(imageFile, {maxWidth: 128, maxHeight: 128})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return pushToast({theme: "error", message: error})
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.ChangeRelayIcon,
|
||||||
|
params: [result.url],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
return pushToast({theme: "error", message: res.error})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force-reload the relay
|
||||||
|
ifLet(await fetchRelayDirectly(url), relay => {
|
||||||
|
relaysByUrl.update($relaysByUrl => {
|
||||||
|
$relaysByUrl.set(url, relay)
|
||||||
|
|
||||||
|
return new Map($relaysByUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
notifyRelay(relay)
|
||||||
|
})
|
||||||
|
|
||||||
|
pushToast({message: "Your changes have been saved!"})
|
||||||
|
clearModals()
|
||||||
|
}
|
||||||
|
|
||||||
|
const trySubmit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await submit()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let imageFile = $state<File | undefined>()
|
||||||
|
let imagePreview = $state(initialValues.icon)
|
||||||
|
|
||||||
|
const handleImageUpload = async (event: Event) => {
|
||||||
|
const file = (event.target as HTMLInputElement).files?.[0]
|
||||||
|
|
||||||
|
if (file && file.type.startsWith("image/")) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = e => {
|
||||||
|
imageFile = file
|
||||||
|
imagePreview = e.target?.result as string
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleIconSelect = (iconUrl: string) => {
|
||||||
|
imagePreview = iconUrl
|
||||||
|
|
||||||
|
const parts = iconUrl.split(",")
|
||||||
|
const imageData = atob(parts[1])
|
||||||
|
const result = new Uint8Array(imageData.length)
|
||||||
|
|
||||||
|
for (let n = 0; n < imageData.length; n++) {
|
||||||
|
result[n] = imageData.charCodeAt(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFile = new File([result], `icon.svg`, {type: "image/svg+xml"})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(trySubmit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Edit a Space</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<span class="text-primary">{displayRelayUrl(url)}</span>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Icon</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
{#if imagePreview}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm opacity-75">Selected:</span>
|
||||||
|
<ImageIcon src={imagePreview} alt="" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<span class="text-sm opacity-75">No icon selected</span>
|
||||||
|
{/if}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
|
||||||
|
<Icon icon={StickerSmileSquare} size={4} />
|
||||||
|
Select
|
||||||
|
</IconPickerButton>
|
||||||
|
<label class="btn btn-neutral btn-sm cursor-pointer">
|
||||||
|
<Icon icon={UploadMinimalistic} size={4} />
|
||||||
|
Upload
|
||||||
|
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Name</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
{#if imagePreview}
|
||||||
|
<ImageIcon src={imagePreview} alt="" />
|
||||||
|
{:else}
|
||||||
|
<Icon icon={SettingsMinimalistic} />
|
||||||
|
{/if}
|
||||||
|
<input bind:value={values.name} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Description</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<input bind:value={values.description} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Save Changes</Spinner>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
await addSpaceMembership(url)
|
await addSpaceMembership(url)
|
||||||
|
|
||||||
broadcastUserData([url])
|
broadcastUserData([url])
|
||||||
goto(makeSpacePath(url), {replaceState: true})
|
|
||||||
relaysMostlyRestricted.update(dissoc(url))
|
relaysMostlyRestricted.update(dissoc(url))
|
||||||
|
goto(makeSpacePath(url), {replaceState: true})
|
||||||
pushToast({message: "Welcome to the space!"})
|
pushToast({message: "Welcome to the space!"})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
||||||
|
import {manageRelay, displayProfileByPubkey} from "@welshman/app"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
import SpaceMembersAdd from "@app/components/SpaceMembersAdd.svelte"
|
||||||
|
import SpaceMembersBanned from "@app/components/SpaceMembersBanned.svelte"
|
||||||
|
import {
|
||||||
|
deriveSpaceMembers,
|
||||||
|
deriveSpaceBannedPubkeyItems,
|
||||||
|
deriveUserIsSpaceAdmin,
|
||||||
|
} from "@app/core/state"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const members = deriveSpaceMembers(url)
|
||||||
|
const bans = deriveSpaceBannedPubkeyItems(url)
|
||||||
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const toggleMenu = (pubkey: string) => {
|
||||||
|
menuPubkey = menuPubkey === pubkey ? undefined : pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
menuPubkey = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const showBannedPubkeyItems = () => pushModal(SpaceMembersBanned, {url})
|
||||||
|
|
||||||
|
const addMember = () => pushModal(SpaceMembersAdd, {url})
|
||||||
|
|
||||||
|
const banMember = (pubkey: string) =>
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Ban User",
|
||||||
|
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.BanPubkey,
|
||||||
|
params: [pubkey],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "User has successfully been banned!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let menuPubkey = $state<string | undefined>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<div class="flex min-w-0 flex-col gap-1">
|
||||||
|
<h1 class="ellipsize whitespace-nowrap text-2xl font-bold">Members</h1>
|
||||||
|
<p class="ellipsize text-sm opacity-75">of {displayRelayUrl(url)}</p>
|
||||||
|
</div>
|
||||||
|
{#if $userIsAdmin}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button class="btn btn-primary" onclick={addMember}>
|
||||||
|
<Icon icon={AddCircle} />
|
||||||
|
Add members
|
||||||
|
</Button>
|
||||||
|
{#if $bans.length > 0}
|
||||||
|
<Button class="btn btn-neutral" onclick={showBannedPubkeyItems}>
|
||||||
|
Banned users ({$bans.length})
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#each $members as pubkey (pubkey)}
|
||||||
|
<div class="card2 bg-alt relative">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<Profile {pubkey} {url} />
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
||||||
|
<Icon icon={MenuDots} />
|
||||||
|
</Button>
|
||||||
|
{#if menuPubkey === pubkey}
|
||||||
|
<Popover hideOnClick onClose={closeMenu}>
|
||||||
|
<ul
|
||||||
|
transition:fly
|
||||||
|
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={() => banMember(pubkey)}>
|
||||||
|
<Icon icon={MinusCircle} />
|
||||||
|
Ban User
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
||||||
|
import {manageRelay} from "@welshman/app"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const addMember = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await Promise.all(
|
||||||
|
pubkeys.map(pubkey =>
|
||||||
|
manageRelay(url, {
|
||||||
|
method: ManagementMethod.AllowPubkey,
|
||||||
|
params: [pubkey],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const {error} of results) {
|
||||||
|
if (error) {
|
||||||
|
return pushToast({theme: "error", message: error})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushToast({message: "Members have successfully been added!"})
|
||||||
|
back()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let pubkeys: string[] = $state([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Add Members</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>to {displayRelayUrl(url)}</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Search for People</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<ProfileMultiSelect bind:value={pubkeys} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={addMember} disabled={loading}>
|
||||||
|
<Spinner {loading}>Save changes</Spinner>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
||||||
|
import {manageRelay} from "@welshman/app"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
|
import Restart from "@assets/icons/restart.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
import {deriveSpaceBannedPubkeyItems} from "@app/core/state"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const bans = deriveSpaceBannedPubkeyItems(url)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const toggleMenu = (pubkey: string) => {
|
||||||
|
menuPubkey = menuPubkey === pubkey ? undefined : pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
menuPubkey = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreMember = async (pubkey: string) => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.AllowPubkey,
|
||||||
|
params: [pubkey],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "User has successfully been restored!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let menuPubkey = $state<string | undefined>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Banned users</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>on {displayRelayUrl(url)}</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
{#each $bans as { pubkey, reason } (pubkey)}
|
||||||
|
<div class="card2 bg-alt relative">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<Profile {pubkey} {url} />
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
||||||
|
<Icon icon={MenuDots} />
|
||||||
|
</Button>
|
||||||
|
{#if menuPubkey === pubkey}
|
||||||
|
<Popover hideOnClick onClose={closeMenu}>
|
||||||
|
<ul
|
||||||
|
transition:fly
|
||||||
|
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||||
|
<li>
|
||||||
|
<Button onclick={() => restoreMember(pubkey)}>
|
||||||
|
<Icon icon={Restart} />
|
||||||
|
Restore User
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Got it
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {displayRelayUrl, getTagValue, EVENT_TIME, ZAP_GOAL, THREAD} from "@welshman/util"
|
import {some} from "@welshman/lib"
|
||||||
import {deriveRelay} from "@welshman/app"
|
import {displayRelayUrl, getTagValue, EVENT_TIME, ZAP_GOAL, THREAD, REPORT} from "@welshman/util"
|
||||||
|
import {deriveRelay, pubkey} from "@welshman/app"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||||
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
|
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||||
|
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
||||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
import Login from "@assets/icons/login-3.svg?dataurl"
|
import Login from "@assets/icons/login-3.svg?dataurl"
|
||||||
import History from "@assets/icons/history.svg?dataurl"
|
import History from "@assets/icons/history.svg?dataurl"
|
||||||
import Tuning2 from "@assets/icons/tuning-2.svg?dataurl"
|
|
||||||
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic-2.svg?dataurl"
|
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic-2.svg?dataurl"
|
||||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||||
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
|
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
|
||||||
@@ -31,7 +32,8 @@
|
|||||||
import SpaceExit from "@app/components/SpaceExit.svelte"
|
import SpaceExit from "@app/components/SpaceExit.svelte"
|
||||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import ProfileList from "@app/components/ProfileList.svelte"
|
import SpaceMembers from "@app/components/SpaceMembers.svelte"
|
||||||
|
import SpaceReports from "@app/components/SpaceReports.svelte"
|
||||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||||
import Alerts from "@app/components/Alerts.svelte"
|
import Alerts from "@app/components/Alerts.svelte"
|
||||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||||
@@ -41,14 +43,14 @@
|
|||||||
ENABLE_ZAPS,
|
ENABLE_ZAPS,
|
||||||
CONTENT_KINDS,
|
CONTENT_KINDS,
|
||||||
deriveSpaceMembers,
|
deriveSpaceMembers,
|
||||||
deriveEventsForUrl,
|
|
||||||
deriveUserRooms,
|
deriveUserRooms,
|
||||||
deriveOtherRooms,
|
deriveOtherRooms,
|
||||||
userSpaceUrls,
|
userSpaceUrls,
|
||||||
hasNip29,
|
hasNip29,
|
||||||
alerts,
|
alertsById,
|
||||||
deriveUserCanCreateRoom,
|
deriveUserCanCreateRoom,
|
||||||
deriveUserIsSpaceAdmin,
|
deriveUserIsSpaceAdmin,
|
||||||
|
deriveEventsForUrl,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {notifications} from "@app/util/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
@@ -64,8 +66,11 @@
|
|||||||
const userRooms = deriveUserRooms(url)
|
const userRooms = deriveUserRooms(url)
|
||||||
const otherRooms = deriveOtherRooms(url)
|
const otherRooms = deriveOtherRooms(url)
|
||||||
const members = deriveSpaceMembers(url)
|
const members = deriveSpaceMembers(url)
|
||||||
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
|
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
const reports = deriveEventsForUrl(url, [{kinds: [REPORT]}])
|
||||||
|
const hasAlerts = $derived(
|
||||||
|
some(a => getTagValue("feed", a.tags)?.includes(url), $alertsById.values()),
|
||||||
|
)
|
||||||
|
|
||||||
const spaceKinds = derived(
|
const spaceKinds = derived(
|
||||||
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
|
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
|
||||||
@@ -82,12 +87,9 @@
|
|||||||
|
|
||||||
const showDetail = () => pushModal(SpaceDetail, {url}, {replaceState})
|
const showDetail = () => pushModal(SpaceDetail, {url}, {replaceState})
|
||||||
|
|
||||||
const showMembers = () =>
|
const showMembers = () => pushModal(SpaceMembers, {url}, {replaceState})
|
||||||
pushModal(
|
|
||||||
ProfileList,
|
const showReports = () => pushModal(SpaceReports, {url}, {replaceState})
|
||||||
{url, pubkeys: $members, title: `Members of`, subtitle: displayRelayUrl(url)},
|
|
||||||
{replaceState},
|
|
||||||
)
|
|
||||||
|
|
||||||
const canCreateRoom = deriveUserCanCreateRoom(url)
|
const canCreateRoom = deriveUserCanCreateRoom(url)
|
||||||
|
|
||||||
@@ -133,7 +135,7 @@
|
|||||||
<Popover hideOnClick onClose={toggleMenu}>
|
<Popover hideOnClick onClose={toggleMenu}>
|
||||||
<ul
|
<ul
|
||||||
transition:fly
|
transition:fly
|
||||||
class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-xl">
|
class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={createInvite}>
|
<Button onclick={createInvite}>
|
||||||
<Icon icon={LinkRound} />
|
<Icon icon={LinkRound} />
|
||||||
@@ -154,12 +156,13 @@
|
|||||||
</li>
|
</li>
|
||||||
{#if $userIsAdmin}
|
{#if $userIsAdmin}
|
||||||
<li>
|
<li>
|
||||||
<Link external href="https://landlubber.coracle.social">
|
<Button onclick={showReports}>
|
||||||
<Icon icon={Tuning2} />
|
<Icon icon={Danger} />
|
||||||
Manage Space
|
View Reports ({$reports.length})
|
||||||
</Link>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{:else if $relay?.pubkey}
|
{/if}
|
||||||
|
{#if $relay?.pubkey && $relay.pubkey !== $pubkey}
|
||||||
<li>
|
<li>
|
||||||
<Link href={makeChatPath([$relay.pubkey])}>
|
<Link href={makeChatPath([$relay.pubkey])}>
|
||||||
<Icon icon={Letter} />
|
<Icon icon={Letter} />
|
||||||
@@ -184,7 +187,7 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto">
|
<div class="flex max-h-[calc(100vh-250px)] min-h-0 flex-col gap-1 overflow-auto">
|
||||||
{#if hasNip29($relay)}
|
{#if hasNip29($relay)}
|
||||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
|
<SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
|
||||||
<Icon icon={History} /> Recent Activity
|
<Icon icon={History} /> Recent Activity
|
||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import MenuSpace from "@app/components/MenuSpace.svelte"
|
import SpaceMenu from "@app/components/SpaceMenu.svelte"
|
||||||
import {notifications} from "@app/util/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
import {pushDrawer} from "@app/util/modal"
|
import {pushDrawer} from "@app/util/modal"
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
const status = deriveSocketStatus(url)
|
const status = deriveSocketStatus(url)
|
||||||
|
|
||||||
const openMenu = () => pushDrawer(MenuSpace, {url})
|
const openMenu = () => pushDrawer(SpaceMenu, {url})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button onclick={openMenu} class="btn btn-neutral btn-sm relative md:hidden">
|
<Button onclick={openMenu} class="btn btn-neutral btn-sm relative md:hidden">
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {REPORT, displayRelayUrl} from "@welshman/util"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ReportItem from "@app/components/ReportItem.svelte"
|
||||||
|
import {deriveEventsForUrl} from "@app/core/state"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const reports = deriveEventsForUrl(url, [{kinds: [REPORT]}])
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<div class="flex min-w-0 flex-col gap-1">
|
||||||
|
<h1 class="ellipsize whitespace-nowrap text-2xl font-bold">Reports</h1>
|
||||||
|
<p class="ellipsize text-sm opacity-75">on {displayRelayUrl(url)}</p>
|
||||||
|
</div>
|
||||||
|
{#each $reports as event (event.id)}
|
||||||
|
<ReportItem {url} {event} />
|
||||||
|
{/each}
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<script module lang="ts">
|
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
|
||||||
|
|
||||||
export const confirmSpaceVisit = (url: string) => {
|
|
||||||
goto(makeSpacePath(url), {replaceState: true})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {url}: Props = $props()
|
|
||||||
|
|
||||||
const confirm = () => confirmSpaceVisit(url)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Confirm
|
|
||||||
{confirm}
|
|
||||||
message="This space does not appear to limit who can post to it. This can result in a large amount of spam or other objectionable content. Continue?" />
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card2 bg-alt col-2 shadow-2xl">
|
<div class="card2 bg-alt col-2 shadow-lg">
|
||||||
<p>
|
<p>
|
||||||
Failed to publish to {displayRelayUrl(url)}: {message}.
|
Failed to publish to {displayRelayUrl(url)}: {message}.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import WalletAsReceivingAddress from "@app/components/WalletAsReceivingAddress.svelte"
|
import WalletAsReceivingAddress from "@app/components/WalletAsReceivingAddress.svelte"
|
||||||
import Divider from "@src/lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Invoice} from "@getalby/lightning-tools/bolt11"
|
||||||
|
import {debounce} from "throttle-debounce"
|
||||||
|
import {session} from "@welshman/app"
|
||||||
|
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Scanner from "@lib/components/Scanner.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {payInvoice} from "@app/core/commands"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {clearModals} from "@app/util/modal"
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const onScan = debounce(1000, async (data: string) => {
|
||||||
|
invoice = new Invoice({pr: data})
|
||||||
|
sats = invoice.satoshi || 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const confirm = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await payInvoice(invoice!.paymentRequest, sats * 1000)
|
||||||
|
|
||||||
|
pushToast({message: `Payment sent!`})
|
||||||
|
clearModals()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
|
||||||
|
const message = String(e).replace(/^.*Error: /, "")
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to send payment: ${message}`,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let invoice: Invoice | undefined = $state()
|
||||||
|
let sats = $state(0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Pay with Lightning</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
Use your Nostr wallet to send Bitcoin payments over lightning.
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
{#if invoice}
|
||||||
|
<div class="card2 bg-alt flex flex-col gap-2">
|
||||||
|
{#if $session?.wallet?.type === "webln" && invoice.satoshi === 0}
|
||||||
|
<p class="text-sm opacity-75">
|
||||||
|
Uh oh! It looks like your current wallet doesn't support invoices without an amount. See
|
||||||
|
if you can get a lightning invoice with a pre-set amount.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
Amount (satoshis)
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="flex flex-grow justify-end">
|
||||||
|
<label class="input input-bordered flex items-center gap-2">
|
||||||
|
<Icon icon={Bolt} />
|
||||||
|
<input
|
||||||
|
bind:value={sats}
|
||||||
|
type="number"
|
||||||
|
class="w-14"
|
||||||
|
disabled={invoice!.satoshi > 0} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<p class="text-sm opacity-75">
|
||||||
|
You're about to pay a bitcoin lightning invoice with the following description:
|
||||||
|
<strong>{invoice.description || "[no description]"}</strong>"
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Scanner onscan={onScan} />
|
||||||
|
<p class="text-center text-sm opacity-75">
|
||||||
|
To make a payment, scan a lightning invoice with your camera.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={confirm} disabled={!invoice || sats === 0 || loading}>
|
||||||
|
{#if loading}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon={Bolt} />
|
||||||
|
{/if}
|
||||||
|
Confirm Payment
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
+38
-39
@@ -31,7 +31,7 @@ import {
|
|||||||
DELETE,
|
DELETE,
|
||||||
REPORT,
|
REPORT,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
INBOX_RELAYS,
|
MESSAGING_RELAYS,
|
||||||
RELAYS,
|
RELAYS,
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
REACTION,
|
REACTION,
|
||||||
@@ -46,7 +46,6 @@ import {
|
|||||||
APP_DATA,
|
APP_DATA,
|
||||||
isSignedEvent,
|
isSignedEvent,
|
||||||
makeEvent,
|
makeEvent,
|
||||||
displayProfile,
|
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
makeList,
|
makeList,
|
||||||
addToListPublicly,
|
addToListPublicly,
|
||||||
@@ -79,21 +78,21 @@ import {
|
|||||||
session,
|
session,
|
||||||
repository,
|
repository,
|
||||||
publishThunk,
|
publishThunk,
|
||||||
profilesByPubkey,
|
|
||||||
tagEvent,
|
tagEvent,
|
||||||
tagEventForReaction,
|
tagEventForReaction,
|
||||||
userRelaySelections,
|
userRelayList,
|
||||||
userInboxRelaySelections,
|
userMessagingRelayList,
|
||||||
nip44EncryptToSelf,
|
nip44EncryptToSelf,
|
||||||
dropSession,
|
dropSession,
|
||||||
tagEventForComment,
|
tagEventForComment,
|
||||||
tagEventForQuote,
|
tagEventForQuote,
|
||||||
waitForThunkError,
|
waitForThunkError,
|
||||||
getPubkeyRelays,
|
getPubkeyRelays,
|
||||||
userBlossomServers,
|
userBlossomServerList,
|
||||||
shouldUnwrap,
|
shouldUnwrap,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {compressFile} from "@src/lib/html"
|
import {compressFile} from "@lib/html"
|
||||||
|
import {kv, db} from "@app/core/storage"
|
||||||
import type {SettingsValues, Alert} from "@app/core/state"
|
import type {SettingsValues, Alert} from "@app/core/state"
|
||||||
import {
|
import {
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
@@ -105,13 +104,11 @@ import {
|
|||||||
userSpaceUrls,
|
userSpaceUrls,
|
||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
getSetting,
|
getSetting,
|
||||||
userInboxRelays,
|
userGroupList,
|
||||||
userGroupSelections,
|
|
||||||
shouldIgnoreError,
|
shouldIgnoreError,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {loadAlertStatuses} from "@app/core/requests"
|
import {loadAlertStatuses} from "@app/core/requests"
|
||||||
import {platform, platformName, getPushInfo} from "@app/util/push"
|
import {platform, platformName, getPushInfo} from "@app/util/push"
|
||||||
import {preferencesStorageProvider, Collection} from "@src/lib/storage"
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
@@ -122,13 +119,6 @@ export const getPubkeyHints = (pubkey: string) => {
|
|||||||
return hints
|
return hints
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPubkeyPetname = (pubkey: string) => {
|
|
||||||
const profile = profilesByPubkey.get().get(pubkey)
|
|
||||||
const display = displayProfile(profile)
|
|
||||||
|
|
||||||
return display
|
|
||||||
}
|
|
||||||
|
|
||||||
export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => {
|
export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => {
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const nevent = nip19.neventEncode({
|
const nevent = nip19.neventEncode({
|
||||||
@@ -156,15 +146,15 @@ export const logout = async () => {
|
|||||||
|
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
|
|
||||||
await preferencesStorageProvider.clear()
|
await kv.clear()
|
||||||
await Collection.clearAll()
|
await db.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Synchronization
|
// Synchronization
|
||||||
|
|
||||||
export const broadcastUserData = async (relays: string[]) => {
|
export const broadcastUserData = async (relays: string[]) => {
|
||||||
const authors = [pubkey.get()!]
|
const authors = [pubkey.get()!]
|
||||||
const kinds = [RELAYS, INBOX_RELAYS, FOLLOWS, PROFILE]
|
const kinds = [RELAYS, MESSAGING_RELAYS, FOLLOWS, PROFILE]
|
||||||
const events = repository.query([{kinds, authors}])
|
const events = repository.query([{kinds, authors}])
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
@@ -177,7 +167,7 @@ export const broadcastUserData = async (relays: string[]) => {
|
|||||||
// List updates
|
// List updates
|
||||||
|
|
||||||
export const addSpaceMembership = async (url: string) => {
|
export const addSpaceMembership = async (url: string) => {
|
||||||
const list = get(userGroupSelections) || makeList({kind: ROOMS})
|
const list = get(userGroupList) || makeList({kind: ROOMS})
|
||||||
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
|
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
|
||||||
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||||
|
|
||||||
@@ -185,8 +175,8 @@ export const addSpaceMembership = async (url: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const removeSpaceMembership = async (url: string) => {
|
export const removeSpaceMembership = async (url: string) => {
|
||||||
const list = get(userGroupSelections) || makeList({kind: ROOMS})
|
const list = get(userGroupList) || makeList({kind: ROOMS})
|
||||||
const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
|
const pred = (t: string[]) => normalizeRelayUrl(t[t[0] === "r" ? 1 : 2]) === url
|
||||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||||
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||||
|
|
||||||
@@ -194,7 +184,7 @@ export const removeSpaceMembership = async (url: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const addRoomMembership = async (url: string, h: string) => {
|
export const addRoomMembership = async (url: string, h: string) => {
|
||||||
const list = get(userGroupSelections) || makeList({kind: ROOMS})
|
const list = get(userGroupList) || makeList({kind: ROOMS})
|
||||||
const newTags = [
|
const newTags = [
|
||||||
["r", url],
|
["r", url],
|
||||||
["group", h, url],
|
["group", h, url],
|
||||||
@@ -206,7 +196,7 @@ export const addRoomMembership = async (url: string, h: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const removeRoomMembership = async (url: string, h: string) => {
|
export const removeRoomMembership = async (url: string, h: string) => {
|
||||||
const list = get(userGroupSelections) || makeList({kind: ROOMS})
|
const list = get(userGroupList) || makeList({kind: ROOMS})
|
||||||
const pred = (t: string[]) => equals(["group", h, url], t.slice(0, 3))
|
const pred = (t: string[]) => equals(["group", h, url], t.slice(0, 3))
|
||||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||||
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||||
@@ -215,7 +205,7 @@ export const removeRoomMembership = async (url: string, h: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
|
export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
|
||||||
const list = get(userRelaySelections) || makeList({kind: RELAYS})
|
const list = get(userRelayList) || makeList({kind: RELAYS})
|
||||||
const tags = getRelayTags(getListTags(list)).filter(t => normalizeRelayUrl(t[1]) !== url)
|
const tags = getRelayTags(getListTags(list)).filter(t => normalizeRelayUrl(t[1]) !== url)
|
||||||
|
|
||||||
if (read && write) {
|
if (read && write) {
|
||||||
@@ -232,10 +222,10 @@ export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
export const setMessagingRelayPolicy = (url: string, enabled: boolean) => {
|
||||||
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
|
const list = get(userMessagingRelayList) || makeList({kind: MESSAGING_RELAYS})
|
||||||
|
|
||||||
// Only update inbox policies if they already exist or we're adding them
|
// Only update messaging policies if they already exist or we're adding them
|
||||||
if (enabled || getRelaysFromList(list).includes(url)) {
|
if (enabled || getRelaysFromList(list).includes(url)) {
|
||||||
const tags = getRelayTags(getListTags(list)).filter(t => normalizeRelayUrl(t[1]) !== url)
|
const tags = getRelayTags(getListTags(list)).filter(t => normalizeRelayUrl(t[1]) !== url)
|
||||||
|
|
||||||
@@ -538,11 +528,13 @@ export const createDmAlert = async () => {
|
|||||||
shouldUnwrap.set(true)
|
shouldUnwrap.set(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const $pubkey = pubkey.get()!
|
||||||
|
|
||||||
return createAlert({
|
return createAlert({
|
||||||
description: `for direct messages.`,
|
description: `for direct messages.`,
|
||||||
feed: makeIntersectionFeed(
|
feed: makeIntersectionFeed(
|
||||||
feedFromFilters([{kinds: [WRAP], "#p": [pubkey.get()!]}]),
|
feedFromFilters([{kinds: [WRAP], "#p": [$pubkey]}]),
|
||||||
makeRelayFeed(...get(userInboxRelays)),
|
makeRelayFeed(...getPubkeyRelays($pubkey, RelayMode.Messaging)),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -592,7 +584,7 @@ export const publishLeaveRequest = (params: LeaveRequestParams) =>
|
|||||||
|
|
||||||
export const getWebLn = () => (window as any).webln
|
export const getWebLn = () => (window as any).webln
|
||||||
|
|
||||||
export const payInvoice = async (invoice: string) => {
|
export const payInvoice = async (invoice: string, msats?: number) => {
|
||||||
const $session = session.get()
|
const $session = session.get()
|
||||||
|
|
||||||
if (!$session?.wallet) {
|
if (!$session?.wallet) {
|
||||||
@@ -600,8 +592,11 @@ export const payInvoice = async (invoice: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($session.wallet.type === "nwc") {
|
if ($session.wallet.type === "nwc") {
|
||||||
return new nwc.NWCClient($session.wallet.info).payInvoice({invoice})
|
const params: {invoice: string; amount?: number} = {invoice}
|
||||||
|
if (msats) params.amount = msats
|
||||||
|
return new nwc.NWCClient($session.wallet.info).payInvoice(params)
|
||||||
} else if ($session.wallet.type === "webln") {
|
} else if ($session.wallet.type === "webln") {
|
||||||
|
if (msats) throw new Error("Unable to pay zero invoices with webln")
|
||||||
return getWebLn()
|
return getWebLn()
|
||||||
.enable()
|
.enable()
|
||||||
.then(() => getWebLn().sendPayment(invoice))
|
.then(() => getWebLn().sendPayment(invoice))
|
||||||
@@ -612,7 +607,7 @@ export const payInvoice = async (invoice: string) => {
|
|||||||
|
|
||||||
export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http"))
|
export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http"))
|
||||||
|
|
||||||
export const hasBlossomSupport = simpleCache(async ([url]: [string]) => {
|
export const fetchHasBlossomSupport = async (url: string) => {
|
||||||
const server = normalizeBlossomUrl(url)
|
const server = normalizeBlossomUrl(url)
|
||||||
const $signer = signer.get() || Nip01Signer.ephemeral()
|
const $signer = signer.get() || Nip01Signer.ephemeral()
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
@@ -633,7 +628,9 @@ export const hasBlossomSupport = simpleCache(async ([url]: [string]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export const hasBlossomSupport = simpleCache(([url]: [string]) => fetchHasBlossomSupport(url))
|
||||||
|
|
||||||
export type GetBlossomServerOptions = {
|
export type GetBlossomServerOptions = {
|
||||||
url?: string
|
url?: string
|
||||||
@@ -646,7 +643,7 @@ export const getBlossomServer = async (options: GetBlossomServerOptions = {}) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userUrls = getTagValues("server", getListTags(userBlossomServers.get()))
|
const userUrls = getTagValues("server", getListTags(get(userBlossomServerList)))
|
||||||
|
|
||||||
for (const url of userUrls) {
|
for (const url of userUrls) {
|
||||||
return normalizeBlossomUrl(url)
|
return normalizeBlossomUrl(url)
|
||||||
@@ -658,6 +655,8 @@ export const getBlossomServer = async (options: GetBlossomServerOptions = {}) =>
|
|||||||
export type UploadFileOptions = {
|
export type UploadFileOptions = {
|
||||||
url?: string
|
url?: string
|
||||||
encrypt?: boolean
|
encrypt?: boolean
|
||||||
|
maxWidth?: number
|
||||||
|
maxHeight?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UploadFileResult = {
|
export type UploadFileResult = {
|
||||||
@@ -669,8 +668,8 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
|||||||
try {
|
try {
|
||||||
const {name, type} = file
|
const {name, type} = file
|
||||||
|
|
||||||
if (!type.match("image/(webp|gif)")) {
|
if (!type.match("image/(webp|gif|svg)")) {
|
||||||
file = await compressFile(file)
|
file = await compressFile(file, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags: string[][] = []
|
const tags: string[][] = []
|
||||||
@@ -701,7 +700,7 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
|||||||
let {uploaded, url, ...task} = parseJson(text) || {}
|
let {uploaded, url, ...task} = parseJson(text) || {}
|
||||||
|
|
||||||
if (!uploaded) {
|
if (!uploaded) {
|
||||||
return {error: text}
|
return {error: text || `Failed to upload file (HTTP ${res.status})`}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always append correct file extension if we encrypted the file, or if it's missing
|
// Always append correct file extension if we encrypted the file, or if it's missing
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
sortBy,
|
sortBy,
|
||||||
now,
|
now,
|
||||||
on,
|
on,
|
||||||
isNotNil,
|
isDefined,
|
||||||
filterVals,
|
filterVals,
|
||||||
fromPairs,
|
fromPairs,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
@@ -47,10 +47,9 @@ export const makeFeed = ({
|
|||||||
element: HTMLElement
|
element: HTMLElement
|
||||||
onExhausted?: () => void
|
onExhausted?: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const initialEvents = getEventsForUrl(url, filters)
|
const seen = new Set<string>()
|
||||||
const seen = new Set(initialEvents.map(e => e.id))
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const buffer = writable(initialEvents)
|
const buffer = writable<TrustedEvent[]>([])
|
||||||
const events = writable<TrustedEvent[]>([])
|
const events = writable<TrustedEvent[]>([])
|
||||||
|
|
||||||
const insertEvent = (event: TrustedEvent) => {
|
const insertEvent = (event: TrustedEvent) => {
|
||||||
@@ -121,6 +120,10 @@ export const makeFeed = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const event of getEventsForUrl(url, filters)) {
|
||||||
|
insertEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events,
|
events,
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
@@ -144,7 +147,6 @@ export const makeCalendarFeed = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const interval = int(5, DAY)
|
const interval = int(5, DAY)
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const initialEvents = getEventsForUrl(url, filters)
|
|
||||||
|
|
||||||
let exhaustedScrollers = 0
|
let exhaustedScrollers = 0
|
||||||
let backwardWindow = [now() - interval, now()]
|
let backwardWindow = [now() - interval, now()]
|
||||||
@@ -154,7 +156,7 @@ export const makeCalendarFeed = ({
|
|||||||
|
|
||||||
const getEnd = (event: TrustedEvent) => parseInt(getTagValue("end", event.tags) || "")
|
const getEnd = (event: TrustedEvent) => parseInt(getTagValue("end", event.tags) || "")
|
||||||
|
|
||||||
const events = writable(sortBy(getStart, initialEvents))
|
const events = writable(sortBy(getStart, getEventsForUrl(url, filters)))
|
||||||
|
|
||||||
const insertEvent = (event: TrustedEvent) => {
|
const insertEvent = (event: TrustedEvent) => {
|
||||||
const start = getStart(event)
|
const start = getStart(event)
|
||||||
@@ -279,6 +281,6 @@ export const requestRelayClaim = async (url: string) => {
|
|||||||
|
|
||||||
export const requestRelayClaims = async (urls: string[]) =>
|
export const requestRelayClaims = async (urls: string[]) =>
|
||||||
filterVals(
|
filterVals(
|
||||||
isNotNil,
|
isDefined,
|
||||||
fromPairs(await Promise.all(urls.map(async url => [url, await requestRelayClaim(url)]))),
|
fromPairs(await Promise.all(urls.map(async url => [url, await requestRelayClaim(url)]))),
|
||||||
)
|
)
|
||||||
|
|||||||
+313
-326
@@ -1,33 +1,33 @@
|
|||||||
import twColors from "tailwindcss/colors"
|
import twColors from "tailwindcss/colors"
|
||||||
import {Capacitor} from "@capacitor/core"
|
import {Capacitor} from "@capacitor/core"
|
||||||
import {get, derived, writable} from "svelte/store"
|
import {get, derived, readable, writable} from "svelte/store"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {
|
import {
|
||||||
on,
|
on,
|
||||||
gt,
|
gt,
|
||||||
max,
|
max,
|
||||||
|
find,
|
||||||
spec,
|
spec,
|
||||||
call,
|
call,
|
||||||
first,
|
first,
|
||||||
assoc,
|
|
||||||
remove,
|
|
||||||
uniqBy,
|
uniqBy,
|
||||||
sortBy,
|
sortBy,
|
||||||
|
append,
|
||||||
sort,
|
sort,
|
||||||
prop,
|
|
||||||
uniq,
|
uniq,
|
||||||
pushToMapKey,
|
indexBy,
|
||||||
|
partition,
|
||||||
shuffle,
|
shuffle,
|
||||||
parseJson,
|
parseJson,
|
||||||
memoize,
|
memoize,
|
||||||
addToMapKey,
|
addToMapKey,
|
||||||
identity,
|
identity,
|
||||||
groupBy,
|
|
||||||
always,
|
always,
|
||||||
tryCatch,
|
tryCatch,
|
||||||
fromPairs,
|
fromPairs,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import type {Socket} from "@welshman/net"
|
import type {Override} from "@welshman/lib"
|
||||||
|
import type {RepositoryUpdate} from "@welshman/net"
|
||||||
import {
|
import {
|
||||||
Pool,
|
Pool,
|
||||||
load,
|
load,
|
||||||
@@ -37,7 +37,18 @@ import {
|
|||||||
SocketEvent,
|
SocketEvent,
|
||||||
netContext,
|
netContext,
|
||||||
} from "@welshman/net"
|
} from "@welshman/net"
|
||||||
import {collection, custom, throttled, deriveEvents, deriveEventsMapped} from "@welshman/store"
|
import {
|
||||||
|
getter,
|
||||||
|
throttled,
|
||||||
|
deriveArray,
|
||||||
|
makeDeriveEvent,
|
||||||
|
makeLoadItem,
|
||||||
|
makeDeriveItem,
|
||||||
|
deriveItemsByKey,
|
||||||
|
deriveEventsByIdByUrl,
|
||||||
|
deriveEventsByIdForUrl,
|
||||||
|
getEventsByIdForUrl,
|
||||||
|
} from "@welshman/store"
|
||||||
import {isKindFeed, findFeed} from "@welshman/feeds"
|
import {isKindFeed, findFeed} from "@welshman/feeds"
|
||||||
import {
|
import {
|
||||||
ALERT_ANDROID,
|
ALERT_ANDROID,
|
||||||
@@ -72,16 +83,14 @@ import {
|
|||||||
ROOMS,
|
ROOMS,
|
||||||
THREAD,
|
THREAD,
|
||||||
WRAP,
|
WRAP,
|
||||||
|
PROFILE,
|
||||||
ZAP_GOAL,
|
ZAP_GOAL,
|
||||||
ZAP_REQUEST,
|
ZAP_REQUEST,
|
||||||
ZAP_RESPONSE,
|
ZAP_RESPONSE,
|
||||||
asDecryptedEvent,
|
asDecryptedEvent,
|
||||||
displayProfile,
|
|
||||||
getGroupTags,
|
getGroupTags,
|
||||||
getIdFilters,
|
|
||||||
getListTags,
|
getListTags,
|
||||||
getPubkeyTagValues,
|
getPubkeyTagValues,
|
||||||
getRelaysFromList,
|
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
getTagValue,
|
getTagValue,
|
||||||
getTagValues,
|
getTagValues,
|
||||||
@@ -89,47 +98,33 @@ import {
|
|||||||
makeEvent,
|
makeEvent,
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
readList,
|
readList,
|
||||||
RelayMode,
|
|
||||||
verifyEvent,
|
verifyEvent,
|
||||||
readRoomMeta,
|
readRoomMeta,
|
||||||
makeRoomMeta,
|
makeRoomMeta,
|
||||||
ManagementMethod,
|
ManagementMethod,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {
|
import type {TrustedEvent, RelayProfile, PublishedRoomMeta, List, Filter} from "@welshman/util"
|
||||||
TrustedEvent,
|
|
||||||
RelayProfile,
|
|
||||||
PublishedList,
|
|
||||||
PublishedRoomMeta,
|
|
||||||
List,
|
|
||||||
Filter,
|
|
||||||
} from "@welshman/util"
|
|
||||||
import {decrypt} from "@welshman/signer"
|
import {decrypt} from "@welshman/signer"
|
||||||
import {routerContext, Router} from "@welshman/router"
|
import {routerContext, Router} from "@welshman/router"
|
||||||
import {
|
import {
|
||||||
pubkey,
|
pubkey,
|
||||||
repository,
|
repository,
|
||||||
profilesByPubkey,
|
|
||||||
tracker,
|
tracker,
|
||||||
makeTrackerStore,
|
|
||||||
makeRepositoryStore,
|
|
||||||
createSearch,
|
createSearch,
|
||||||
userFollows,
|
userFollowList,
|
||||||
ensurePlaintext,
|
ensurePlaintext,
|
||||||
thunks,
|
|
||||||
sign,
|
sign,
|
||||||
signer,
|
signer,
|
||||||
makeOutboxLoader,
|
makeOutboxLoader,
|
||||||
appContext,
|
appContext,
|
||||||
getThunkError,
|
getThunkError,
|
||||||
publishThunk,
|
publishThunk,
|
||||||
userRelaySelections,
|
|
||||||
userInboxRelaySelections,
|
|
||||||
deriveRelay,
|
deriveRelay,
|
||||||
makeUserData,
|
makeUserData,
|
||||||
makeUserLoader,
|
makeUserLoader,
|
||||||
manageRelay,
|
manageRelay,
|
||||||
|
displayProfileByPubkey,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {Thunk} from "@welshman/app"
|
|
||||||
|
|
||||||
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
||||||
|
|
||||||
@@ -208,87 +203,31 @@ export const entityLink = (entity: string) => `https://coracle.social/${entity}`
|
|||||||
export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) =>
|
export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) =>
|
||||||
entityLink(nip19.nprofileEncode({pubkey, relays}))
|
entityLink(nip19.nprofileEncode({pubkey, relays}))
|
||||||
|
|
||||||
export const bootstrapPubkeys = derived(userFollows, $userFollows => {
|
export const bootstrapPubkeys = derived(userFollowList, $userFollowList => {
|
||||||
const appPubkeys = DEFAULT_PUBKEYS.split(",")
|
const appPubkeys = DEFAULT_PUBKEYS.split(",")
|
||||||
const userPubkeys = shuffle(getPubkeyTagValues(getListTags($userFollows)))
|
const userPubkeys = shuffle(getPubkeyTagValues(getListTags($userFollowList)))
|
||||||
|
|
||||||
return userPubkeys.length > 5 ? userPubkeys : [...userPubkeys, ...appPubkeys]
|
return userPubkeys.length > 5 ? userPubkeys : [...userPubkeys, ...appPubkeys]
|
||||||
})
|
})
|
||||||
|
|
||||||
export const trackerStore = makeTrackerStore()
|
export const deriveEvent = makeDeriveEvent({
|
||||||
|
repository,
|
||||||
export const repositoryStore = makeRepositoryStore()
|
includeDeleted: true,
|
||||||
|
onDerive: (filters: Filter[], relays: string[]) => load({filters, relays}),
|
||||||
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
|
||||||
let attempted = false
|
|
||||||
|
|
||||||
const filters = getIdFilters([idOrAddress])
|
|
||||||
const relays = [...hints, ...INDEXER_RELAYS]
|
|
||||||
|
|
||||||
return derived(
|
|
||||||
deriveEvents(repository, {filters, includeDeleted: true}),
|
|
||||||
(events: TrustedEvent[]) => {
|
|
||||||
if (!attempted && events.length === 0) {
|
|
||||||
load({relays, filters})
|
|
||||||
attempted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return events[0]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getUrlsForEvent = derived([trackerStore, thunks], ([$tracker, $thunks]) => {
|
|
||||||
const getThunksByEventId = memoize(() => {
|
|
||||||
const thunksByEventId = new Map<string, Thunk[]>()
|
|
||||||
|
|
||||||
for (const thunk of $thunks) {
|
|
||||||
pushToMapKey(thunksByEventId, thunk.event.id, thunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
return thunksByEventId
|
|
||||||
})
|
|
||||||
|
|
||||||
return (id: string) => {
|
|
||||||
const urls = Array.from($tracker.getRelays(id))
|
|
||||||
|
|
||||||
for (const thunk of getThunksByEventId().get(id) || []) {
|
|
||||||
for (const url of thunk.options.relays) {
|
|
||||||
urls.push(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return uniq(urls)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getEventsForUrl = (url: string, filters: Filter[]) => {
|
export const getEventsForUrl = (url: string, filters: Filter[]) =>
|
||||||
const ids = uniq([
|
getEventsByIdForUrl({url, tracker, repository, filters}).values()
|
||||||
...tracker.getIds(url),
|
|
||||||
...get(thunks)
|
|
||||||
.filter(t => t.options.relays.includes(url))
|
|
||||||
.map(t => t.event.id),
|
|
||||||
])
|
|
||||||
|
|
||||||
return repository.query(filters.map(assoc("ids", ids)))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
|
export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
|
||||||
derived([trackerStore, thunks], ([$tracker, $thunks]) => {
|
deriveArray(deriveEventsByIdForUrl({url, tracker, repository, filters}))
|
||||||
const ids = uniq([
|
|
||||||
...$tracker.getIds(url),
|
|
||||||
...$thunks.filter(t => t.options.relays.includes(url)).map(t => t.event.id),
|
|
||||||
])
|
|
||||||
|
|
||||||
return repository.query(filters.map(assoc("ids", ids)))
|
export const deriveRelaySignedEvents = (url: string, filters: Filter[]) =>
|
||||||
})
|
|
||||||
|
|
||||||
export const deriveSignedEventsForUrl = (url: string, filters: Filter[]) =>
|
|
||||||
derived(
|
derived(
|
||||||
[deriveEventsForUrl(url, filters), deriveRelay(url)],
|
[deriveRelay(url), deriveEventsForUrl(url, filters)],
|
||||||
([$events, $relay]) => $events,
|
([relay, events]) => events,
|
||||||
// Disable this check for now since khatru doesn't support self
|
// khatru doesn't support relay.self, uncomment when it's ready
|
||||||
// $relay?.self ? $events.filter(spec({pubkey: $relay.self})) : [],
|
// filter(spec({pubkey: relay.self}), events)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
@@ -346,35 +285,32 @@ export const defaultSettings = {
|
|||||||
report_usage: true,
|
report_usage: true,
|
||||||
report_errors: true,
|
report_errors: true,
|
||||||
send_delay: 0,
|
send_delay: 0,
|
||||||
font_size: 1,
|
font_size: 1.1,
|
||||||
play_notification_sound: true,
|
play_notification_sound: true,
|
||||||
show_notifications_badge: true,
|
show_notifications_badge: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settings = deriveEventsMapped<Settings>(repository, {
|
export const settingsByPubkey = deriveItemsByKey({
|
||||||
filters: [{kinds: [APP_DATA], "#d": [SETTINGS]}],
|
repository,
|
||||||
itemToEvent: item => item.event,
|
|
||||||
eventToItem: async (event: TrustedEvent) => ({
|
|
||||||
event,
|
|
||||||
values: {...defaultSettings, ...parseJson(await ensurePlaintext(event))},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const {
|
|
||||||
indexStore: settingsByPubkey,
|
|
||||||
deriveItem: deriveSettings,
|
|
||||||
loadItem: loadSettings,
|
|
||||||
} = collection({
|
|
||||||
name: "settings",
|
|
||||||
store: settings,
|
|
||||||
getKey: settings => settings.event.pubkey,
|
getKey: settings => settings.event.pubkey,
|
||||||
load: makeOutboxLoader(APP_DATA, {"#d": [SETTINGS]}),
|
filters: [{kinds: [APP_DATA], "#d": [SETTINGS]}],
|
||||||
|
eventToItem: async (event: TrustedEvent) => {
|
||||||
|
const values = {...defaultSettings, ...parseJson(await ensurePlaintext(event))}
|
||||||
|
|
||||||
|
return {event, values}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const userSettings = makeUserData({
|
export const getSettingsByPubkey = getter(settingsByPubkey)
|
||||||
mapStore: settingsByPubkey,
|
|
||||||
loadItem: loadSettings,
|
export const getSettings = (pubkey: string) => getSettingsByPubkey().get(pubkey)
|
||||||
})
|
|
||||||
|
export const loadSettings = makeLoadItem(
|
||||||
|
makeOutboxLoader(APP_DATA, {"#d": [SETTINGS]}),
|
||||||
|
getSettings,
|
||||||
|
)
|
||||||
|
|
||||||
|
export const userSettings = makeUserData(settingsByPubkey, loadSettings)
|
||||||
|
|
||||||
export const loadUserSettings = makeUserLoader(loadSettings)
|
export const loadUserSettings = makeUserLoader(loadSettings)
|
||||||
|
|
||||||
@@ -390,18 +326,6 @@ export const relaysPendingTrust = writable<string[]>([])
|
|||||||
|
|
||||||
export const relaysMostlyRestricted = writable<Record<string, string>>({})
|
export const relaysMostlyRestricted = writable<Record<string, string>>({})
|
||||||
|
|
||||||
// Relay selections
|
|
||||||
|
|
||||||
export const userReadRelays = derived(userRelaySelections, $l =>
|
|
||||||
getRelaysFromList($l, RelayMode.Read),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const userWriteRelays = derived(userRelaySelections, $l =>
|
|
||||||
getRelaysFromList($l, RelayMode.Write),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const userInboxRelays = derived(userInboxRelaySelections, $l => getRelaysFromList($l))
|
|
||||||
|
|
||||||
// Alerts
|
// Alerts
|
||||||
|
|
||||||
export type Alert = {
|
export type Alert = {
|
||||||
@@ -409,9 +333,10 @@ export type Alert = {
|
|||||||
tags: string[][]
|
tags: string[][]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const alerts = deriveEventsMapped<Alert>(repository, {
|
export const alertsById = deriveItemsByKey<Alert>({
|
||||||
|
repository,
|
||||||
|
getKey: alert => alert.event.id,
|
||||||
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
|
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
|
||||||
itemToEvent: item => item.event,
|
|
||||||
eventToItem: async event => {
|
eventToItem: async event => {
|
||||||
const $signer = signer.get()
|
const $signer = signer.get()
|
||||||
|
|
||||||
@@ -426,13 +351,13 @@ export const alerts = deriveEventsMapped<Alert>(repository, {
|
|||||||
export const getAlertFeed = (alert: Alert) =>
|
export const getAlertFeed = (alert: Alert) =>
|
||||||
tryCatch(() => JSON.parse(getTagValue("feed", alert.tags)!))
|
tryCatch(() => JSON.parse(getTagValue("feed", alert.tags)!))
|
||||||
|
|
||||||
export const dmAlert = derived(alerts, $alerts =>
|
export const dmAlert = derived(alertsById, $alertsById => {
|
||||||
$alerts.find(alert => {
|
for (const alert of $alertsById.values()) {
|
||||||
const feed = getAlertFeed(alert)
|
if (findFeed(getAlertFeed(alert), f => isKindFeed(f) && f.includes(WRAP))) {
|
||||||
|
return alert
|
||||||
return findFeed(feed, f => isKindFeed(f) && f.includes(WRAP))
|
}
|
||||||
}),
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
// Alert Statuses
|
// Alert Statuses
|
||||||
|
|
||||||
@@ -441,9 +366,10 @@ export type AlertStatus = {
|
|||||||
tags: string[][]
|
tags: string[][]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
|
export const alertStatusesByAddress = deriveItemsByKey<AlertStatus>({
|
||||||
|
repository,
|
||||||
filters: [{kinds: [ALERT_STATUS]}],
|
filters: [{kinds: [ALERT_STATUS]}],
|
||||||
itemToEvent: item => item.event,
|
getKey: alertStatus => getTagValue("d", alertStatus.event.tags)!,
|
||||||
eventToItem: async event => {
|
eventToItem: async event => {
|
||||||
const $signer = signer.get()
|
const $signer = signer.get()
|
||||||
|
|
||||||
@@ -455,15 +381,10 @@ export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deriveAlertStatus = (address: string) =>
|
export const deriveAlertStatus = makeDeriveItem(alertStatusesByAddress)
|
||||||
derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address))
|
|
||||||
|
|
||||||
// Chats
|
// Chats
|
||||||
|
|
||||||
export const chatMessages = deriveEvents(repository, {
|
|
||||||
filters: [{kinds: [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]}],
|
|
||||||
})
|
|
||||||
|
|
||||||
export type Chat = {
|
export type Chat = {
|
||||||
id: string
|
id: string
|
||||||
pubkeys: string[]
|
pubkeys: string[]
|
||||||
@@ -472,66 +393,85 @@ export type Chat = {
|
|||||||
search_text: string
|
search_text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeChatId = (pubkeys: string[]) => sort(uniq(pubkeys.concat(pubkey.get()!))).join(",")
|
export const makeChatId = (pubkeys: string[]) => sort(uniq(pubkeys)).join(",")
|
||||||
|
|
||||||
export const splitChatId = (id: string) => id.split(",")
|
export const splitChatId = (id: string) => id.split(",")
|
||||||
|
|
||||||
export const chats = derived(
|
export const chatsById = call(() => {
|
||||||
[pubkey, chatMessages, profilesByPubkey],
|
const chatsById = new Map<string, Chat>()
|
||||||
([$pubkey, $messages, $profilesByPubkey]) => {
|
const chatsByPubkey = new Map<string, Chat[]>()
|
||||||
const messagesByChatId = new Map<string, TrustedEvent[]>()
|
|
||||||
|
|
||||||
for (const message of $messages) {
|
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
|
||||||
const chatId = makeChatId(getPubkeyTagValues(message.tags).concat(message.pubkey))
|
chat.search_text =
|
||||||
|
chat.pubkeys.length === 1
|
||||||
|
? displayProfileByPubkey(chat.pubkeys[0]) + " note to self"
|
||||||
|
: chat.pubkeys.map(displayProfileByPubkey).join(" ")
|
||||||
|
|
||||||
pushToMapKey(messagesByChatId, chatId, message)
|
return chat as Chat
|
||||||
|
}
|
||||||
|
|
||||||
|
return readable(chatsById, set => {
|
||||||
|
const addEvents = (events: TrustedEvent[]) => {
|
||||||
|
let dirty = false
|
||||||
|
for (const event of events) {
|
||||||
|
if ([DIRECT_MESSAGE, DIRECT_MESSAGE_FILE].includes(event.kind)) {
|
||||||
|
const pubkeys = getPubkeyTagValues(event.tags).concat(event.pubkey)
|
||||||
|
const id = makeChatId(pubkeys)
|
||||||
|
const chat = chatsById.get(id)
|
||||||
|
const messages = append(event, chat?.messages || [])
|
||||||
|
const last_activity = Math.max(chat?.last_activity || 0, event.created_at)
|
||||||
|
const updatedChat = addSearchText({id, pubkeys, messages, last_activity})
|
||||||
|
|
||||||
|
chatsById.set(id, 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 chat of chatsByPubkey.get(event.pubkey) || []) {
|
||||||
|
addSearchText(chat)
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirty) {
|
||||||
|
set(chatsById)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayPubkey = (pubkey: string) => {
|
addEvents(repository.query([{kinds: [DIRECT_MESSAGE, PROFILE]}]))
|
||||||
const profile = $profilesByPubkey.get(pubkey)
|
|
||||||
|
|
||||||
return profile ? displayProfile(profile) : ""
|
const unsubscribers = [
|
||||||
}
|
on(repository, "update", ({added}: RepositoryUpdate) => addEvents(added)),
|
||||||
|
]
|
||||||
|
|
||||||
return sortBy(
|
return () => unsubscribers.forEach(call)
|
||||||
c => -c.last_activity,
|
})
|
||||||
Array.from(messagesByChatId.entries()).map(([id, events]): Chat => {
|
|
||||||
const pubkeys = remove($pubkey!, splitChatId(id))
|
|
||||||
const messages = sortBy(e => -e.created_at, uniqBy(prop("id"), events))
|
|
||||||
const last_activity = messages[0].created_at
|
|
||||||
const search_text =
|
|
||||||
pubkeys.length === 0
|
|
||||||
? displayPubkey($pubkey!) + " note to self"
|
|
||||||
: pubkeys.map(displayPubkey).join(" ")
|
|
||||||
|
|
||||||
return {id, pubkeys, messages, last_activity, search_text}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const {
|
|
||||||
indexStore: chatsById,
|
|
||||||
deriveItem: deriveChat,
|
|
||||||
loadItem: loadChat,
|
|
||||||
} = collection({
|
|
||||||
name: "chats",
|
|
||||||
store: chats,
|
|
||||||
getKey: chat => chat.id,
|
|
||||||
load: always(Promise.resolve()),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const chatSearch = derived(chats, $chats =>
|
export const deriveChat = call(() => {
|
||||||
createSearch($chats, {
|
const _deriveChat = makeDeriveItem(chatsById)
|
||||||
|
|
||||||
|
return (pubkeys: string[]) => _deriveChat(makeChatId(pubkeys))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => {
|
||||||
|
return createSearch(Array.from($chatsByPubkey.values()), {
|
||||||
getValue: (chat: Chat) => chat.id,
|
getValue: (chat: Chat) => chat.id,
|
||||||
fuseOptions: {keys: ["search_text"]},
|
fuseOptions: {keys: ["search_text"]},
|
||||||
}),
|
})
|
||||||
)
|
})
|
||||||
|
|
||||||
// Rooms
|
// Rooms
|
||||||
|
|
||||||
export const messages = deriveEvents(repository, {filters: [{kinds: [MESSAGE]}]})
|
|
||||||
|
|
||||||
export type Room = PublishedRoomMeta & {
|
export type Room = PublishedRoomMeta & {
|
||||||
id: string
|
id: string
|
||||||
url: string
|
url: string
|
||||||
@@ -544,95 +484,108 @@ export const splitRoomId = (id: string) => id.split("'")
|
|||||||
export const hasNip29 = (relay?: RelayProfile) =>
|
export const hasNip29 = (relay?: RelayProfile) =>
|
||||||
relay?.supported_nips?.map?.(String)?.includes?.("29")
|
relay?.supported_nips?.map?.(String)?.includes?.("29")
|
||||||
|
|
||||||
export const roomMetas = deriveEventsMapped<PublishedRoomMeta>(repository, {
|
export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
|
||||||
filters: [{kinds: [ROOM_META]}],
|
tracker,
|
||||||
itemToEvent: item => item.event,
|
repository,
|
||||||
eventToItem: readRoomMeta,
|
filters: [{kinds: [ROOM_META, ROOM_DELETE]}],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const roomDeletes = deriveEvents(repository, {
|
export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => {
|
||||||
filters: [{kinds: [ROOM_DELETE]}],
|
const metaByIdByUrl = new Map<string, Map<string, Room>>()
|
||||||
})
|
|
||||||
|
|
||||||
export const rooms = derived(
|
for (const [url, events] of roomMetaEventsByIdByUrl.entries()) {
|
||||||
[roomMetas, roomDeletes, getUrlsForEvent],
|
const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values())
|
||||||
([$roomMetas, $roomDeletes, $getUrlsForEvent]) => {
|
|
||||||
const result = new Map<string, Room>()
|
|
||||||
const deletedByH = new Map<string, number>()
|
const deletedByH = new Map<string, number>()
|
||||||
|
|
||||||
for (const event of $roomDeletes) {
|
for (const event of deleteEvents) {
|
||||||
for (const h of getTagValues("h", event.tags)) {
|
for (const h of getTagValues("h", event.tags)) {
|
||||||
deletedByH.set(h, max([deletedByH.get(h), event.created_at]))
|
deletedByH.set(h, max([deletedByH.get(h), event.created_at]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const meta of $roomMetas) {
|
for (const event of metaEvents) {
|
||||||
|
const meta = readRoomMeta(event)
|
||||||
|
|
||||||
if (gt(deletedByH.get(meta.h), meta.event.created_at)) {
|
if (gt(deletedByH.get(meta.h), meta.event.created_at)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const url of $getUrlsForEvent(meta.event.id)) {
|
let metaById = metaByIdByUrl.get(url)
|
||||||
const id = makeRoomId(url, meta.h)
|
if (!metaById) {
|
||||||
|
metaById = new Map()
|
||||||
result.set(id, {...meta, url, id})
|
metaByIdByUrl.set(url, metaById)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(result.values())
|
const id = makeRoomId(url, meta.h)
|
||||||
},
|
|
||||||
|
metaById.set(id, {...meta, url, id})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = new Map<string, Room[]>()
|
||||||
|
|
||||||
|
for (const [url, metaById] of metaByIdByUrl.entries()) {
|
||||||
|
result.set(url, Array.from(metaById.values()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
export const roomsById = derived(roomsByUrl, roomsByUrl =>
|
||||||
|
indexBy(room => room.id, Array.from(roomsByUrl.values()).flatMap(identity)),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const roomsByUrl = derived(rooms, $rooms => groupBy(c => c.url, $rooms))
|
export const getRoomsById = getter(roomsById)
|
||||||
|
|
||||||
export const {
|
export const getRoom = (id: string) => getRoomsById().get(id)
|
||||||
indexStore: roomsById,
|
|
||||||
deriveItem: _deriveRoom,
|
export const loadRoom = call(() => {
|
||||||
loadItem: _loadRoom,
|
const _fetchRoom = async (id: string) => {
|
||||||
} = collection({
|
|
||||||
name: "rooms",
|
|
||||||
store: rooms,
|
|
||||||
getKey: room => room.id,
|
|
||||||
load: async (id: string) => {
|
|
||||||
const [url, h] = splitRoomId(id)
|
const [url, h] = splitRoomId(id)
|
||||||
|
|
||||||
await load({
|
await load({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
filters: [{kinds: [ROOM_META], "#d": [h]}],
|
filters: [{kinds: [ROOM_META], "#d": [h]}],
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
|
|
||||||
|
const _loadRoom = makeLoadItem(_fetchRoom, getRoom)
|
||||||
|
|
||||||
|
return (url: string, h: string) => _loadRoom(makeRoomId(url, h))
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deriveRoom = (url: string, h: string) =>
|
export const deriveRoom = call(() => {
|
||||||
derived(_deriveRoom(makeRoomId(url, h)), $meta => $meta || makeRoomMeta({h}))
|
const _deriveRoom = makeDeriveItem(roomsById, loadRoom)
|
||||||
|
|
||||||
export const displayRoom = (url: string, h: string) =>
|
return (url: string, h: string) =>
|
||||||
roomsById.get().get(makeRoomId(url, h))?.name || h
|
derived(_deriveRoom(makeRoomId(url, h)), room => room || makeRoomMeta({h}))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const displayRoom = (url: string, h: string) => getRoom(makeRoomId(url, h))?.name || h
|
||||||
|
|
||||||
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
|
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
|
||||||
|
|
||||||
// User space/room selections
|
// User space/room lists
|
||||||
|
|
||||||
export const groupSelections = deriveEventsMapped<PublishedList>(repository, {
|
export const groupListsByPubkey = deriveItemsByKey({
|
||||||
|
repository,
|
||||||
filters: [{kinds: [ROOMS]}],
|
filters: [{kinds: [ROOMS]}],
|
||||||
itemToEvent: item => item.event,
|
getKey: list => list.event.pubkey,
|
||||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const {
|
export const getGroupListsByPubkey = getter(groupListsByPubkey)
|
||||||
indexStore: groupSelectionsByPubkey,
|
|
||||||
deriveItem: deriveGroupSelections,
|
|
||||||
loadItem: loadGroupSelections,
|
|
||||||
} = collection({
|
|
||||||
name: "groupSelections",
|
|
||||||
store: groupSelections,
|
|
||||||
getKey: list => list.event.pubkey,
|
|
||||||
load: makeOutboxLoader(ROOMS),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const groupSelectionsPubkeysByUrl = derived(groupSelections, $groupSelections => {
|
export const getGroupList = (pubkey: string) => getGroupListsByPubkey().get(pubkey)
|
||||||
|
|
||||||
|
export const loadGroupList = makeLoadItem(makeOutboxLoader(ROOMS), getGroupList)
|
||||||
|
|
||||||
|
export const deriveGroupList = makeDeriveItem(groupListsByPubkey, loadGroupList)
|
||||||
|
|
||||||
|
export const groupListPubkeysByUrl = derived(groupListsByPubkey, $groupListsByPubkey => {
|
||||||
const result = new Map<string, Set<string>>()
|
const result = new Map<string, Set<string>>()
|
||||||
|
|
||||||
for (const list of $groupSelections) {
|
for (const list of $groupListsByPubkey.values()) {
|
||||||
const tags = getListTags(list)
|
const tags = getListTags(list)
|
||||||
|
|
||||||
for (const url of getRelayTagValues(tags)) {
|
for (const url of getRelayTagValues(tags)) {
|
||||||
@@ -651,8 +604,11 @@ export const groupSelectionsPubkeysByUrl = derived(groupSelections, $groupSelect
|
|||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getSpaceUrlsFromGroupSelections = ($groupSelections: List | undefined) => {
|
export const deriveGroupListPubkeys = (url: string) =>
|
||||||
const tags = getListTags($groupSelections)
|
derived(groupListPubkeysByUrl, $groupListPubkeysByUrl => new Set($groupListPubkeysByUrl.get(url)))
|
||||||
|
|
||||||
|
export const getSpaceUrlsFromGroupList = (groupList: List | undefined) => {
|
||||||
|
const tags = getListTags(groupList)
|
||||||
const urls = getRelayTagValues(tags)
|
const urls = getRelayTagValues(tags)
|
||||||
|
|
||||||
for (const tag of getGroupTags(tags)) {
|
for (const tag of getGroupTags(tags)) {
|
||||||
@@ -666,13 +622,10 @@ export const getSpaceUrlsFromGroupSelections = ($groupSelections: List | undefin
|
|||||||
return uniq(urls.map(normalizeRelayUrl))
|
return uniq(urls.map(normalizeRelayUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSpaceRoomsFromGroupSelections = (
|
export const getSpaceRoomsFromGroupList = (url: string, groupList: List | undefined) => {
|
||||||
url: string,
|
|
||||||
$groupSelections: List | undefined,
|
|
||||||
) => {
|
|
||||||
const rooms: string[] = []
|
const rooms: string[] = []
|
||||||
|
|
||||||
for (const [_, h, relay] of getGroupTags(getListTags($groupSelections))) {
|
for (const [_, h, relay] of getGroupTags(getListTags(groupList))) {
|
||||||
if (url === relay) {
|
if (url === relay) {
|
||||||
rooms.push(h)
|
rooms.push(h)
|
||||||
}
|
}
|
||||||
@@ -681,20 +634,17 @@ export const getSpaceRoomsFromGroupSelections = (
|
|||||||
return sortBy(roomComparator(url), rooms)
|
return sortBy(roomComparator(url), rooms)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userGroupSelections = makeUserData({
|
export const userGroupList = makeUserData(groupListsByPubkey, loadGroupList)
|
||||||
mapStore: groupSelectionsByPubkey,
|
|
||||||
loadItem: loadGroupSelections,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const loadUserGroupSelections = makeUserLoader(loadGroupSelections)
|
export const loadUserGroupList = makeUserLoader(loadGroupList)
|
||||||
|
|
||||||
export const userSpaceUrls = derived(userGroupSelections, getSpaceUrlsFromGroupSelections)
|
export const userSpaceUrls = derived(userGroupList, getSpaceUrlsFromGroupList)
|
||||||
|
|
||||||
export const deriveUserRooms = (url: string) =>
|
export const deriveUserRooms = (url: string) =>
|
||||||
derived([userGroupSelections, roomsById], ([$userGroupSelections, $roomsById]) => {
|
derived([userGroupList, roomsById], ([$userGroupList, $roomsById]) => {
|
||||||
const rooms: string[] = []
|
const rooms: string[] = []
|
||||||
|
|
||||||
for (const h of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
|
for (const h of getSpaceRoomsFromGroupList(url, $userGroupList)) {
|
||||||
if ($roomsById.has(makeRoomId(url, h))) {
|
if ($roomsById.has(makeRoomId(url, h))) {
|
||||||
rooms.push(h)
|
rooms.push(h)
|
||||||
}
|
}
|
||||||
@@ -720,17 +670,15 @@ export const deriveOtherRooms = (url: string) =>
|
|||||||
|
|
||||||
export const deriveSpaceMembers = (url: string) =>
|
export const deriveSpaceMembers = (url: string) =>
|
||||||
derived(
|
derived(
|
||||||
deriveSignedEventsForUrl(url, [
|
deriveRelaySignedEvents(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]}]),
|
||||||
{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]},
|
|
||||||
]),
|
|
||||||
$events => {
|
$events => {
|
||||||
const membersEvent = $events.find(spec({kind: RELAY_MEMBERS}))
|
const membersEvent = $events.find(spec({kind: RELAY_MEMBERS}))
|
||||||
|
|
||||||
if (membersEvent) {
|
if (membersEvent) {
|
||||||
return getTagValues("member", membersEvent.tags)
|
return uniq(getTagValues("member", membersEvent.tags))
|
||||||
}
|
}
|
||||||
|
|
||||||
const members = new Set()
|
const members = new Set<string>()
|
||||||
|
|
||||||
for (const event of sortBy(e => e.created_at, $events)) {
|
for (const event of sortBy(e => e.created_at, $events)) {
|
||||||
const pubkeys = getPubkeyTagValues(event.tags)
|
const pubkeys = getPubkeyTagValues(event.tags)
|
||||||
@@ -752,43 +700,63 @@ export const deriveSpaceMembers = (url: string) =>
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export const deriveRoomMembers = (url: string, h: string) =>
|
export type BannedPubkeyItem = {
|
||||||
derived(
|
pubkey: string
|
||||||
deriveEventsForUrl(url, [
|
reason: string
|
||||||
{kinds: [ROOM_MEMBERS], "#d": [h]},
|
}
|
||||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
|
|
||||||
]),
|
|
||||||
$events => {
|
|
||||||
const membersEvent = $events.find(spec({kind: ROOM_MEMBERS}))
|
|
||||||
|
|
||||||
if (membersEvent) {
|
export const spaceBannedPubkeyItems = new Map<string, BannedPubkeyItem[]>()
|
||||||
return getPubkeyTagValues(membersEvent.tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
const members = new Set()
|
export const deriveSpaceBannedPubkeyItems = (url: string) => {
|
||||||
|
const store = writable(spaceBannedPubkeyItems.get(url) || [])
|
||||||
|
|
||||||
for (const event of sortBy(e => -e.created_at, $events)) {
|
manageRelay(url, {method: ManagementMethod.ListBannedPubkeys, params: []}).then(res => {
|
||||||
const pubkeys = getPubkeyTagValues(event.tags)
|
spaceBannedPubkeyItems.set(url, res.result)
|
||||||
|
store.set(res.result)
|
||||||
|
})
|
||||||
|
|
||||||
if (event.kind === ROOM_ADD_MEMBER) {
|
return store
|
||||||
for (const pubkey of pubkeys) {
|
}
|
||||||
members.add(pubkey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.kind === ROOM_REMOVE_MEMBER) {
|
export const deriveRoomMembers = (url: string, h: string) => {
|
||||||
for (const pubkey of pubkeys) {
|
const filters: Filter[] = [
|
||||||
members.delete(pubkey)
|
{kinds: [ROOM_MEMBERS], "#d": [h]},
|
||||||
}
|
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
|
||||||
|
]
|
||||||
|
|
||||||
|
return derived(deriveEventsForUrl(url, filters), $events => {
|
||||||
|
const membersEvent = find(spec({kind: ROOM_MEMBERS}), $events)
|
||||||
|
|
||||||
|
if (membersEvent) {
|
||||||
|
return uniq(getPubkeyTagValues(membersEvent.tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = new Set<string>()
|
||||||
|
|
||||||
|
for (const event of sortBy(e => -e.created_at, $events)) {
|
||||||
|
const pubkeys = getPubkeyTagValues(event.tags)
|
||||||
|
|
||||||
|
if (event.kind === ROOM_ADD_MEMBER) {
|
||||||
|
for (const pubkey of pubkeys) {
|
||||||
|
members.add(pubkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(members)
|
if (event.kind === ROOM_REMOVE_MEMBER) {
|
||||||
},
|
for (const pubkey of pubkeys) {
|
||||||
)
|
members.delete(pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const deriveRoomAdmins = (url: string, h: string) =>
|
return Array.from(members)
|
||||||
derived(deriveEventsForUrl(url, [{kinds: [ROOM_ADMINS], "#d": [h]}]), $events => {
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deriveRoomAdmins = (url: string, h: string) => {
|
||||||
|
const filters: Filter[] = [{kinds: [ROOM_ADMINS], "#d": [h]}]
|
||||||
|
|
||||||
|
return derived(deriveEventsForUrl(url, filters), $events => {
|
||||||
const adminsEvent = first($events)
|
const adminsEvent = first($events)
|
||||||
|
|
||||||
if (adminsEvent) {
|
if (adminsEvent) {
|
||||||
@@ -797,6 +765,7 @@ export const deriveRoomAdmins = (url: string, h: string) =>
|
|||||||
|
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// User membership status
|
// User membership status
|
||||||
|
|
||||||
@@ -806,18 +775,33 @@ export enum MembershipStatus {
|
|||||||
Granted,
|
Granted,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deriveUserSpaceMembershipStatus = (url: string) =>
|
export const deriveUserIsSpaceAdmin = memoize((url?: string) => {
|
||||||
derived(
|
const store = writable(false)
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
manageRelay(url, {method: ManagementMethod.SupportedMethods, params: []}).then(res =>
|
||||||
|
store.set(Boolean(res.result?.length)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deriveUserSpaceMembershipStatus = (url: string) => {
|
||||||
|
const filters: Filter[] = [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]
|
||||||
|
|
||||||
|
return derived(
|
||||||
[
|
[
|
||||||
pubkey,
|
pubkey,
|
||||||
deriveSpaceMembers(url),
|
deriveSpaceMembers(url),
|
||||||
deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]),
|
deriveEventsForUrl(url, filters),
|
||||||
|
deriveUserIsSpaceAdmin(url),
|
||||||
],
|
],
|
||||||
([$pubkey, $members, $events]) => {
|
([$pubkey, $members, $events, $isAdmin]) => {
|
||||||
const isMember = $members.includes($pubkey)
|
const isMember = $members.includes($pubkey!) || $isAdmin
|
||||||
|
|
||||||
for (const event of $events) {
|
for (const event of $events) {
|
||||||
if (!getPubkeyTagValues(event.tags).includes($pubkey!)) {
|
if (event.pubkey !== $pubkey) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,19 +817,29 @@ export const deriveUserSpaceMembershipStatus = (url: string) =>
|
|||||||
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
|
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
|
export const deriveUserIsRoomAdmin = (url: string, h: string) =>
|
||||||
derived(
|
derived(
|
||||||
|
[pubkey, deriveRoomAdmins(url, h), deriveUserIsSpaceAdmin(url)],
|
||||||
|
([$pubkey, $admins, $isSpaceAdmin]) => $isSpaceAdmin || $admins.includes($pubkey!),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
|
||||||
|
const filters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]
|
||||||
|
|
||||||
|
return derived(
|
||||||
[
|
[
|
||||||
pubkey,
|
pubkey,
|
||||||
deriveRoomMembers(url, h),
|
deriveRoomMembers(url, h),
|
||||||
deriveEventsForUrl(url, [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]),
|
deriveEventsForUrl(url, filters),
|
||||||
|
deriveUserIsRoomAdmin(url, h),
|
||||||
],
|
],
|
||||||
([$pubkey, $members, $events]) => {
|
([$pubkey, $members, $events, $isAdmin]) => {
|
||||||
const isMember = $members.includes($pubkey)
|
const isMember = $members.includes($pubkey!) || $isAdmin
|
||||||
|
|
||||||
for (const event of $events) {
|
for (const event of $events) {
|
||||||
if (!getPubkeyTagValues(event.tags).includes($pubkey!)) {
|
if (event.pubkey !== $pubkey) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,28 +855,23 @@ export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
|
|||||||
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
|
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const deriveUserCanCreateRoom = (url: string) =>
|
export const deriveUserCanCreateRoom = (url: string) => {
|
||||||
derived(
|
const filters: Filter[] = [{kinds: [ROOM_CREATE_PERMISSION]}]
|
||||||
[pubkey, deriveEventsForUrl(url, [{kinds: [ROOM_CREATE_PERMISSION]}])],
|
|
||||||
([$pubkey, $events]) => {
|
|
||||||
const event = first($events)
|
|
||||||
|
|
||||||
return event ? getPubkeyTagValues(event.tags).includes($pubkey!) : true
|
return derived(
|
||||||
|
[pubkey, deriveEventsForUrl(url, filters), deriveUserIsSpaceAdmin(url)],
|
||||||
|
([$pubkey, $events, $isAdmin]) => {
|
||||||
|
for (const event of $events) {
|
||||||
|
if (getPubkeyTagValues(event.tags).includes($pubkey!)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $isAdmin
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export const deriveUserIsRoomAdmin = (url: string, h: string) =>
|
|
||||||
derived([pubkey, deriveRoomAdmins(url, h)], ([$pubkey, $admins]) => $admins.includes($pubkey!))
|
|
||||||
|
|
||||||
export const deriveUserIsSpaceAdmin = (url: string) => {
|
|
||||||
const store = writable(false)
|
|
||||||
|
|
||||||
manageRelay(url, {method: ManagementMethod.SupportedMethods, params: []}).then(res =>
|
|
||||||
store.set(Boolean(res.result?.length)),
|
|
||||||
)
|
|
||||||
|
|
||||||
return store
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other utils
|
// Other utils
|
||||||
@@ -902,13 +891,10 @@ export const displayReaction = (content: string) => {
|
|||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deriveSocket = (url: string) =>
|
export const deriveSocket = (url: string) => {
|
||||||
custom<Socket>(set => {
|
const socket = Pool.get().get(url)
|
||||||
const pool = Pool.get()
|
|
||||||
const socket = pool.get(url)
|
|
||||||
|
|
||||||
set(socket)
|
|
||||||
|
|
||||||
|
return readable(socket, set => {
|
||||||
const subs = [
|
const subs = [
|
||||||
on(socket, SocketEvent.Error, () => set(socket)),
|
on(socket, SocketEvent.Error, () => set(socket)),
|
||||||
on(socket, SocketEvent.Status, () => set(socket)),
|
on(socket, SocketEvent.Status, () => set(socket)),
|
||||||
@@ -917,6 +903,7 @@ export const deriveSocket = (url: string) =>
|
|||||||
|
|
||||||
return () => subs.forEach(call)
|
return () => subs.forEach(call)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const deriveSocketStatus = (url: string) =>
|
export const deriveSocketStatus = (url: string) =>
|
||||||
throttled(
|
throttled(
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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(() => {
|
||||||
|
let p = Promise.resolve()
|
||||||
|
|
||||||
|
const get = async <T>(key: string): Promise<T | undefined> => {
|
||||||
|
const result = await Preferences.get({key})
|
||||||
|
if (!result.value) return undefined
|
||||||
|
try {
|
||||||
|
return JSON.parse(result.value)
|
||||||
|
} catch (e) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const set = async <T>(key: string, value: T): Promise<void> => {
|
||||||
|
p = p.then(() => Preferences.set({key, value: JSON.stringify(value)}))
|
||||||
|
|
||||||
|
await p
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = async () => {
|
||||||
|
p = p.then(() => Preferences.clear())
|
||||||
|
|
||||||
|
await p
|
||||||
|
}
|
||||||
|
|
||||||
|
return {get, set, clear}
|
||||||
|
})
|
||||||
|
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
+68
-163
@@ -6,7 +6,6 @@ import {
|
|||||||
getListTags,
|
getListTags,
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
WRAP,
|
WRAP,
|
||||||
MESSAGE,
|
|
||||||
ROOM_META,
|
ROOM_META,
|
||||||
ROOM_DELETE,
|
ROOM_DELETE,
|
||||||
ROOM_ADMINS,
|
ROOM_ADMINS,
|
||||||
@@ -25,34 +24,33 @@ import {request, load, pull} from "@welshman/net"
|
|||||||
import {
|
import {
|
||||||
pubkey,
|
pubkey,
|
||||||
loadRelay,
|
loadRelay,
|
||||||
userFollows,
|
userFollowList,
|
||||||
userRelaySelections,
|
userRelayList,
|
||||||
userInboxRelaySelections,
|
userMessagingRelayList,
|
||||||
loadRelaySelections,
|
loadRelayList,
|
||||||
loadInboxRelaySelections,
|
loadMessagingRelayList,
|
||||||
loadBlossomServers,
|
loadBlossomServerList,
|
||||||
loadFollows,
|
loadFollowList,
|
||||||
loadMutes,
|
loadMuteList,
|
||||||
loadProfile,
|
loadProfile,
|
||||||
|
tracker,
|
||||||
repository,
|
repository,
|
||||||
shouldUnwrap,
|
shouldUnwrap,
|
||||||
hasNegentropy,
|
hasNegentropy,
|
||||||
relaysByUrl,
|
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
|
REACTION_KINDS,
|
||||||
MESSAGE_KINDS,
|
MESSAGE_KINDS,
|
||||||
CONTENT_KINDS,
|
CONTENT_KINDS,
|
||||||
INDEXER_RELAYS,
|
INDEXER_RELAYS,
|
||||||
loadSettings,
|
loadSettings,
|
||||||
loadGroupSelections,
|
loadGroupList,
|
||||||
userSpaceUrls,
|
userSpaceUrls,
|
||||||
userGroupSelections,
|
userGroupList,
|
||||||
bootstrapPubkeys,
|
bootstrapPubkeys,
|
||||||
decodeRelay,
|
decodeRelay,
|
||||||
getUrlsForEvent,
|
getSpaceUrlsFromGroupList,
|
||||||
hasNip29,
|
getSpaceRoomsFromGroupList,
|
||||||
getSpaceUrlsFromGroupSelections,
|
|
||||||
getSpaceRoomsFromGroupSelections,
|
|
||||||
makeCommentFilter,
|
makeCommentFilter,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {loadAlerts, loadAlertStatuses} from "@app/core/requests"
|
import {loadAlerts, loadAlertStatuses} from "@app/core/requests"
|
||||||
@@ -67,7 +65,6 @@ type PullOpts = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pullWithFallback = ({relays, filters, signal}: PullOpts) => {
|
const pullWithFallback = ({relays, filters, signal}: PullOpts) => {
|
||||||
const $getUrlsForEvent = get(getUrlsForEvent)
|
|
||||||
const [smart, dumb] = partition(hasNegentropy, relays)
|
const [smart, dumb] = partition(hasNegentropy, relays)
|
||||||
const events = repository.query(filters, {shouldSort: false}).filter(isSignedEvent)
|
const events = repository.query(filters, {shouldSort: false}).filter(isSignedEvent)
|
||||||
const promises: Promise<TrustedEvent[]>[] = [pull({relays: smart, filters, signal, events})]
|
const promises: Promise<TrustedEvent[]>[] = [pull({relays: smart, filters, signal, events})]
|
||||||
@@ -75,7 +72,7 @@ const pullWithFallback = ({relays, filters, signal}: PullOpts) => {
|
|||||||
// Since pulling from relays without negentropy is expensive, limit how many
|
// Since pulling from relays without negentropy is expensive, limit how many
|
||||||
// duplicates we repeatedly download
|
// duplicates we repeatedly download
|
||||||
for (const url of dumb) {
|
for (const url of dumb) {
|
||||||
const urlEvents = events.filter(e => $getUrlsForEvent(e.id).includes(url))
|
const urlEvents = events.filter(e => tracker.getRelays(e.id).has(url))
|
||||||
|
|
||||||
if (urlEvents.length >= 100) {
|
if (urlEvents.length >= 100) {
|
||||||
filters = filters.map(assoc("since", sortBy(e => -e.created_at, urlEvents)[10]!.created_at))
|
filters = filters.map(assoc("since", sortBy(e => -e.created_at, urlEvents)[10]!.created_at))
|
||||||
@@ -140,10 +137,9 @@ const syncUserSpaceMembership = (url: string) => {
|
|||||||
relays: [url],
|
relays: [url],
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{kinds: [RELAY_ADD_MEMBER], "#p": [$pubkey], limit: 1},
|
||||||
kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, ROOM_CREATE_PERMISSION],
|
{kinds: [RELAY_REMOVE_MEMBER], "#p": [$pubkey], limit: 1},
|
||||||
"#p": [$pubkey],
|
{kinds: [ROOM_CREATE_PERMISSION], "#p": [$pubkey], limit: 1},
|
||||||
},
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -160,11 +156,8 @@ const syncUserRoomMembership = (url: string, h: string) => {
|
|||||||
relays: [url],
|
relays: [url],
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{kinds: [ROOM_ADD_MEMBER], "#p": [$pubkey], "#h": [h], limit: 1},
|
||||||
kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
|
{kinds: [ROOM_REMOVE_MEMBER], "#p": [$pubkey], "#h": [h], limit: 1},
|
||||||
"#p": [$pubkey],
|
|
||||||
"#h": [h],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -175,20 +168,18 @@ const syncUserRoomMembership = (url: string, h: string) => {
|
|||||||
const syncUserData = () => {
|
const syncUserData = () => {
|
||||||
const unsubscribersByKey = new Map<string, Unsubscriber>()
|
const unsubscribersByKey = new Map<string, Unsubscriber>()
|
||||||
|
|
||||||
const unsubscribeGroupSelections = userGroupSelections.subscribe($l => {
|
const unsubscribeGroupList = userGroupList.subscribe($userGroupList => {
|
||||||
const $pubkey = pubkey.get()
|
if ($userGroupList) {
|
||||||
|
|
||||||
if ($pubkey) {
|
|
||||||
const keys = new Set<string>()
|
const keys = new Set<string>()
|
||||||
|
|
||||||
for (const url of getSpaceUrlsFromGroupSelections($l)) {
|
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
|
||||||
if (!unsubscribersByKey.has(url)) {
|
if (!unsubscribersByKey.has(url)) {
|
||||||
unsubscribersByKey.set(url, syncUserSpaceMembership(url))
|
unsubscribersByKey.set(url, syncUserSpaceMembership(url))
|
||||||
}
|
}
|
||||||
|
|
||||||
keys.add(url)
|
keys.add(url)
|
||||||
|
|
||||||
for (const h of getSpaceRoomsFromGroupSelections(url, $l)) {
|
for (const h of getSpaceRoomsFromGroupList(url, $userGroupList)) {
|
||||||
const key = `${url}'${h}`
|
const key = `${url}'${h}`
|
||||||
|
|
||||||
if (!unsubscribersByKey.has(key)) {
|
if (!unsubscribersByKey.has(key)) {
|
||||||
@@ -208,50 +199,41 @@ const syncUserData = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const unsubscribeSelections = userRelaySelections.subscribe($l => {
|
const unsubscribeRelayList = userRelayList.subscribe($userRelayList => {
|
||||||
const $pubkey = pubkey.get()
|
if ($userRelayList) {
|
||||||
|
loadAlerts($userRelayList.event.pubkey)
|
||||||
if ($pubkey) {
|
loadAlertStatuses($userRelayList.event.pubkey)
|
||||||
loadAlerts($pubkey)
|
loadBlossomServerList($userRelayList.event.pubkey)
|
||||||
loadAlertStatuses($pubkey)
|
loadFollowList($userRelayList.event.pubkey)
|
||||||
loadBlossomServers($pubkey)
|
loadGroupList($userRelayList.event.pubkey)
|
||||||
loadFollows($pubkey)
|
loadMuteList($userRelayList.event.pubkey)
|
||||||
loadGroupSelections($pubkey)
|
loadProfile($userRelayList.event.pubkey)
|
||||||
loadMutes($pubkey)
|
loadSettings($userRelayList.event.pubkey)
|
||||||
loadProfile($pubkey)
|
|
||||||
loadSettings($pubkey)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const unsubscribeFollows = userFollows.subscribe(async $l => {
|
const unsubscribeFollows = userFollowList.subscribe(async $userFollowList => {
|
||||||
for (const pubkeys of chunk(10, get(bootstrapPubkeys))) {
|
for (const pubkeys of chunk(10, get(bootstrapPubkeys))) {
|
||||||
// This isn't urgent, avoid clogging other stuff up
|
// This isn't urgent, avoid clogging other stuff up
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pubkeys.map(async pk => {
|
pubkeys.map(async pk => {
|
||||||
await loadRelaySelections(pk)
|
await loadRelayList(pk)
|
||||||
await loadGroupSelections(pk)
|
await loadGroupList(pk)
|
||||||
await loadProfile(pk)
|
await loadProfile(pk)
|
||||||
await loadFollows(pk)
|
await loadFollowList(pk)
|
||||||
await loadMutes(pk)
|
await loadMuteList(pk)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const unsubscribePubkey = pubkey.subscribe($pubkey => {
|
|
||||||
if ($pubkey) {
|
|
||||||
loadRelaySelections($pubkey)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribersByKey.forEach(call)
|
unsubscribersByKey.forEach(call)
|
||||||
unsubscribeGroupSelections()
|
unsubscribeGroupList()
|
||||||
unsubscribeSelections()
|
unsubscribeRelayList()
|
||||||
unsubscribeFollows()
|
unsubscribeFollows()
|
||||||
unsubscribePubkey()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,11 +247,13 @@ const syncSpace = (url: string) => {
|
|||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
filters: [
|
filters: [
|
||||||
{kinds: [RELAY_MEMBERS]},
|
{kinds: [RELAY_MEMBERS]},
|
||||||
{kinds: [ROOM_META, ROOM_DELETE]},
|
|
||||||
{kinds: [ROOM_ADMINS, ROOM_MEMBERS]},
|
{kinds: [ROOM_ADMINS, ROOM_MEMBERS]},
|
||||||
|
{kinds: [ROOM_META, ROOM_DELETE], limit: 1000},
|
||||||
|
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]},
|
||||||
{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]},
|
{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]},
|
||||||
...MESSAGE_KINDS.map(kind => ({kinds: [kind]})),
|
...MESSAGE_KINDS.map(kind => ({kinds: [kind]})),
|
||||||
makeCommentFilter(CONTENT_KINDS),
|
makeCommentFilter(CONTENT_KINDS),
|
||||||
|
{kinds: REACTION_KINDS, limit: 0},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -277,116 +261,37 @@ const syncSpace = (url: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const syncSpaces = () => {
|
const syncSpaces = () => {
|
||||||
const membershipUnsubscribersByUrl = new Map<string, Unsubscriber>()
|
const store = derived([userSpaceUrls, page], identity)
|
||||||
|
const unsubscribersByUrl = new Map<string, Unsubscriber>()
|
||||||
|
const unsubscribe = store.subscribe(([$userSpaceUrls, $page]) => {
|
||||||
|
const urls = Array.from($userSpaceUrls)
|
||||||
|
|
||||||
|
if ($page.params.relay) {
|
||||||
|
urls.push(decodeRelay($page.params.relay))
|
||||||
|
}
|
||||||
|
|
||||||
const unsubscribeSpaceUrls = userSpaceUrls.subscribe(urls => {
|
|
||||||
// stop syncing removed spaces
|
// stop syncing removed spaces
|
||||||
for (const [url, unsubscribe] of membershipUnsubscribersByUrl.entries()) {
|
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
|
||||||
if (!urls.includes(url)) {
|
if (!urls.includes(url)) {
|
||||||
membershipUnsubscribersByUrl.delete(url)
|
unsubscribersByUrl.delete(url)
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start syncing newly added spaces
|
// Start syncing newly added spaces
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
if (!membershipUnsubscribersByUrl.has(url)) {
|
if (!unsubscribersByUrl.has(url)) {
|
||||||
membershipUnsubscribersByUrl.set(url, syncSpace(url))
|
unsubscribersByUrl.set(url, syncSpace(url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const pageUnsubscribersByUrl = new Map<string, Unsubscriber>()
|
return () => {
|
||||||
|
for (const unsubscriber of unsubscribersByUrl.values()) {
|
||||||
// Sync the space the user is currently visiting
|
unsubscriber()
|
||||||
const unsubscribePage = page.subscribe($page => {
|
|
||||||
if ($page.params.relay) {
|
|
||||||
const url = decodeRelay($page.params.relay)
|
|
||||||
|
|
||||||
// Don't subscribe twice if the user is a member
|
|
||||||
if (!pageUnsubscribersByUrl.has(url) && !get(userSpaceUrls).includes(url)) {
|
|
||||||
pageUnsubscribersByUrl.set(url, syncSpace(url))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up old subscriptions
|
|
||||||
for (const [oldUrl, unsubscribe] of pageUnsubscribersByUrl.entries()) {
|
|
||||||
if (url !== oldUrl) {
|
|
||||||
pageUnsubscribersByUrl.delete(oldUrl)
|
|
||||||
unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Array.from(pageUnsubscribersByUrl.values()).forEach(call)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
unsubscribe()
|
||||||
Array.from(membershipUnsubscribersByUrl.values()).forEach(call)
|
|
||||||
Array.from(pageUnsubscribersByUrl.values()).forEach(call)
|
|
||||||
unsubscribeSpaceUrls()
|
|
||||||
unsubscribePage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chat
|
|
||||||
|
|
||||||
const syncRoom = (url: string, h: string) => {
|
|
||||||
const controller = new AbortController()
|
|
||||||
|
|
||||||
pullAndListen({
|
|
||||||
relays: [url],
|
|
||||||
signal: controller.signal,
|
|
||||||
filters: [
|
|
||||||
{kinds: [ROOM_ADMINS, ROOM_MEMBERS], "#d": [h]},
|
|
||||||
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
|
|
||||||
{kinds: [MESSAGE], "#h": [h]},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => controller.abort()
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncRooms = () => {
|
|
||||||
const unsubscribersByKey = new Map<string, Unsubscriber>()
|
|
||||||
|
|
||||||
const unsubscribeSpaceUrls = derived([userGroupSelections, relaysByUrl], identity).subscribe(
|
|
||||||
([$l, $relaysByUrl]) => {
|
|
||||||
const keys = new Set<string>()
|
|
||||||
const newUnsubscribersByKey = new Map<string, Unsubscriber>()
|
|
||||||
|
|
||||||
// Add new subscriptions, depending on whether nip 29 is supported
|
|
||||||
for (const url of getRelayTagValues(getListTags($l))) {
|
|
||||||
if (hasNip29($relaysByUrl.get(url))) {
|
|
||||||
for (const h of getSpaceRoomsFromGroupSelections(url, $l)) {
|
|
||||||
const key = `${url}'${h}`
|
|
||||||
|
|
||||||
if (!unsubscribersByKey.has(key)) {
|
|
||||||
newUnsubscribersByKey.set(key, syncRoom(url, h))
|
|
||||||
}
|
|
||||||
|
|
||||||
keys.add(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop syncing removed selections
|
|
||||||
for (const [key, unsubscribe] of unsubscribersByKey.entries()) {
|
|
||||||
if (!keys.has(key)) {
|
|
||||||
unsubscribersByKey.delete(key)
|
|
||||||
unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start syncing newly added spaces
|
|
||||||
for (const [key, unsubscriber] of newUnsubscribersByKey.entries()) {
|
|
||||||
unsubscribersByKey.set(key, unsubscriber)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
Array.from(unsubscribersByKey.values()).forEach(call)
|
|
||||||
unsubscribeSpaceUrls()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,10 +353,10 @@ const syncDMs = () => {
|
|||||||
unsubscribeAll()
|
unsubscribeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a pubkey, refresh our user's relay selections then sync our subscriptions
|
// If we have a pubkey, refresh our user's relay list then sync our subscriptions
|
||||||
if ($pubkey && $shouldUnwrap) {
|
if ($pubkey && $shouldUnwrap) {
|
||||||
loadRelaySelections($pubkey)
|
loadRelayList($pubkey)
|
||||||
.then(() => loadInboxRelaySelections($pubkey))
|
.then(() => loadMessagingRelayList($pubkey))
|
||||||
.then($l => subscribeAll($pubkey, getRelayTagValues(getListTags($l))))
|
.then($l => subscribeAll($pubkey, getRelayTagValues(getListTags($l))))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,27 +364,27 @@ const syncDMs = () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// When user inbox relays change, update synchronization
|
// When user messaging relays change, update synchronization
|
||||||
const unsubscribeSelections = userInboxRelaySelections.subscribe($userInboxRelaySelections => {
|
const unsubscribeList = userMessagingRelayList.subscribe($userMessagingRelayList => {
|
||||||
const $pubkey = pubkey.get()
|
const $pubkey = pubkey.get()
|
||||||
const $shouldUnwrap = shouldUnwrap.get()
|
const $shouldUnwrap = shouldUnwrap.get()
|
||||||
|
|
||||||
if ($pubkey && $shouldUnwrap) {
|
if ($pubkey && $shouldUnwrap) {
|
||||||
subscribeAll($pubkey, getRelayTagValues(getListTags($userInboxRelaySelections)))
|
subscribeAll($pubkey, getRelayTagValues(getListTags($userMessagingRelayList)))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribeAll()
|
unsubscribeAll()
|
||||||
unsubscribePubkey()
|
unsubscribePubkey()
|
||||||
unsubscribeSelections()
|
unsubscribeList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge all synchronization functions
|
// Merge all synchronization functions
|
||||||
|
|
||||||
export const syncApplicationData = () => {
|
export const syncApplicationData = () => {
|
||||||
const unsubscribers = [syncRelays(), syncUserData(), syncSpaces(), syncRooms(), syncDMs()]
|
const unsubscribers = [syncRelays(), syncUserData(), syncSpaces(), syncDMs()]
|
||||||
|
|
||||||
return () => unsubscribers.forEach(call)
|
return () => unsubscribers.forEach(call)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type {NodeViewProps} from "@tiptap/core"
|
import type {NodeViewProps} from "@tiptap/core"
|
||||||
import {removeNil} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {deriveProfileDisplay} from "@welshman/app"
|
import {deriveProfileDisplay} from "@welshman/app"
|
||||||
|
|
||||||
export const makeMentionNodeView =
|
export const makeMentionNodeView =
|
||||||
(url?: string) =>
|
(url?: string) =>
|
||||||
({node}: NodeViewProps) => {
|
({node}: NodeViewProps) => {
|
||||||
const dom = document.createElement("span")
|
const dom = document.createElement("span")
|
||||||
const display = deriveProfileDisplay(node.attrs.pubkey, removeNil([url]))
|
const display = deriveProfileDisplay(node.attrs.pubkey, removeUndefined([url]))
|
||||||
|
|
||||||
dom.classList.add("tiptap-object")
|
dom.classList.add("tiptap-object")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {removeNil} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {displayPubkey} from "@welshman/util"
|
import {displayPubkey} from "@welshman/util"
|
||||||
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
|
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
|
||||||
import WotScore from "@app/components/WotScore.svelte"
|
import WotScore from "@app/components/WotScore.svelte"
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
const {value, url}: Props = $props()
|
const {value, url}: Props = $props()
|
||||||
|
|
||||||
const pubkey = value
|
const pubkey = value
|
||||||
const profileDisplay = deriveProfileDisplay(pubkey, removeNil([url]))
|
const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
|
||||||
const handle = deriveHandleForPubkey(pubkey)
|
const handle = deriveHandleForPubkey(pubkey)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import {
|
|||||||
profiles,
|
profiles,
|
||||||
searchProfiles,
|
searchProfiles,
|
||||||
handlesByNip05,
|
handlesByNip05,
|
||||||
maxWot,
|
getMaxWot,
|
||||||
wotGraph,
|
getWotGraph,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {FileAttributes} from "@welshman/editor"
|
import type {FileAttributes} from "@welshman/editor"
|
||||||
import {Editor, MentionSuggestion, WelshmanExtension} from "@welshman/editor"
|
import {Editor, MentionSuggestion, WelshmanExtension, editorProps} from "@welshman/editor"
|
||||||
import {makeMentionNodeView} from "@app/editor/MentionNodeView"
|
import {makeMentionNodeView} from "@app/editor/MentionNodeView"
|
||||||
import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
|
import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
|
||||||
import {uploadFile} from "@app/core/commands"
|
import {uploadFile} from "@app/core/commands"
|
||||||
@@ -62,10 +62,10 @@ export const makeEditor = async ({
|
|||||||
onSearch: searchProfiles,
|
onSearch: searchProfiles,
|
||||||
getValue: (profile: PublishedProfile) => profile.event.pubkey,
|
getValue: (profile: PublishedProfile) => profile.event.pubkey,
|
||||||
sortFn: ({score = 1, item}) => {
|
sortFn: ({score = 1, item}) => {
|
||||||
const wotScore = wotGraph.get().get(item.event.pubkey) || 0
|
const wotScore = getWotGraph().get(item.event.pubkey) || 0
|
||||||
const membershipScale = $spaceMembers.includes(item.event.pubkey) ? 2 : 1
|
const membershipScale = $spaceMembers.includes(item.event.pubkey) ? 2 : 1
|
||||||
|
|
||||||
return dec(score) * inc(wotScore / maxWot.get()) * membershipScale
|
return dec(score) * inc(wotScore / getMaxWot()) * membershipScale
|
||||||
},
|
},
|
||||||
fuseOptions: {
|
fuseOptions: {
|
||||||
keys: [
|
keys: [
|
||||||
@@ -84,6 +84,7 @@ export const makeEditor = async ({
|
|||||||
return new Editor({
|
return new Editor({
|
||||||
content,
|
content,
|
||||||
autofocus,
|
autofocus,
|
||||||
|
editorProps,
|
||||||
element: document.createElement("div"),
|
element: document.createElement("div"),
|
||||||
extensions: [
|
extensions: [
|
||||||
WelshmanExtension.configure({
|
WelshmanExtension.configure({
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {Capacitor} from "@capacitor/core"
|
||||||
|
import {Keyboard} from "@capacitor/keyboard"
|
||||||
|
import {noop} from "@welshman/lib"
|
||||||
|
|
||||||
|
export const syncKeyboard = () => {
|
||||||
|
if (!Capacitor.isNativePlatform()) return noop
|
||||||
|
|
||||||
|
const showListener = Keyboard.addListener("keyboardWillShow", () => {
|
||||||
|
document.body.classList.add("keyboard-open")
|
||||||
|
})
|
||||||
|
|
||||||
|
const hideListener = Keyboard.addListener("keyboardWillHide", () => {
|
||||||
|
document.body.classList.remove("keyboard-open")
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
showListener.then(listener => listener.remove())
|
||||||
|
hideListener.then(listener => listener.remove())
|
||||||
|
document.body.classList.remove("keyboard-open")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
||||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
import {Nip46Broker} from "@welshman/signer"
|
||||||
import {
|
import {makeSecret} from "@welshman/util"
|
||||||
NIP46_PERMS,
|
import {PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO, SIGNER_RELAYS} from "@app/core/state"
|
||||||
PLATFORM_URL,
|
|
||||||
PLATFORM_NAME,
|
|
||||||
PLATFORM_LOGO,
|
|
||||||
SIGNER_RELAYS,
|
|
||||||
} from "@app/core/state"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
export class Nip46Controller {
|
export class Nip46Controller {
|
||||||
@@ -25,7 +20,6 @@ export class Nip46Controller {
|
|||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
const url = await this.broker.makeNostrconnectUrl({
|
const url = await this.broker.makeNostrconnectUrl({
|
||||||
perms: NIP46_PERMS,
|
|
||||||
url: PLATFORM_URL,
|
url: PLATFORM_URL,
|
||||||
name: PLATFORM_NAME,
|
name: PLATFORM_NAME,
|
||||||
image: PLATFORM_LOGO,
|
image: PLATFORM_LOGO,
|
||||||
|
|||||||
+164
-154
@@ -1,8 +1,10 @@
|
|||||||
import {derived, get} from "svelte/store"
|
import {derived, get} from "svelte/store"
|
||||||
|
import {Badge} from "@capawesome/capacitor-badge"
|
||||||
import {synced, throttled} from "@welshman/store"
|
import {synced, throttled} from "@welshman/store"
|
||||||
import {pubkey, relaysByUrl} from "@welshman/app"
|
import {pubkey, tracker, repository, relaysByUrl} from "@welshman/app"
|
||||||
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
|
import {prop, find, call, spec, first, identity, now, groupBy} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {deriveEventsByIdByUrl} from "@welshman/store"
|
||||||
import {ZAP_GOAL, EVENT_TIME, MESSAGE, THREAD, COMMENT, getTagValue} from "@welshman/util"
|
import {ZAP_GOAL, EVENT_TIME, MESSAGE, THREAD, COMMENT, getTagValue} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
makeSpacePath,
|
makeSpacePath,
|
||||||
@@ -14,24 +16,21 @@ import {
|
|||||||
makeRoomPath,
|
makeRoomPath,
|
||||||
} from "@app/util/routes"
|
} from "@app/util/routes"
|
||||||
import {
|
import {
|
||||||
chats,
|
chatsById,
|
||||||
hasNip29,
|
hasNip29,
|
||||||
getUrlsForEvent,
|
|
||||||
repositoryStore,
|
|
||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
userGroupSelections,
|
userGroupList,
|
||||||
getSpaceUrlsFromGroupSelections,
|
getSpaceUrlsFromGroupList,
|
||||||
getSpaceRoomsFromGroupSelections,
|
getSpaceRoomsFromGroupList,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {preferencesStorageProvider} from "@src/lib/storage"
|
import {kv} from "@app/core/storage"
|
||||||
import {Badge} from "@capawesome/capacitor-badge"
|
|
||||||
|
|
||||||
// Checked state
|
// Checked state
|
||||||
|
|
||||||
export const checked = synced<Record<string, number>>({
|
export const checked = synced<Record<string, number>>({
|
||||||
key: "checked",
|
key: "checked",
|
||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
storage: preferencesStorageProvider,
|
storage: kv,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deriveChecked = (key: string) => derived(checked, prop(key))
|
export const deriveChecked = (key: string) => derived(checked, prop(key))
|
||||||
@@ -40,159 +39,170 @@ export const setChecked = (key: string) => checked.update(state => ({...state, [
|
|||||||
|
|
||||||
// Derived notifications state
|
// Derived notifications state
|
||||||
|
|
||||||
export const notifications = derived(
|
export const notifications = call(() => {
|
||||||
throttled(
|
const goalCommentFilters = [{kinds: [COMMENT], "#K": [String(ZAP_GOAL)]}]
|
||||||
1000,
|
const threadCommentFilters = [{kinds: [COMMENT], "#K": [String(THREAD)]}]
|
||||||
derived(
|
const calendarCommentFilters = [{kinds: [COMMENT], "#K": [String(EVENT_TIME)]}]
|
||||||
[pubkey, checked, chats, userGroupSelections, repositoryStore, getUrlsForEvent, relaysByUrl],
|
const messageFilters = [{kinds: [MESSAGE, THREAD, ZAP_GOAL, EVENT_TIME]}]
|
||||||
identity,
|
|
||||||
|
return derived(
|
||||||
|
throttled(
|
||||||
|
1000,
|
||||||
|
derived(
|
||||||
|
[
|
||||||
|
pubkey,
|
||||||
|
checked,
|
||||||
|
chatsById,
|
||||||
|
userGroupList,
|
||||||
|
relaysByUrl,
|
||||||
|
deriveEventsByIdByUrl({tracker, repository, filters: goalCommentFilters}),
|
||||||
|
deriveEventsByIdByUrl({tracker, repository, filters: threadCommentFilters}),
|
||||||
|
deriveEventsByIdByUrl({tracker, repository, filters: calendarCommentFilters}),
|
||||||
|
deriveEventsByIdByUrl({tracker, repository, filters: messageFilters}),
|
||||||
|
],
|
||||||
|
identity,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
([
|
||||||
([
|
$pubkey,
|
||||||
$pubkey,
|
$checked,
|
||||||
$checked,
|
$chatsById,
|
||||||
$chats,
|
$userGroupList,
|
||||||
$userGroupSelections,
|
$relaysByUrl,
|
||||||
$repository,
|
goalCommentsByUrl,
|
||||||
$getUrlsForEvent,
|
threadCommentsByUrl,
|
||||||
$relaysByUrl,
|
calendarCommentsByUrl,
|
||||||
]) => {
|
messagesByUrl,
|
||||||
const hasNotification = (path: string, latestEvent: TrustedEvent | undefined) => {
|
]) => {
|
||||||
if (!latestEvent || latestEvent.pubkey === $pubkey) {
|
const hasNotification = (path: string, latestEvent: TrustedEvent | undefined) => {
|
||||||
return false
|
if (!latestEvent || latestEvent.pubkey === $pubkey) {
|
||||||
}
|
|
||||||
|
|
||||||
for (const [entryPath, ts] of Object.entries($checked)) {
|
|
||||||
const isMatch =
|
|
||||||
entryPath === "*" ||
|
|
||||||
entryPath.startsWith(path) ||
|
|
||||||
(entryPath === "/chat/*" && path.startsWith("/chat/"))
|
|
||||||
|
|
||||||
if (isMatch && ts > latestEvent.created_at) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
for (const [entryPath, ts] of Object.entries($checked)) {
|
||||||
}
|
const isMatch =
|
||||||
|
entryPath === "*" ||
|
||||||
|
entryPath.startsWith(path) ||
|
||||||
|
(entryPath === "/chat/*" && path.startsWith("/chat/"))
|
||||||
|
|
||||||
const paths = new Set<string>()
|
if (isMatch && ts > latestEvent.created_at) {
|
||||||
|
return false
|
||||||
for (const {pubkeys, messages} of $chats) {
|
|
||||||
const chatPath = makeChatPath(pubkeys)
|
|
||||||
|
|
||||||
if (hasNotification(chatPath, messages[0])) {
|
|
||||||
paths.add("/chat")
|
|
||||||
paths.add(chatPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allGoalComments = $repository.query([{kinds: [COMMENT], "#K": [String(ZAP_GOAL)]}])
|
|
||||||
|
|
||||||
const allThreadComments = $repository.query([{kinds: [COMMENT], "#K": [String(THREAD)]}])
|
|
||||||
|
|
||||||
const allCalendarComments = $repository.query([{kinds: [COMMENT], "#K": [String(EVENT_TIME)]}])
|
|
||||||
|
|
||||||
const allMessages = $repository.query([{kinds: [MESSAGE, THREAD, ZAP_GOAL, EVENT_TIME]}])
|
|
||||||
|
|
||||||
for (const url of getSpaceUrlsFromGroupSelections($userGroupSelections)) {
|
|
||||||
const spacePath = makeSpacePath(url)
|
|
||||||
const spacePathMobile = spacePath + ":mobile"
|
|
||||||
const goalPath = makeGoalPath(url)
|
|
||||||
const threadPath = makeThreadPath(url)
|
|
||||||
const calendarPath = makeCalendarPath(url)
|
|
||||||
const messagesPath = makeSpaceChatPath(url)
|
|
||||||
const goalComments = allGoalComments.filter(e => $getUrlsForEvent(e.id).includes(url))
|
|
||||||
const threadComments = allThreadComments.filter(e => $getUrlsForEvent(e.id).includes(url))
|
|
||||||
const calendarComments = allCalendarComments.filter(e => $getUrlsForEvent(e.id).includes(url))
|
|
||||||
const messages = allMessages.filter(e => $getUrlsForEvent(e.id).includes(url))
|
|
||||||
|
|
||||||
const commentsByGoalId = groupBy(
|
|
||||||
e => getTagValue("E", e.tags),
|
|
||||||
goalComments.filter(spec({kind: COMMENT})),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const [goalId, [comment]] of commentsByGoalId.entries()) {
|
|
||||||
const goalItemPath = makeGoalPath(url, goalId)
|
|
||||||
|
|
||||||
if (hasNotification(spacePathMobile, comment)) {
|
|
||||||
paths.add(spacePathMobile)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNotification(goalPath, comment)) {
|
|
||||||
paths.add(goalPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNotification(goalItemPath, comment)) {
|
|
||||||
paths.add(goalItemPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const commentsByThreadId = groupBy(
|
|
||||||
e => getTagValue("E", e.tags),
|
|
||||||
threadComments.filter(spec({kind: COMMENT})),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const [threadId, [comment]] of commentsByThreadId.entries()) {
|
|
||||||
const threadItemPath = makeThreadPath(url, threadId)
|
|
||||||
|
|
||||||
if (hasNotification(spacePathMobile, comment)) {
|
|
||||||
paths.add(spacePathMobile)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNotification(threadPath, comment)) {
|
|
||||||
paths.add(threadPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNotification(threadItemPath, comment)) {
|
|
||||||
paths.add(threadItemPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const commentsByEventId = groupBy(
|
|
||||||
e => getTagValue("E", e.tags),
|
|
||||||
calendarComments.filter(spec({kind: COMMENT})),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const [eventId, [comment]] of commentsByEventId.entries()) {
|
|
||||||
const calendarItemPath = makeCalendarPath(url, eventId)
|
|
||||||
|
|
||||||
if (hasNotification(spacePathMobile, comment)) {
|
|
||||||
paths.add(spacePathMobile)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNotification(calendarPath, comment)) {
|
|
||||||
paths.add(calendarPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNotification(calendarItemPath, comment)) {
|
|
||||||
paths.add(calendarItemPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNip29($relaysByUrl.get(url))) {
|
|
||||||
for (const h of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
|
|
||||||
const roomPath = makeRoomPath(url, h)
|
|
||||||
const latestEvent = messages.find(e => e.tags.some(spec(["h", h])))
|
|
||||||
|
|
||||||
if (hasNotification(roomPath, latestEvent)) {
|
|
||||||
paths.add(spacePathMobile)
|
|
||||||
paths.add(spacePath)
|
|
||||||
paths.add(roomPath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (hasNotification(messagesPath, messages[0])) {
|
return true
|
||||||
paths.add(spacePathMobile)
|
}
|
||||||
paths.add(spacePath)
|
|
||||||
paths.add(messagesPath)
|
const paths = new Set<string>()
|
||||||
|
|
||||||
|
for (const {pubkeys, messages} of $chatsById.values()) {
|
||||||
|
const chatPath = makeChatPath(pubkeys)
|
||||||
|
|
||||||
|
if (hasNotification(chatPath, messages[0])) {
|
||||||
|
paths.add("/chat")
|
||||||
|
paths.add(chatPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return paths
|
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
|
||||||
},
|
const spacePath = makeSpacePath(url)
|
||||||
)
|
const spacePathMobile = spacePath + ":mobile"
|
||||||
|
const goalPath = makeGoalPath(url)
|
||||||
|
const threadPath = makeThreadPath(url)
|
||||||
|
const calendarPath = makeCalendarPath(url)
|
||||||
|
const messagesPath = makeSpaceChatPath(url)
|
||||||
|
const goalComments = goalCommentsByUrl.get(url)?.values() || []
|
||||||
|
const threadComments = threadCommentsByUrl.get(url)?.values() || []
|
||||||
|
const calendarComments = calendarCommentsByUrl.get(url)?.values() || []
|
||||||
|
const messages = messagesByUrl.get(url)?.values() || []
|
||||||
|
|
||||||
|
const commentsByGoalId = groupBy(
|
||||||
|
e => getTagValue("E", e.tags),
|
||||||
|
goalComments.filter(spec({kind: COMMENT})),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [goalId, [comment]] of commentsByGoalId.entries()) {
|
||||||
|
const goalItemPath = makeGoalPath(url, goalId)
|
||||||
|
|
||||||
|
if (hasNotification(spacePathMobile, comment)) {
|
||||||
|
paths.add(spacePathMobile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNotification(goalPath, comment)) {
|
||||||
|
paths.add(goalPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNotification(goalItemPath, comment)) {
|
||||||
|
paths.add(goalItemPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentsByThreadId = groupBy(
|
||||||
|
e => getTagValue("E", e.tags),
|
||||||
|
threadComments.filter(spec({kind: COMMENT})),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [threadId, [comment]] of commentsByThreadId.entries()) {
|
||||||
|
const threadItemPath = makeThreadPath(url, threadId)
|
||||||
|
|
||||||
|
if (hasNotification(spacePathMobile, comment)) {
|
||||||
|
paths.add(spacePathMobile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNotification(threadPath, comment)) {
|
||||||
|
paths.add(threadPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNotification(threadItemPath, comment)) {
|
||||||
|
paths.add(threadItemPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentsByEventId = groupBy(
|
||||||
|
e => getTagValue("E", e.tags),
|
||||||
|
calendarComments.filter(spec({kind: COMMENT})),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [eventId, [comment]] of commentsByEventId.entries()) {
|
||||||
|
const calendarItemPath = makeCalendarPath(url, eventId)
|
||||||
|
|
||||||
|
if (hasNotification(spacePathMobile, comment)) {
|
||||||
|
paths.add(spacePathMobile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNotification(calendarPath, comment)) {
|
||||||
|
paths.add(calendarPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNotification(calendarItemPath, comment)) {
|
||||||
|
paths.add(calendarItemPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNip29($relaysByUrl.get(url))) {
|
||||||
|
for (const h of getSpaceRoomsFromGroupList(url, $userGroupList)) {
|
||||||
|
const roomPath = makeRoomPath(url, h)
|
||||||
|
const latestEvent = find(e => e.tags.some(spec(["h", h])), messages)
|
||||||
|
|
||||||
|
if (hasNotification(roomPath, latestEvent)) {
|
||||||
|
paths.add(spacePathMobile)
|
||||||
|
paths.add(spacePath)
|
||||||
|
paths.add(roomPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hasNotification(messagesPath, first(messages))) {
|
||||||
|
paths.add(spacePathMobile)
|
||||||
|
paths.add(spacePath)
|
||||||
|
paths.add(messagesPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
export const badgeCount = derived(notifications, notifications => {
|
export const badgeCount = derived(notifications, notifications => {
|
||||||
return notifications.size
|
return notifications.size
|
||||||
|
|||||||
@@ -58,14 +58,18 @@ export const trustPolicy = (socket: Socket) => {
|
|||||||
export const mostlyRestrictedPolicy = (socket: Socket) => {
|
export const mostlyRestrictedPolicy = (socket: Socket) => {
|
||||||
let total = 0
|
let total = 0
|
||||||
let restricted = 0
|
let restricted = 0
|
||||||
let error = ""
|
|
||||||
|
|
||||||
const pending = new Set<string>()
|
const pending = new Set<string>()
|
||||||
|
|
||||||
const updateStatus = () =>
|
const updateStatus = (error?: string) => {
|
||||||
relaysMostlyRestricted.update(
|
if (restricted > total / 2) {
|
||||||
restricted > total / 2 ? assoc(socket.url, error) : dissoc(socket.url),
|
if (error) {
|
||||||
)
|
return relaysMostlyRestricted.update(assoc(socket.url, error))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
relaysMostlyRestricted.update(dissoc(socket.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const unsubscribers = [
|
const unsubscribers = [
|
||||||
on(socket, SocketEvent.Receive, (message: RelayMessage) => {
|
on(socket, SocketEvent.Receive, (message: RelayMessage) => {
|
||||||
@@ -83,8 +87,7 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
|
|||||||
|
|
||||||
if (details.startsWith("restricted: ")) {
|
if (details.startsWith("restricted: ")) {
|
||||||
restricted++
|
restricted++
|
||||||
error = details
|
updateStatus(details)
|
||||||
updateStatus()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,8 +106,7 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
|
|||||||
|
|
||||||
if (details.startsWith("restricted: ")) {
|
if (details.startsWith("restricted: ")) {
|
||||||
restricted++
|
restricted++
|
||||||
error = details
|
updateStatus(details)
|
||||||
updateStatus()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type {Page} from "@sveltejs/kit"
|
|||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {nthEq, sleep} from "@welshman/lib"
|
import {nthEq, remove, sleep} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {tracker, loadRelay} from "@welshman/app"
|
import {pubkey, tracker, loadRelay} from "@welshman/app"
|
||||||
import {scrollToEvent} from "@lib/html"
|
import {scrollToEvent} from "@lib/html"
|
||||||
import {identity} from "@welshman/lib"
|
import {identity} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
@@ -55,7 +55,11 @@ export const goToSpace = async (url: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}`
|
export const makeChatPath = (pubkeys: string[]) => {
|
||||||
|
const id = makeChatId(remove(pubkey.get()!, pubkeys))
|
||||||
|
|
||||||
|
return `/chat/${id}`
|
||||||
|
}
|
||||||
|
|
||||||
export const makeRoomPath = (url: string, h: string) => `/spaces/${encodeRelay(url)}/${h}`
|
export const makeRoomPath = (url: string, h: string) => `/spaces/${encodeRelay(url)}/${h}`
|
||||||
|
|
||||||
|
|||||||
+186
-177
@@ -1,5 +1,5 @@
|
|||||||
import {prop, call, on, throttle, fromPairs, batch} from "@welshman/lib"
|
import {on, throttle, indexBy, fromPairs, batch} from "@welshman/lib"
|
||||||
import {throttled, freshness} from "@welshman/store"
|
import {throttled} from "@welshman/store"
|
||||||
import {
|
import {
|
||||||
ALERT_ANDROID,
|
ALERT_ANDROID,
|
||||||
ALERT_EMAIL,
|
ALERT_EMAIL,
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
DIRECT_MESSAGE,
|
DIRECT_MESSAGE,
|
||||||
EVENT_TIME,
|
EVENT_TIME,
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
INBOX_RELAYS,
|
MESSAGING_RELAYS,
|
||||||
MESSAGE,
|
MESSAGE,
|
||||||
MUTES,
|
MUTES,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
@@ -38,45 +38,27 @@ import type {Zapper, TrustedEvent, RelayProfile} from "@welshman/util"
|
|||||||
import type {RepositoryUpdate, WrapItem} from "@welshman/net"
|
import type {RepositoryUpdate, WrapItem} from "@welshman/net"
|
||||||
import type {Handle, RelayStats} from "@welshman/app"
|
import type {Handle, RelayStats} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
plaintext,
|
|
||||||
tracker,
|
tracker,
|
||||||
relays,
|
plaintext,
|
||||||
relayStats,
|
|
||||||
repository,
|
repository,
|
||||||
handles,
|
relaysByUrl,
|
||||||
zappers,
|
relayStatsByUrl,
|
||||||
|
onRelayStats,
|
||||||
|
handlesByNip05,
|
||||||
|
zappersByLnurl,
|
||||||
onZapper,
|
onZapper,
|
||||||
onHandle,
|
onHandle,
|
||||||
wrapManager,
|
wrapManager,
|
||||||
|
onRelay,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {Collection} from "@lib/storage"
|
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
|
import type {IDBTable} from "@lib/indexeddb"
|
||||||
|
|
||||||
const syncEvents = async () => {
|
const kinds = {
|
||||||
const collection = new Collection<TrustedEvent>({table: "events", getId: prop("id")})
|
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS],
|
||||||
|
alert: [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID],
|
||||||
const initialEvents = await collection.get()
|
space: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS, RELAY_JOIN, RELAY_LEAVE],
|
||||||
|
room: [
|
||||||
// Mark events verified to avoid re-verification of signatures
|
|
||||||
for (const event of initialEvents) {
|
|
||||||
event[verifiedSymbol] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
repository.load(initialEvents)
|
|
||||||
|
|
||||||
const metaKinds = [
|
|
||||||
PROFILE,
|
|
||||||
FOLLOWS,
|
|
||||||
MUTES,
|
|
||||||
RELAYS,
|
|
||||||
BLOSSOM_SERVERS,
|
|
||||||
INBOX_RELAYS,
|
|
||||||
APP_DATA,
|
|
||||||
ROOMS,
|
|
||||||
]
|
|
||||||
const alertKinds = [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]
|
|
||||||
const spaceKinds = [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS, RELAY_JOIN, RELAY_LEAVE]
|
|
||||||
const roomKinds = [
|
|
||||||
ROOM_META,
|
ROOM_META,
|
||||||
ROOM_DELETE,
|
ROOM_DELETE,
|
||||||
ROOM_ADMINS,
|
ROOM_ADMINS,
|
||||||
@@ -84,178 +66,205 @@ const syncEvents = async () => {
|
|||||||
ROOM_ADD_MEMBER,
|
ROOM_ADD_MEMBER,
|
||||||
ROOM_REMOVE_MEMBER,
|
ROOM_REMOVE_MEMBER,
|
||||||
ROOM_CREATE_PERMISSION,
|
ROOM_CREATE_PERMISSION,
|
||||||
]
|
],
|
||||||
const contentKinds = [EVENT_TIME, THREAD, MESSAGE, ZAP_GOAL, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]
|
content: [EVENT_TIME, THREAD, MESSAGE, ZAP_GOAL, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE],
|
||||||
|
}
|
||||||
|
|
||||||
const rankEvent = (event: TrustedEvent) => {
|
const rankEvent = (event: TrustedEvent) => {
|
||||||
if (metaKinds.includes(event.kind)) return 9
|
if (kinds.meta.includes(event.kind)) return 9
|
||||||
if (alertKinds.includes(event.kind)) return 8
|
if (kinds.alert.includes(event.kind)) return 8
|
||||||
if (spaceKinds.includes(event.kind)) return 7
|
if (kinds.space.includes(event.kind)) return 7
|
||||||
if (roomKinds.includes(event.kind)) return 6
|
if (kinds.room.includes(event.kind)) return 6
|
||||||
if (!isMobile && contentKinds.includes(event.kind)) return 5
|
if (!isMobile && kinds.content.includes(event.kind)) return 5
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return on(
|
const eventsAdapter = {
|
||||||
repository,
|
name: "events",
|
||||||
"update",
|
keyPath: "id",
|
||||||
batch(3000, async (updates: RepositoryUpdate[]) => {
|
init: async (table: IDBTable<TrustedEvent>) => {
|
||||||
const add: TrustedEvent[] = []
|
const initialEvents = await table.getAll()
|
||||||
const remove = new Set<string>()
|
|
||||||
|
|
||||||
for (const update of updates) {
|
// Mark events verified to avoid re-verification of signatures
|
||||||
for (const event of update.added) {
|
for (const event of initialEvents) {
|
||||||
if (rankEvent(event) > 0) {
|
event[verifiedSymbol] = true
|
||||||
add.push(event)
|
}
|
||||||
remove.delete(event.id)
|
|
||||||
|
repository.load(initialEvents)
|
||||||
|
|
||||||
|
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 (rankEvent(event) > 0) {
|
||||||
|
add.push(event)
|
||||||
|
remove.delete(event.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of update.removed) {
|
||||||
|
remove.add(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const id of update.removed) {
|
if (add.length > 0) {
|
||||||
remove.add(id)
|
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})
|
||||||
}
|
}
|
||||||
|
|
||||||
await collection.update({add, remove})
|
await table.bulkPut(items)
|
||||||
}),
|
}
|
||||||
)
|
|
||||||
|
const _onRemove = async (ids: Iterable<string>) => {
|
||||||
|
await table.bulkDelete(Array.from(ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAdd = batch(3000, _onAdd)
|
||||||
|
|
||||||
|
const onRemove = batch(3000, _onRemove)
|
||||||
|
|
||||||
|
const onLoad = () => _onAdd(tracker.relaysById.keys())
|
||||||
|
|
||||||
|
const onClear = () => _onRemove(tracker.relaysById.keys())
|
||||||
|
|
||||||
|
tracker.on("add", onAdd)
|
||||||
|
tracker.on("remove", onRemove)
|
||||||
|
tracker.on("load", onLoad)
|
||||||
|
tracker.on("clear", onClear)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tracker.off("add", onAdd)
|
||||||
|
tracker.off("remove", onRemove)
|
||||||
|
tracker.off("load", onLoad)
|
||||||
|
tracker.off("clear", onClear)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrackerItem = [string, string[]]
|
const relaysAdapter = {
|
||||||
|
name: "relays",
|
||||||
|
keyPath: "url",
|
||||||
|
init: async (table: IDBTable<RelayProfile>) => {
|
||||||
|
relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
|
||||||
|
|
||||||
const syncTracker = async () => {
|
return onRelay(batch(1000, table.bulkPut))
|
||||||
const collection = new Collection<TrackerItem>({
|
},
|
||||||
table: "tracker",
|
|
||||||
getId: (item: TrackerItem) => item[0],
|
|
||||||
})
|
|
||||||
|
|
||||||
const relaysById = new Map<string, Set<string>>()
|
|
||||||
|
|
||||||
for (const [id, relays] of await collection.get()) {
|
|
||||||
relaysById.set(id, new Set(relays))
|
|
||||||
}
|
|
||||||
|
|
||||||
tracker.load(relaysById)
|
|
||||||
|
|
||||||
const updateOne = batch(3000, (ids: string[]) => {
|
|
||||||
collection.add(ids.map(id => [id, Array.from(tracker.getRelays(id))]))
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateAll = throttle(3000, () => {
|
|
||||||
collection.set(
|
|
||||||
Array.from(tracker.relaysById.entries()).map(([id, relays]) => [id, Array.from(relays)]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
tracker.on("add", updateOne)
|
|
||||||
tracker.on("remove", updateOne)
|
|
||||||
tracker.on("load", updateAll)
|
|
||||||
tracker.on("clear", updateAll)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
tracker.off("add", updateOne)
|
|
||||||
tracker.off("remove", updateOne)
|
|
||||||
tracker.off("load", updateAll)
|
|
||||||
tracker.off("clear", updateAll)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncRelays = async () => {
|
const relayStatsAdapter = {
|
||||||
const collection = new Collection<RelayProfile>({table: "relays", getId: prop("url")})
|
name: "relayStats",
|
||||||
|
keyPath: "url",
|
||||||
|
init: async (table: IDBTable<RelayStats>) => {
|
||||||
|
relayStatsByUrl.set(indexBy(r => r.url, await table.getAll()))
|
||||||
|
|
||||||
relays.set(await collection.get())
|
return onRelayStats(batch(1000, table.bulkPut))
|
||||||
|
},
|
||||||
return throttled(3000, relays).subscribe(collection.set)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncRelayStats = async () => {
|
const handlesAdapter = {
|
||||||
const collection = new Collection<RelayStats>({table: "relayStats", getId: prop("url")})
|
name: "handles",
|
||||||
|
keyPath: "nip05",
|
||||||
|
init: async (table: IDBTable<Handle>) => {
|
||||||
|
handlesByNip05.set(indexBy(r => r.nip05, await table.getAll()))
|
||||||
|
|
||||||
relayStats.set(await collection.get())
|
return onHandle(batch(1000, table.bulkPut))
|
||||||
|
},
|
||||||
return throttled(3000, relayStats).subscribe(collection.set)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncHandles = async () => {
|
const zappersAdapter = {
|
||||||
const collection = new Collection<Handle>({table: "handles", getId: prop("nip05")})
|
name: "zappers",
|
||||||
|
keyPath: "lnurl",
|
||||||
|
init: async (table: IDBTable<Zapper>) => {
|
||||||
|
zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll()))
|
||||||
|
|
||||||
handles.set(await collection.get())
|
return onZapper(batch(3000, table.bulkPut))
|
||||||
|
},
|
||||||
return onHandle(batch(3000, collection.add))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncZappers = async () => {
|
type PlaintextItem = {key: string; value: string}
|
||||||
const collection = new Collection<Zapper>({table: "zappers", getId: prop("lnurl")})
|
|
||||||
|
|
||||||
zappers.set(await collection.get())
|
const plaintextAdapter = {
|
||||||
|
name: "plaintext",
|
||||||
|
keyPath: "key",
|
||||||
|
init: async (table: IDBTable<PlaintextItem>) => {
|
||||||
|
const initialRecords = await table.getAll()
|
||||||
|
|
||||||
return onZapper(batch(3000, collection.add))
|
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})))
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type FreshnessItem = [string, number]
|
const wrapManagerAdapter = {
|
||||||
|
name: "wrapManager",
|
||||||
|
keyPath: "id",
|
||||||
|
init: async (table: IDBTable<WrapItem>) => {
|
||||||
|
wrapManager.load(await table.getAll())
|
||||||
|
|
||||||
const syncFreshness = async () => {
|
const addOne = batch(3000, table.bulkPut)
|
||||||
const collection = new Collection<FreshnessItem>({
|
|
||||||
table: "freshness",
|
|
||||||
getId: (item: FreshnessItem) => item[0],
|
|
||||||
})
|
|
||||||
|
|
||||||
freshness.set(fromPairs(await collection.get()))
|
const removeOne = throttle(3000, table.bulkDelete)
|
||||||
|
|
||||||
return throttled(3000, freshness).subscribe($freshness => {
|
wrapManager.on("add", addOne)
|
||||||
collection.set(Object.entries($freshness))
|
wrapManager.on("remove", removeOne)
|
||||||
})
|
|
||||||
|
return () => {
|
||||||
|
wrapManager.off("add", addOne)
|
||||||
|
wrapManager.off("remove", removeOne)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaintextItem = [string, string]
|
export const adapters = [
|
||||||
|
eventsAdapter,
|
||||||
const syncPlaintext = async () => {
|
trackerAdapter,
|
||||||
const collection = new Collection<PlaintextItem>({
|
relaysAdapter,
|
||||||
table: "plaintext",
|
relayStatsAdapter,
|
||||||
getId: (item: PlaintextItem) => item[0],
|
handlesAdapter,
|
||||||
})
|
zappersAdapter,
|
||||||
|
plaintextAdapter,
|
||||||
plaintext.set(fromPairs(await collection.get()))
|
wrapManagerAdapter,
|
||||||
|
]
|
||||||
return throttled(3000, plaintext).subscribe($plaintext => {
|
|
||||||
collection.set(Object.entries($plaintext))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncWrapManager = async () => {
|
|
||||||
const collection = new Collection<WrapItem>({table: "wraps", getId: prop("id")})
|
|
||||||
|
|
||||||
wrapManager.load(await collection.get())
|
|
||||||
|
|
||||||
const addOne = batch(3000, (wrapItems: WrapItem[]) => collection.add(wrapItems))
|
|
||||||
|
|
||||||
const updateAll = throttle(3000, () => collection.set(wrapManager.dump()))
|
|
||||||
|
|
||||||
wrapManager.on("add", addOne)
|
|
||||||
wrapManager.on("remove", updateAll)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
wrapManager.off("add", addOne)
|
|
||||||
wrapManager.off("remove", updateAll)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const syncDataStores = async () => {
|
|
||||||
const promises = [
|
|
||||||
syncEvents(),
|
|
||||||
syncTracker(),
|
|
||||||
syncRelays(),
|
|
||||||
syncHandles(),
|
|
||||||
syncZappers(),
|
|
||||||
syncPlaintext(),
|
|
||||||
syncWrapManager(),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (!isMobile) {
|
|
||||||
promises.push(syncFreshness(), syncRelayStats())
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribers = await Promise.all(promises)
|
|
||||||
|
|
||||||
return () => unsubscribers.forEach(call)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {preferencesStorageProvider} from "@src/lib/storage"
|
import {kv} from "@app/core/storage"
|
||||||
import {synced} from "@welshman/store"
|
import {synced} from "@welshman/store"
|
||||||
|
|
||||||
export const theme = synced({
|
export const theme = synced({
|
||||||
key: "theme",
|
key: "theme",
|
||||||
defaultValue: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
|
defaultValue: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
|
||||||
storage: preferencesStorageProvider,
|
storage: kv,
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user