Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1510f39a8a | |||
| bbbe011482 | |||
| 82ab7a043f | |||
| 798253a50e | |||
| 52432ca068 | |||
| b3f1d8464b | |||
| 87bb62b359 | |||
| 3f914d02cc | |||
| d1db77d0f5 | |||
| 6aa297c1a4 | |||
| f3647e9bc1 | |||
| 5b43c62f2d | |||
| 23ffb15a8d | |||
| adb2ce4846 | |||
| cdee6ca743 | |||
| fe30aa4af2 | |||
| 9943728eab | |||
| 8ae7cf05cc | |||
| a7c944e8ef | |||
| 102339d7e8 | |||
| 9a0ad0c663 | |||
| f86afc08fa | |||
| cd1b328b1b | |||
| 48f2bb1c75 | |||
| d416fe913e | |||
| 7f8744725c | |||
| e5d1b82a9d | |||
| 619cf2e134 | |||
| 28b522f015 | |||
| 39233f261e | |||
| 00f0127caf | |||
| f69b575381 | |||
| 986973a605 | |||
| 0d6b4591f1 | |||
| 2c62749d9b | |||
| 4be4288ef0 | |||
| c7eec167cf | |||
| 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 |
+6
-4
@@ -1,6 +1,6 @@
|
|||||||
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||||
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
||||||
VITE_BURROW_URL=
|
VITE_POMADE_SIGNERS=
|
||||||
VITE_PLATFORM_URL=https://app.flotilla.social
|
VITE_PLATFORM_URL=https://app.flotilla.social
|
||||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||||
@@ -10,10 +10,12 @@ VITE_PLATFORM_RELAYS=
|
|||||||
VITE_PLATFORM_ACCENT="#7161FF"
|
VITE_PLATFORM_ACCENT="#7161FF"
|
||||||
VITE_PLATFORM_SECONDARY="#EB5E28"
|
VITE_PLATFORM_SECONDARY="#EB5E28"
|
||||||
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
|
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
|
||||||
VITE_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.band/,wss://indexer.coracle.social/
|
VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
|
||||||
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://offchain.pub/
|
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
|
||||||
|
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com
|
||||||
|
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
|
||||||
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
|
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
|
||||||
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
|
VITE_NOTIFIER_RELAY=anchor.coracle.social
|
||||||
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
||||||
VITE_GLITCHTIP_API_KEY=
|
VITE_GLITCHTIP_API_KEY=
|
||||||
GLITCHTIP_AUTH_TOKEN=
|
GLITCHTIP_AUTH_TOKEN=
|
||||||
|
|||||||
@@ -1,5 +1,55 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 1.6.3
|
||||||
|
|
||||||
|
* Fix scroll down button z index
|
||||||
|
* Hide tooltips on mobile
|
||||||
|
* Sort comments ascending
|
||||||
|
* Make video embeds rounded
|
||||||
|
* Fix ProfileMultiSelect styling
|
||||||
|
* Accept hex pubkeys/npubs/nprofiles in ProfileMultiSelect
|
||||||
|
* Tweak room edit form design
|
||||||
|
* Report pending signer to user
|
||||||
|
* Update default relays
|
||||||
|
* Fix chat list responsiveness
|
||||||
|
* Fix memory leak, notification badge not showing
|
||||||
|
* Improve space join flow
|
||||||
|
* Fix opening images in fullscreen dialog
|
||||||
|
* Add support for blocked relays
|
||||||
|
* Add authentication policy setting
|
||||||
|
* Add login with key if no signer is detected
|
||||||
|
* Publish default relay selections on signup
|
||||||
|
|
||||||
|
# 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
|
# 1.5.3
|
||||||
|
|
||||||
* Add space edit form
|
* Add space edit form
|
||||||
|
|||||||
+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 34
|
versionCode 39
|
||||||
versionName "1.5.3"
|
versionName "1.6.3"
|
||||||
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.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.8.0'
|
classpath 'com.android.tools.build:gradle:8.13.2'
|
||||||
classpath 'com.google.gms:google-services:4.4.2'
|
classpath 'com.google.gms:google-services:4.4.2'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -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 = 25;
|
CURRENT_PROJECT_VERSION = 29;
|
||||||
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.3;
|
MARKETING_VERSION = 1.6.3;
|
||||||
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 = 25;
|
CURRENT_PROJECT_VERSION = 29;
|
||||||
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.3;
|
MARKETING_VERSION = 1.6.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { execSync } from 'child_process'
|
||||||
|
|
||||||
|
if (execSync('git status --porcelain', { encoding: 'utf8' }).trim()) {
|
||||||
|
console.error('Error: Git working tree is dirty. Please commit or stash your changes first.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
|
||||||
|
|
||||||
|
pkg.pnpm.overrides = pkg.pnpm.overrides || {}
|
||||||
|
pkg.pnpm.overrides["@welshman/app"] = "link:../welshman/packages/app"
|
||||||
|
pkg.pnpm.overrides["@welshman/content"] = "link:../welshman/packages/content"
|
||||||
|
pkg.pnpm.overrides["@welshman/editor"] = "link:../welshman/packages/editor"
|
||||||
|
pkg.pnpm.overrides["@welshman/feeds"] = "link:../welshman/packages/feeds"
|
||||||
|
pkg.pnpm.overrides["@welshman/lib"] = "link:../welshman/packages/lib"
|
||||||
|
pkg.pnpm.overrides["@welshman/net"] = "link:../welshman/packages/net"
|
||||||
|
pkg.pnpm.overrides["@welshman/router"] = "link:../welshman/packages/router"
|
||||||
|
pkg.pnpm.overrides["@welshman/signer"] = "link:../welshman/packages/signer"
|
||||||
|
pkg.pnpm.overrides["@welshman/store"] = "link:../welshman/packages/store"
|
||||||
|
pkg.pnpm.overrides["@welshman/util"] = "link:../welshman/packages/util"
|
||||||
|
pkg.pnpm.overrides["@pomade/core"] = "link:../pomade/packages/core"
|
||||||
|
|
||||||
|
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n')
|
||||||
|
|
||||||
|
execSync('pnpm i', { stdio: 'inherit' })
|
||||||
|
|
||||||
|
execSync('git checkout -f pnpm-lock.yaml', { stdio: 'inherit' })
|
||||||
|
execSync('git checkout -f package.json', { stdio: 'inherit' })
|
||||||
+15
-16
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "1.5.3",
|
"version": "1.6.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -10,14 +10,13 @@
|
|||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check src && eslint src",
|
"lint": "prettier --check src && eslint src",
|
||||||
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte)$' | xargs -r prettier --write",
|
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
|
||||||
"format:all": "prettier --write src",
|
"format:all": "prettier --write src",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
"@eslint/js": "^9.37.0",
|
"@eslint/js": "^9.37.0",
|
||||||
"@sentry/cli": "^2.56.1",
|
|
||||||
"@sveltejs/kit": "^2.46.5",
|
"@sveltejs/kit": "^2.46.5",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
@@ -51,25 +50,26 @@
|
|||||||
"@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",
|
||||||
|
"@pomade/core": "^0.0.12",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@sentry/browser": "^8.55.0",
|
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@tiptap/core": "^2.26.3",
|
"@tiptap/core": "^2.26.3",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@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.8",
|
"@welshman/app": "^0.8.1",
|
||||||
"@welshman/content": "^0.6.8",
|
"@welshman/content": "^0.8.1",
|
||||||
"@welshman/editor": "^0.6.8",
|
"@welshman/editor": "^0.8.1",
|
||||||
"@welshman/feeds": "^0.6.8",
|
"@welshman/feeds": "^0.8.1",
|
||||||
"@welshman/lib": "^0.6.8",
|
"@welshman/lib": "^0.8.1",
|
||||||
"@welshman/net": "^0.6.8",
|
"@welshman/net": "^0.8.1",
|
||||||
"@welshman/router": "^0.6.8",
|
"@welshman/router": "^0.8.1",
|
||||||
"@welshman/signer": "^0.6.8",
|
"@welshman/signer": "^0.8.1",
|
||||||
"@welshman/store": "^0.6.8",
|
"@welshman/store": "^0.8.1",
|
||||||
"@welshman/util": "^0.6.8",
|
"@welshman/util": "^0.8.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",
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"nostr-signer-capacitor-plugin": "^0.0.4",
|
"nostr-signer-capacitor-plugin": "^0.0.4",
|
||||||
"nostr-tools": "^2.14.2",
|
"nostr-tools": "^2.19.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
@@ -88,7 +88,6 @@
|
|||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"ignoredBuiltDependencies": [
|
"ignoredBuiltDependencies": [
|
||||||
"@sentry/cli",
|
|
||||||
"esbuild"
|
"esbuild"
|
||||||
],
|
],
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
Generated
+246
-360
@@ -44,15 +44,18 @@ 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)
|
||||||
|
'@pomade/core':
|
||||||
|
specifier: ^0.0.12
|
||||||
|
version: 0.0.12(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.1(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(nostr-tools@2.19.4(typescript@5.9.3)))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
'@poppanator/sveltekit-svg':
|
'@poppanator/sveltekit-svg':
|
||||||
specifier: ^4.2.1
|
specifier: ^4.2.1
|
||||||
version: 4.2.1(rollup@2.79.2)(svelte@5.39.12)(svgo@3.3.2)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0))
|
version: 4.2.1(rollup@2.79.2)(svelte@5.39.12)(svgo@3.3.2)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0))
|
||||||
'@sentry/browser':
|
|
||||||
specifier: ^8.55.0
|
|
||||||
version: 8.55.0
|
|
||||||
'@sveltejs/adapter-static':
|
'@sveltejs/adapter-static':
|
||||||
specifier: ^3.0.10
|
specifier: ^3.0.10
|
||||||
version: 3.0.10(@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)))
|
version: 3.0.10(@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)))
|
||||||
@@ -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.8
|
specifier: ^0.8.1
|
||||||
version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
version: 0.8.1(7cc2d202a89b51b45c1ecb6d486f020b)
|
||||||
'@welshman/content':
|
'@welshman/content':
|
||||||
specifier: ^0.6.8
|
specifier: ^0.8.1
|
||||||
version: 0.6.8(typescript@5.9.3)
|
version: 0.8.1(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
'@welshman/editor':
|
'@welshman/editor':
|
||||||
specifier: ^0.6.8
|
specifier: ^0.8.1
|
||||||
version: 0.6.8(@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.8.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))(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(linkifyjs@4.3.2)(nostr-tools@2.19.4(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)))
|
||||||
'@welshman/feeds':
|
'@welshman/feeds':
|
||||||
specifier: ^0.6.8
|
specifier: ^0.8.1
|
||||||
version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
version: 0.8.1(c3072638adf29bdf3b0a110142b651da)
|
||||||
'@welshman/lib':
|
'@welshman/lib':
|
||||||
specifier: ^0.6.8
|
specifier: ^0.8.1
|
||||||
version: 0.6.8
|
version: 0.8.1
|
||||||
'@welshman/net':
|
'@welshman/net':
|
||||||
specifier: ^0.6.8
|
specifier: ^0.8.1
|
||||||
version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
version: 0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/router':
|
'@welshman/router':
|
||||||
specifier: ^0.6.8
|
specifier: ^0.8.1
|
||||||
version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
version: 0.8.1(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))
|
||||||
'@welshman/signer':
|
'@welshman/signer':
|
||||||
specifier: ^0.6.8
|
specifier: ^0.8.1
|
||||||
version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
version: 0.8.1(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
'@welshman/store':
|
'@welshman/store':
|
||||||
specifier: ^0.6.8
|
specifier: ^0.8.1
|
||||||
version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
version: 0.8.1(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(svelte@5.39.12)
|
||||||
'@welshman/util':
|
'@welshman/util':
|
||||||
specifier: ^0.6.8
|
specifier: ^0.8.1
|
||||||
version: 0.6.8(typescript@5.9.3)
|
version: 0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
compressorjs:
|
compressorjs:
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
@@ -129,8 +132,8 @@ importers:
|
|||||||
specifier: ^0.0.4
|
specifier: ^0.0.4
|
||||||
version: 0.0.4(@capacitor/core@7.4.3)
|
version: 0.0.4(@capacitor/core@7.4.3)
|
||||||
nostr-tools:
|
nostr-tools:
|
||||||
specifier: ^2.14.2
|
specifier: ^2.19.4
|
||||||
version: 2.17.0(typescript@5.9.3)
|
version: 2.19.4(typescript@5.9.3)
|
||||||
prettier-plugin-tailwindcss:
|
prettier-plugin-tailwindcss:
|
||||||
specifier: ^0.6.14
|
specifier: ^0.6.14
|
||||||
version: 0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.39.12))(prettier@3.6.2)
|
version: 0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.39.12))(prettier@3.6.2)
|
||||||
@@ -153,9 +156,6 @@ importers:
|
|||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.37.0
|
specifier: ^9.37.0
|
||||||
version: 9.37.0
|
version: 9.37.0
|
||||||
'@sentry/cli':
|
|
||||||
specifier: ^2.56.1
|
|
||||||
version: 2.56.1
|
|
||||||
'@sveltejs/kit':
|
'@sveltejs/kit':
|
||||||
specifier: ^2.46.5
|
specifier: ^2.46.5
|
||||||
version: 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))
|
version: 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))
|
||||||
@@ -217,10 +217,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
|
||||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
|
||||||
engines: {node: '>=6.0.0'}
|
|
||||||
|
|
||||||
'@antfu/utils@0.7.10':
|
'@antfu/utils@0.7.10':
|
||||||
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
|
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
|
||||||
|
|
||||||
@@ -802,6 +798,15 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@capacitor/core': '>=7.0.0'
|
'@capacitor/core': '>=7.0.0'
|
||||||
|
|
||||||
|
'@cmdcode/buff@2.2.5':
|
||||||
|
resolution: {integrity: sha512-+nc3QDoJ+MU/fp+YkX6WuEjJrXLF6ME+eVX1sj5a+MfBKO9LWb4R9Y2zH6APBrySd7nFr48ozscAui7SKvLmXg==}
|
||||||
|
|
||||||
|
'@cmdcode/frost@1.1.3':
|
||||||
|
resolution: {integrity: sha512-Aap5+IqJCisHJzdFJS4h+i5IRaw/yrZ5m5tpoGpKPBljMWjH3K17iD53VvLEQPI85l2asDrjj8vnrFKlwnK7zw==}
|
||||||
|
|
||||||
|
'@cmdcode/nostr-p2p@2.0.11':
|
||||||
|
resolution: {integrity: sha512-hZpWYqRPdvXoaG5LaP2/dR1ObloQxPUYHAA1japQveGdhPbXWiS5aKPxOyUXqgjHDShWeXQ3giOTg6SsCJwnYA==}
|
||||||
|
|
||||||
'@cspotcode/source-map-support@0.8.1':
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -982,10 +987,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==}
|
resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@frostr/bifrost@1.0.7':
|
||||||
|
resolution: {integrity: sha512-9PO8s8ra7Cf94HqsF0sArRkLLFYqDyGfRKUOflTWMGgaDvSWIksNA8PckcXvy5/G6u4RtAkTAqki47+ga+7yow==}
|
||||||
|
|
||||||
'@getalby/lightning-tools@5.2.1':
|
'@getalby/lightning-tools@5.2.1':
|
||||||
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'}
|
||||||
@@ -1095,6 +1107,10 @@ packages:
|
|||||||
'@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==}
|
||||||
|
|
||||||
|
'@noble/ciphers@1.3.0':
|
||||||
|
resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
|
||||||
|
engines: {node: ^14.21.3 || >=16}
|
||||||
|
|
||||||
'@noble/curves@1.1.0':
|
'@noble/curves@1.1.0':
|
||||||
resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==}
|
resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==}
|
||||||
|
|
||||||
@@ -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'}
|
||||||
@@ -1139,6 +1159,18 @@ packages:
|
|||||||
'@polka/url@1.0.0-next.29':
|
'@polka/url@1.0.0-next.29':
|
||||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||||
|
|
||||||
|
'@pomade/core@0.0.12':
|
||||||
|
resolution: {integrity: sha512-xI8DSPwpm8m124RjHmcpko3lCvfobNwwl11Fkvpt5L6vgORFMGFA6UM2PGBfgKcVAXR/ao957Hza6yYpMNHEGQ==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@frostr/bifrost': ^1.0.7
|
||||||
|
'@noble/hashes': ^2.0.1
|
||||||
|
'@welshman/lib': ^0.8.0-pre.1
|
||||||
|
'@welshman/net': ^0.8.0-pre.1
|
||||||
|
'@welshman/signer': ^0.8.0-pre.1
|
||||||
|
'@welshman/util': ^0.8.0-pre.1
|
||||||
|
nostr-tools: ^2.19.3
|
||||||
|
|
||||||
'@poppanator/sveltekit-svg@4.2.1':
|
'@poppanator/sveltekit-svg@4.2.1':
|
||||||
resolution: {integrity: sha512-w7jl4EVOOF+X+uv2BEUiMDJwds+GfbczwGpcS0+rsjIsKYmqmwMi4ts3bVZR9ZvdFHWy5rS84U+pSBClz6cbBg==}
|
resolution: {integrity: sha512-w7jl4EVOOF+X+uv2BEUiMDJwds+GfbczwGpcS0+rsjIsKYmqmwMi4ts3bVZR9ZvdFHWy5rS84U+pSBClz6cbBg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1326,82 +1358,6 @@ packages:
|
|||||||
'@scure/bip39@1.2.1':
|
'@scure/bip39@1.2.1':
|
||||||
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
|
resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
|
||||||
|
|
||||||
'@sentry-internal/browser-utils@8.55.0':
|
|
||||||
resolution: {integrity: sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==}
|
|
||||||
engines: {node: '>=14.18'}
|
|
||||||
|
|
||||||
'@sentry-internal/feedback@8.55.0':
|
|
||||||
resolution: {integrity: sha512-cP3BD/Q6pquVQ+YL+rwCnorKuTXiS9KXW8HNKu4nmmBAyf7urjs+F6Hr1k9MXP5yQ8W3yK7jRWd09Yu6DHWOiw==}
|
|
||||||
engines: {node: '>=14.18'}
|
|
||||||
|
|
||||||
'@sentry-internal/replay-canvas@8.55.0':
|
|
||||||
resolution: {integrity: sha512-nIkfgRWk1091zHdu4NbocQsxZF1rv1f7bbp3tTIlZYbrH62XVZosx5iHAuZG0Zc48AETLE7K4AX9VGjvQj8i9w==}
|
|
||||||
engines: {node: '>=14.18'}
|
|
||||||
|
|
||||||
'@sentry-internal/replay@8.55.0':
|
|
||||||
resolution: {integrity: sha512-roCDEGkORwolxBn8xAKedybY+Jlefq3xYmgN2fr3BTnsXjSYOPC7D1/mYqINBat99nDtvgFvNfRcZPiwwZ1hSw==}
|
|
||||||
engines: {node: '>=14.18'}
|
|
||||||
|
|
||||||
'@sentry/browser@8.55.0':
|
|
||||||
resolution: {integrity: sha512-1A31mCEWCjaMxJt6qGUK+aDnLDcK6AwLAZnqpSchNysGni1pSn1RWSmk9TBF8qyTds5FH8B31H480uxMPUJ7Cw==}
|
|
||||||
engines: {node: '>=14.18'}
|
|
||||||
|
|
||||||
'@sentry/cli-darwin@2.56.1':
|
|
||||||
resolution: {integrity: sha512-zfhT8MrvB5x/xRdIVGwg+sG0Cx3i0G6RH2zCrdQ/moWn8TfkwsM0O1k/AxpwbpcRfAHCkVb04CU/yKciKwg2KA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@sentry/cli-linux-arm64@2.56.1':
|
|
||||||
resolution: {integrity: sha512-AypXIwZvOMJb9RgjI/98hTAd06FcOjqjIm6G9IR0OI4pJCOcaAXz9NKXdJqxpZd7phSMJnD+Bx/8iYOUPeY73A==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux, freebsd, android]
|
|
||||||
|
|
||||||
'@sentry/cli-linux-arm@2.56.1':
|
|
||||||
resolution: {integrity: sha512-fNB/Ng11HrkGOSEIDg+fc3zfTCV7q6kJddp6ndK3QlYFsCffRSnclaX1SMp+mqxdWkHqe1kkp85OY8G/x5uAWw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [arm]
|
|
||||||
os: [linux, freebsd, android]
|
|
||||||
|
|
||||||
'@sentry/cli-linux-i686@2.56.1':
|
|
||||||
resolution: {integrity: sha512-vnH+WJEsUq7Lf7xc9udzE/M4hoDXXsniFFYr/7BvdnXtCQlNNaWFMXHbEDYAql3baIlHkWoG8cEHWuB/YKyniw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [x86, ia32]
|
|
||||||
os: [linux, freebsd, android]
|
|
||||||
|
|
||||||
'@sentry/cli-linux-x64@2.56.1':
|
|
||||||
resolution: {integrity: sha512-3/BlKe5Vdnia36MeovghHJD8lbcum5TFIxLp+PSfH2sVb09+5Jo0L95oRTI2JkD8Fs+QNssvTqTxJj5eIo/n+A==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux, freebsd, android]
|
|
||||||
|
|
||||||
'@sentry/cli-win32-arm64@2.56.1':
|
|
||||||
resolution: {integrity: sha512-Gg8RV7CV7Tz4fiR1EN1Af5AVhJsnEXiZvfvfQXI4lp51MKAhcxZIMtEfg9HaWsn3Dm/wgwYBinyeywfWbTXYDg==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@sentry/cli-win32-i686@2.56.1':
|
|
||||||
resolution: {integrity: sha512-6u6a060yC3i76Ze1apqgWr5luQSyhuD5ND84eWfh/UbddsEa42UHjoVHOiBwmpZqf/hvNZAtzLnE4NCvU4zOMg==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [x86, ia32]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@sentry/cli-win32-x64@2.56.1':
|
|
||||||
resolution: {integrity: sha512-11cdflajBrDWlRZqI9MOu7ok2vnPzFjKmbU3YvBYWQapNE+HHAsWdsRL/u/P1RmU62vj7Y42iSUcj6x1SNrdPw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@sentry/cli@2.56.1':
|
|
||||||
resolution: {integrity: sha512-VDAIg+gmjNtJS5VUZQMDSK9RaKC9hYQi3PoXpNa+owNfQNk60bCi8z8jkbWRcKbNGn3V51WqvrQAqLoNAdPc9w==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
'@sentry/core@8.55.0':
|
|
||||||
resolution: {integrity: sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==}
|
|
||||||
engines: {node: '>=14.18'}
|
|
||||||
|
|
||||||
'@standard-schema/spec@1.0.0':
|
'@standard-schema/spec@1.0.0':
|
||||||
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||||
|
|
||||||
@@ -1692,38 +1648,82 @@ packages:
|
|||||||
'@vite-pwa/assets-generator':
|
'@vite-pwa/assets-generator':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@welshman/app@0.6.8':
|
'@welshman/app@0.8.1':
|
||||||
resolution: {integrity: sha512-bhl18VWA9tzHLY7D+b2xlkc/RbJr03XiA7+otcjzf8X48S4pih/F4TDw1yJbAWOMOx9G3NI6sWLffpZQeSUPiQ==}
|
resolution: {integrity: sha512-1xMVFUpFq3ZI0sqfz/5h8rlSGHzBm1o8yvSIBTFhoM+Yzz+So4qrO0fpQiYBidlA0f9AEVGcKYlKm8Vcyhqlag==}
|
||||||
|
peerDependencies:
|
||||||
|
'@pomade/core': ^0.0.12
|
||||||
|
'@welshman/feeds': 0.8.1
|
||||||
|
'@welshman/lib': 0.8.1
|
||||||
|
'@welshman/net': 0.8.1
|
||||||
|
'@welshman/router': 0.8.1
|
||||||
|
'@welshman/signer': 0.8.1
|
||||||
|
'@welshman/store': 0.8.1
|
||||||
|
'@welshman/util': 0.8.1
|
||||||
|
svelte: ^4.0.0 || ^5.0.0
|
||||||
|
|
||||||
'@welshman/content@0.6.8':
|
'@welshman/content@0.8.1':
|
||||||
resolution: {integrity: sha512-VLek8oOoMMTrEtpIfqFqM9BsbifWYwPC7UiuVuWYqaTSmiAbU3DM2J+tYFcrgnQF8xMnUi/JoVXJ+b2AtpjFrw==}
|
resolution: {integrity: sha512-DQ08ijfQQojNKWj/LRzcry6IqA45s+xpg25sc2zxP6iXE4Q0cidSoVwvJjPa2PjYWsInC6Zmj/ZQOErWtsyBjQ==}
|
||||||
|
peerDependencies:
|
||||||
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@welshman/editor@0.6.8':
|
'@welshman/editor@0.8.1':
|
||||||
resolution: {integrity: sha512-QzNX7/Nobkh+bpjFnuW2REVpX7Sa+lj70LDdGmEJpjtXlTKlLuNZzpFLee5F9fSObcKCl1G2xBN3tYbZD3vHUA==}
|
resolution: {integrity: sha512-nJdGkWVA4J/ETNKcu2LGUXCjNScd2jgP30khNdIGzXUDhKfuKZYS6MOb8B+c5U1lDbl7o2BQr7CYchtwezXFaQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@welshman/lib': 0.8.1
|
||||||
|
'@welshman/util': 0.8.1
|
||||||
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@welshman/feeds@0.6.8':
|
'@welshman/feeds@0.8.1':
|
||||||
resolution: {integrity: sha512-95VRR2QmGrBUyzYgdsMxhntVoOnaEMsMHRsci1/GX1oOFZPJFTiV7e/m/dD/aWVLUQV1hlRxxXosFFtEDkpjIw==}
|
resolution: {integrity: sha512-yUFN+lMz4R1+90VWRBDOKZFZzc3KHJVkcpKacOR+3st6CJSdPYMWgGc8eI6lXXHMIWQu9qDjMTTubx7VrsbJfA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@welshman/lib': 0.8.1
|
||||||
|
'@welshman/net': 0.8.1
|
||||||
|
'@welshman/router': 0.8.1
|
||||||
|
'@welshman/signer': 0.8.1
|
||||||
|
'@welshman/util': 0.8.1
|
||||||
|
|
||||||
'@welshman/lib@0.6.8':
|
'@welshman/lib@0.8.1':
|
||||||
resolution: {integrity: sha512-1Wybkk8+vBdqv9nRhnNwIW9YVbhu3di07A2fUYWAQvldto49X26U8u7EV2CkUsz4iNC/799EBYuelcc6W9oZYw==}
|
resolution: {integrity: sha512-s4gRg4NXwDPiXgZVuAaZKS+rpnfYcFfqTqvw44hBTIWa1o12J4k7bqP7Oyi3r6Q901lW/6tvP7TInBWkdhm5Bw==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
'@welshman/net@0.6.8':
|
'@welshman/net@0.8.1':
|
||||||
resolution: {integrity: sha512-Lc1nIckdxW2ILiknowcbaKo+192QWQOBn6FLhFCEUZNyRNEOJYkAgDu4jKn7GXu91xpfJUFnq5KDvvq7hUeHqg==}
|
resolution: {integrity: sha512-ugPs8YT6B2WNVA9A443x3t0DAQyLnzj1nueDTAsHLbB9/4nAJDsXy7EKKcw4rMVXyZyaWZlBdOIR59hKhw1uew==}
|
||||||
|
|
||||||
'@welshman/router@0.6.8':
|
|
||||||
resolution: {integrity: sha512-+OJoD2Jm+yFiLc5FYb4/za66639CeIMYk7j4UAzR7n1z/gFQYMDviXqFYbcWHln3fgy4G7UF1HWBoU0sQD8EEw==}
|
|
||||||
|
|
||||||
'@welshman/signer@0.6.8':
|
|
||||||
resolution: {integrity: sha512-lt9Qq89TWyx/zSWgHkeVUX7MBCx86iBCkvzTdUIS7Ad6KfjjcYtsL9wAtfCc+TlvE87okOg97hAOvw18yIwfbw==}
|
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
'@welshman/lib': 0.8.1
|
||||||
|
'@welshman/util': 0.8.1
|
||||||
|
|
||||||
|
'@welshman/router@0.8.1':
|
||||||
|
resolution: {integrity: sha512-WOSVGa5utQ7lx99g6pUMibIz4PtlNiYgnEHsimVGGwngKo/bXb7XcSNXggdkTfD5+iHoaHFfil/27Jotfjxmeg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@welshman/lib': 0.8.1
|
||||||
|
'@welshman/net': 0.8.1
|
||||||
|
'@welshman/util': 0.8.1
|
||||||
|
|
||||||
|
'@welshman/signer@0.8.1':
|
||||||
|
resolution: {integrity: sha512-lJJWuMZYcgjmVaFNSxnxR1Zs6PcWOaF+MNjEzakLNs8t1P1zBnKGhrmQrzPMFbdy+NcQvfVlmJOd97jN+VZUQw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@noble/curves': ^1.9.7
|
||||||
|
'@noble/hashes': ^2.0.1
|
||||||
|
'@welshman/lib': 0.8.1
|
||||||
|
'@welshman/net': 0.8.1
|
||||||
|
'@welshman/util': 0.8.1
|
||||||
nostr-signer-capacitor-plugin: ~0.0.4
|
nostr-signer-capacitor-plugin: ~0.0.4
|
||||||
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@welshman/store@0.6.8':
|
'@welshman/store@0.8.1':
|
||||||
resolution: {integrity: sha512-s5s5+tdPyXB1m2vLn2wfo7nx+uNKWBdwCyomk+soWKWEY3LWvg4DAKgQ1gF5hyOcja+UIHOJY9hS3BBEo0DtDA==}
|
resolution: {integrity: sha512-l+4qU4dvQDY10ESlOhV/Orq3tMIUa2LV0UA8zbqOfF0ZCaar6SxSdS3QodCsVIpViIvO9XQkhuAyUT5R6p6hKw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@welshman/lib': 0.8.1
|
||||||
|
'@welshman/net': 0.8.1
|
||||||
|
'@welshman/util': 0.8.1
|
||||||
|
svelte: ^4.0.0 || ^5.0.0
|
||||||
|
|
||||||
'@welshman/util@0.6.8':
|
'@welshman/util@0.8.1':
|
||||||
resolution: {integrity: sha512-Q4x3Jm3yIk4zORYOscMuxyC7fJGyZFetE5U4PVYNrvgtSLCtULYKs1y6WkAra4FD7zfAa7lqzTlQq4uIZWzdkA==}
|
resolution: {integrity: sha512-eMXeZ6hGuZnCeliPKQqpADNBKBEiXCEZgHIVN3+9qRVx/RVO2YRfDGFde9MKt+PO1qspot91wlbjpgcJ2P9M8A==}
|
||||||
|
peerDependencies:
|
||||||
|
'@noble/curves': ^1.9.7
|
||||||
|
'@welshman/lib': 0.8.1
|
||||||
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@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==}
|
||||||
@@ -1758,10 +1758,6 @@ packages:
|
|||||||
add-stream@1.0.0:
|
add-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==}
|
resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==}
|
||||||
|
|
||||||
agent-base@6.0.2:
|
|
||||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
|
||||||
engines: {node: '>= 6.0.0'}
|
|
||||||
|
|
||||||
aggregate-error@3.1.0:
|
aggregate-error@3.1.0:
|
||||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2069,9 +2065,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
code-red@1.0.4:
|
|
||||||
resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
|
|
||||||
|
|
||||||
color-convert@1.9.3:
|
color-convert@1.9.3:
|
||||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
|
|
||||||
@@ -2586,9 +2579,6 @@ packages:
|
|||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
estree-walker@3.0.3:
|
|
||||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
|
||||||
|
|
||||||
esutils@2.0.3:
|
esutils@2.0.3:
|
||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2870,6 +2860,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hash-wasm@4.12.0:
|
||||||
|
resolution: {integrity: sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==}
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2885,10 +2878,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
https-proxy-agent@5.0.1:
|
|
||||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
|
||||||
engines: {node: '>= 6'}
|
|
||||||
|
|
||||||
husky@9.1.7:
|
husky@9.1.7:
|
||||||
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -3518,8 +3507,8 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
nostr-tools@2.17.0:
|
nostr-tools@2.19.4:
|
||||||
resolution: {integrity: sha512-lrvHM7cSaGhz7F0YuBvgHMoU2s8/KuThihDoOYk8w5gpVHTy0DeUCAgCN8uLGeuSl5MAWekJr9Dkfo5HClqO9w==}
|
resolution: {integrity: sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=5.0.0'
|
typescript: '>=5.0.0'
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
@@ -3659,9 +3648,6 @@ packages:
|
|||||||
pend@1.2.0:
|
pend@1.2.0:
|
||||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||||
|
|
||||||
periscopic@3.1.0:
|
|
||||||
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
|
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -3865,10 +3851,6 @@ packages:
|
|||||||
process-nextick-args@2.0.1:
|
process-nextick-args@2.0.1:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
|
||||||
progress@2.0.3:
|
|
||||||
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
|
||||||
engines: {node: '>=0.4.0'}
|
|
||||||
|
|
||||||
prompts@2.4.2:
|
prompts@2.4.2:
|
||||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -3931,9 +3913,6 @@ packages:
|
|||||||
prosemirror-view@1.41.3:
|
prosemirror-view@1.41.3:
|
||||||
resolution: {integrity: sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==}
|
resolution: {integrity: sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==}
|
||||||
|
|
||||||
proxy-from-env@1.1.0:
|
|
||||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
|
||||||
|
|
||||||
pump@3.0.3:
|
pump@3.0.3:
|
||||||
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
||||||
|
|
||||||
@@ -4381,10 +4360,6 @@ packages:
|
|||||||
svelte:
|
svelte:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
svelte@4.2.20:
|
|
||||||
resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==}
|
|
||||||
engines: {node: '>=16'}
|
|
||||||
|
|
||||||
svelte@5.39.12:
|
svelte@5.39.12:
|
||||||
resolution: {integrity: sha512-CEzwxFuEycokU8K8CE/OuwVbmei+ivu2HvBGYIdASfMa1hCRSNr4RRkzNSvbAvu6h+BOig2CsZTAEY+WKvwZpA==}
|
resolution: {integrity: sha512-CEzwxFuEycokU8K8CE/OuwVbmei+ivu2HvBGYIdASfMa1hCRSNr4RRkzNSvbAvu6h+BOig2CsZTAEY+WKvwZpA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4926,15 +4901,16 @@ packages:
|
|||||||
zimmerframe@1.1.4:
|
zimmerframe@1.1.4:
|
||||||
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
||||||
|
|
||||||
|
zod@3.25.76:
|
||||||
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
|
zod@4.3.5:
|
||||||
|
resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/gen-mapping': 0.3.13
|
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
|
||||||
|
|
||||||
'@antfu/utils@0.7.10': {}
|
'@antfu/utils@0.7.10': {}
|
||||||
|
|
||||||
'@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)':
|
'@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)':
|
||||||
@@ -5718,6 +5694,27 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@capacitor/core': 7.4.3
|
'@capacitor/core': 7.4.3
|
||||||
|
|
||||||
|
'@cmdcode/buff@2.2.5':
|
||||||
|
dependencies:
|
||||||
|
'@noble/hashes': 1.8.0
|
||||||
|
'@scure/base': 1.2.6
|
||||||
|
|
||||||
|
'@cmdcode/frost@1.1.3':
|
||||||
|
dependencies:
|
||||||
|
'@cmdcode/buff': 2.2.5
|
||||||
|
'@noble/curves': 1.9.7
|
||||||
|
'@noble/hashes': 1.8.0
|
||||||
|
|
||||||
|
'@cmdcode/nostr-p2p@2.0.11(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@cmdcode/buff': 2.2.5
|
||||||
|
'@noble/ciphers': 1.3.0
|
||||||
|
'@noble/curves': 1.9.7
|
||||||
|
nostr-tools: 2.19.4(typescript@5.9.3)
|
||||||
|
zod: 3.25.76
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
|
||||||
'@cspotcode/source-map-support@0.8.1':
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.9
|
'@jridgewell/trace-mapping': 0.3.9
|
||||||
@@ -5837,8 +5834,21 @@ snapshots:
|
|||||||
'@eslint/core': 0.16.0
|
'@eslint/core': 0.16.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
|
'@frostr/bifrost@1.0.7(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@cmdcode/buff': 2.2.5
|
||||||
|
'@cmdcode/frost': 1.1.3
|
||||||
|
'@cmdcode/nostr-p2p': 2.0.11(typescript@5.9.3)
|
||||||
|
'@noble/ciphers': 1.3.0
|
||||||
|
'@noble/curves': 1.9.7
|
||||||
|
zod: 3.25.76
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
|
||||||
'@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
|
||||||
@@ -6026,6 +6036,8 @@ snapshots:
|
|||||||
|
|
||||||
'@noble/ciphers@0.5.3': {}
|
'@noble/ciphers@0.5.3': {}
|
||||||
|
|
||||||
|
'@noble/ciphers@1.3.0': {}
|
||||||
|
|
||||||
'@noble/curves@1.1.0':
|
'@noble/curves@1.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 1.3.1
|
'@noble/hashes': 1.3.1
|
||||||
@@ -6044,6 +6056,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
|
||||||
@@ -6065,6 +6079,18 @@ snapshots:
|
|||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
|
'@pomade/core@0.0.12(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.1(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(nostr-tools@2.19.4(typescript@5.9.3)))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(nostr-tools@2.19.4(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
'@frostr/bifrost': 1.0.7(typescript@5.9.3)
|
||||||
|
'@noble/hashes': 2.0.1
|
||||||
|
'@welshman/lib': 0.8.1
|
||||||
|
'@welshman/net': 0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3)
|
||||||
|
'@welshman/signer': 0.8.1(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
|
'@welshman/util': 0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
|
hash-wasm: 4.12.0
|
||||||
|
nostr-tools: 2.19.4(typescript@5.9.3)
|
||||||
|
zod: 4.3.5
|
||||||
|
|
||||||
'@poppanator/sveltekit-svg@4.2.1(rollup@2.79.2)(svelte@5.39.12)(svgo@3.3.2)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0))':
|
'@poppanator/sveltekit-svg@4.2.1(rollup@2.79.2)(svelte@5.39.12)(svgo@3.3.2)(vite@5.4.20(@types/node@24.7.2)(terser@5.44.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
|
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
|
||||||
@@ -6212,78 +6238,6 @@ snapshots:
|
|||||||
'@noble/hashes': 1.3.2
|
'@noble/hashes': 1.3.2
|
||||||
'@scure/base': 1.1.1
|
'@scure/base': 1.1.1
|
||||||
|
|
||||||
'@sentry-internal/browser-utils@8.55.0':
|
|
||||||
dependencies:
|
|
||||||
'@sentry/core': 8.55.0
|
|
||||||
|
|
||||||
'@sentry-internal/feedback@8.55.0':
|
|
||||||
dependencies:
|
|
||||||
'@sentry/core': 8.55.0
|
|
||||||
|
|
||||||
'@sentry-internal/replay-canvas@8.55.0':
|
|
||||||
dependencies:
|
|
||||||
'@sentry-internal/replay': 8.55.0
|
|
||||||
'@sentry/core': 8.55.0
|
|
||||||
|
|
||||||
'@sentry-internal/replay@8.55.0':
|
|
||||||
dependencies:
|
|
||||||
'@sentry-internal/browser-utils': 8.55.0
|
|
||||||
'@sentry/core': 8.55.0
|
|
||||||
|
|
||||||
'@sentry/browser@8.55.0':
|
|
||||||
dependencies:
|
|
||||||
'@sentry-internal/browser-utils': 8.55.0
|
|
||||||
'@sentry-internal/feedback': 8.55.0
|
|
||||||
'@sentry-internal/replay': 8.55.0
|
|
||||||
'@sentry-internal/replay-canvas': 8.55.0
|
|
||||||
'@sentry/core': 8.55.0
|
|
||||||
|
|
||||||
'@sentry/cli-darwin@2.56.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@sentry/cli-linux-arm64@2.56.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@sentry/cli-linux-arm@2.56.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@sentry/cli-linux-i686@2.56.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@sentry/cli-linux-x64@2.56.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@sentry/cli-win32-arm64@2.56.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@sentry/cli-win32-i686@2.56.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@sentry/cli-win32-x64@2.56.1':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@sentry/cli@2.56.1':
|
|
||||||
dependencies:
|
|
||||||
https-proxy-agent: 5.0.1
|
|
||||||
node-fetch: 2.7.0
|
|
||||||
progress: 2.0.3
|
|
||||||
proxy-from-env: 1.1.0
|
|
||||||
which: 2.0.2
|
|
||||||
optionalDependencies:
|
|
||||||
'@sentry/cli-darwin': 2.56.1
|
|
||||||
'@sentry/cli-linux-arm': 2.56.1
|
|
||||||
'@sentry/cli-linux-arm64': 2.56.1
|
|
||||||
'@sentry/cli-linux-i686': 2.56.1
|
|
||||||
'@sentry/cli-linux-x64': 2.56.1
|
|
||||||
'@sentry/cli-win32-arm64': 2.56.1
|
|
||||||
'@sentry/cli-win32-i686': 2.56.1
|
|
||||||
'@sentry/cli-win32-x64': 2.56.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- encoding
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@sentry/core@8.55.0': {}
|
|
||||||
|
|
||||||
'@standard-schema/spec@1.0.0': {}
|
'@standard-schema/spec@1.0.0': {}
|
||||||
|
|
||||||
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
'@surma/rollup-plugin-off-main-thread@2.2.3':
|
||||||
@@ -6651,32 +6605,26 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@vite-pwa/assets-generator': 0.2.6
|
'@vite-pwa/assets-generator': 0.2.6
|
||||||
|
|
||||||
'@welshman/app@0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/app@0.8.1(7cc2d202a89b51b45c1ecb6d486f020b)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/throttle-debounce': 5.0.2
|
'@pomade/core': 0.0.12(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.1(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(nostr-tools@2.19.4(typescript@5.9.3)))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
'@welshman/feeds': 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/feeds': 0.8.1(c3072638adf29bdf3b0a110142b651da)
|
||||||
'@welshman/lib': 0.6.8
|
'@welshman/lib': 0.8.1
|
||||||
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/net': 0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/router': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/router': 0.8.1(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))
|
||||||
'@welshman/signer': 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/signer': 0.8.1(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
'@welshman/store': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/store': 0.8.1(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(svelte@5.39.12)
|
||||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
'@welshman/util': 0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
fuse.js: 7.1.0
|
fuse.js: 7.1.0
|
||||||
svelte: 4.2.20
|
svelte: 5.39.12
|
||||||
throttle-debounce: 5.0.2
|
throttle-debounce: 5.0.2
|
||||||
transitivePeerDependencies:
|
|
||||||
- nostr-signer-capacitor-plugin
|
|
||||||
- typescript
|
|
||||||
- ws
|
|
||||||
|
|
||||||
'@welshman/content@0.6.8(typescript@5.9.3)':
|
'@welshman/content@0.8.1(nostr-tools@2.19.4(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.19.4(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
|
||||||
- typescript
|
|
||||||
|
|
||||||
'@welshman/editor@0.6.8(@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.8.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))(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(linkifyjs@4.3.2)(nostr-tools@2.19.4(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)))':
|
||||||
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,10 +6639,10 @@ 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.8
|
'@welshman/lib': 0.8.1
|
||||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
'@welshman/util': 0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(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.19.4(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.19.4(typescript@5.9.3)
|
||||||
tippy.js: 6.3.7
|
tippy.js: 6.3.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@tiptap/extension-image'
|
- '@tiptap/extension-image'
|
||||||
@@ -6705,78 +6653,62 @@ snapshots:
|
|||||||
- prosemirror-state
|
- prosemirror-state
|
||||||
- prosemirror-view
|
- prosemirror-view
|
||||||
- tiptap-markdown
|
- tiptap-markdown
|
||||||
- typescript
|
|
||||||
|
|
||||||
'@welshman/feeds@0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/feeds@0.8.1(c3072638adf29bdf3b0a110142b651da)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.6.8
|
'@welshman/lib': 0.8.1
|
||||||
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/net': 0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/router': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/router': 0.8.1(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))
|
||||||
'@welshman/signer': 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/signer': 0.8.1(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
'@welshman/util': 0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
trava: 1.2.1
|
trava: 1.2.1
|
||||||
transitivePeerDependencies:
|
|
||||||
- nostr-signer-capacitor-plugin
|
|
||||||
- typescript
|
|
||||||
- ws
|
|
||||||
|
|
||||||
'@welshman/lib@0.6.8':
|
'@welshman/lib@0.8.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.8(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.6.8
|
'@welshman/lib': 0.8.1
|
||||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
'@welshman/util': 0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(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
|
|
||||||
- ws
|
- ws
|
||||||
|
|
||||||
'@welshman/router@0.6.8(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/router@0.8.1(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.6.8
|
'@welshman/lib': 0.8.1
|
||||||
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/net': 0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
'@welshman/util': 0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
transitivePeerDependencies:
|
|
||||||
- typescript
|
|
||||||
- ws
|
|
||||||
|
|
||||||
'@welshman/signer@0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/signer@0.8.1(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(nostr-tools@2.19.4(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/curves': 1.9.7
|
'@noble/curves': 1.9.7
|
||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 2.0.1
|
||||||
'@welshman/lib': 0.6.8
|
'@welshman/lib': 0.8.1
|
||||||
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/net': 0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
'@welshman/util': 0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(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.4(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
|
||||||
- typescript
|
|
||||||
- ws
|
|
||||||
|
|
||||||
'@welshman/store@0.6.8(typescript@5.9.3)(ws@8.18.3)':
|
'@welshman/store@0.8.1(@welshman/lib@0.8.1)(@welshman/net@0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(svelte@5.39.12)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.6.8
|
'@welshman/lib': 0.8.1
|
||||||
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
|
'@welshman/net': 0.8.1(@welshman/lib@0.8.1)(@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3)))(ws@8.18.3)
|
||||||
'@welshman/util': 0.6.8(typescript@5.9.3)
|
'@welshman/util': 0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3))
|
||||||
svelte: 4.2.20
|
svelte: 5.39.12
|
||||||
transitivePeerDependencies:
|
|
||||||
- typescript
|
|
||||||
- ws
|
|
||||||
|
|
||||||
'@welshman/util@0.6.8(typescript@5.9.3)':
|
'@welshman/util@0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(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.8
|
'@welshman/lib': 0.8.1
|
||||||
js-base64: 3.7.8
|
js-base64: 3.7.8
|
||||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
nostr-tools: 2.19.4(typescript@5.9.3)
|
||||||
nostr-wasm: 0.1.0
|
nostr-wasm: 0.1.0
|
||||||
transitivePeerDependencies:
|
|
||||||
- typescript
|
|
||||||
|
|
||||||
'@xml-tools/parser@1.0.11':
|
'@xml-tools/parser@1.0.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6803,12 +6735,6 @@ snapshots:
|
|||||||
|
|
||||||
add-stream@1.0.0: {}
|
add-stream@1.0.0: {}
|
||||||
|
|
||||||
agent-base@6.0.2:
|
|
||||||
dependencies:
|
|
||||||
debug: 4.4.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
aggregate-error@3.1.0:
|
aggregate-error@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
clean-stack: 2.2.0
|
clean-stack: 2.2.0
|
||||||
@@ -7121,14 +7047,6 @@ snapshots:
|
|||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
code-red@1.0.4:
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
|
||||||
'@types/estree': 1.0.8
|
|
||||||
acorn: 8.15.0
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
periscopic: 3.1.0
|
|
||||||
|
|
||||||
color-convert@1.9.3:
|
color-convert@1.9.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.3
|
color-name: 1.1.3
|
||||||
@@ -7770,10 +7688,6 @@ snapshots:
|
|||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
estree-walker@3.0.3:
|
|
||||||
dependencies:
|
|
||||||
'@types/estree': 1.0.8
|
|
||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
events-universal@1.0.1:
|
events-universal@1.0.1:
|
||||||
@@ -8077,6 +7991,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-symbols: 1.1.0
|
has-symbols: 1.1.0
|
||||||
|
|
||||||
|
hash-wasm@4.12.0: {}
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
@@ -8089,13 +8005,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lru-cache: 6.0.0
|
lru-cache: 6.0.0
|
||||||
|
|
||||||
https-proxy-agent@5.0.1:
|
|
||||||
dependencies:
|
|
||||||
agent-base: 6.0.2
|
|
||||||
debug: 4.4.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
husky@9.1.7: {}
|
husky@9.1.7: {}
|
||||||
|
|
||||||
ico-endec@0.1.6: {}
|
ico-endec@0.1.6: {}
|
||||||
@@ -8625,7 +8534,7 @@ snapshots:
|
|||||||
|
|
||||||
normalize-range@0.1.2: {}
|
normalize-range@0.1.2: {}
|
||||||
|
|
||||||
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.19.4(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))):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.26.3(@tiptap/pm@2.26.3)
|
'@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-image': 2.27.1(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))
|
||||||
@@ -8634,7 +8543,7 @@ snapshots:
|
|||||||
js-base64: 3.7.8
|
js-base64: 3.7.8
|
||||||
light-bolt11-decoder: 3.2.0
|
light-bolt11-decoder: 3.2.0
|
||||||
linkifyjs: 4.3.2
|
linkifyjs: 4.3.2
|
||||||
nostr-tools: 2.17.0(typescript@5.9.3)
|
nostr-tools: 2.19.4(typescript@5.9.3)
|
||||||
prosemirror-markdown: 1.13.2
|
prosemirror-markdown: 1.13.2
|
||||||
prosemirror-model: 1.25.3
|
prosemirror-model: 1.25.3
|
||||||
prosemirror-state: 1.4.3
|
prosemirror-state: 1.4.3
|
||||||
@@ -8657,7 +8566,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
nostr-tools@2.17.0(typescript@5.9.3):
|
nostr-tools@2.19.4(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/ciphers': 0.5.3
|
'@noble/ciphers': 0.5.3
|
||||||
'@noble/curves': 1.2.0
|
'@noble/curves': 1.2.0
|
||||||
@@ -8797,12 +8706,6 @@ snapshots:
|
|||||||
|
|
||||||
pend@1.2.0: {}
|
pend@1.2.0: {}
|
||||||
|
|
||||||
periscopic@3.1.0:
|
|
||||||
dependencies:
|
|
||||||
'@types/estree': 1.0.8
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
is-reference: 3.0.3
|
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
@@ -8917,8 +8820,6 @@ snapshots:
|
|||||||
|
|
||||||
process-nextick-args@2.0.1: {}
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
progress@2.0.3: {}
|
|
||||||
|
|
||||||
prompts@2.4.2:
|
prompts@2.4.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
kleur: 3.0.3
|
kleur: 3.0.3
|
||||||
@@ -9027,8 +8928,6 @@ snapshots:
|
|||||||
prosemirror-state: 1.4.3
|
prosemirror-state: 1.4.3
|
||||||
prosemirror-transform: 1.10.4
|
prosemirror-transform: 1.10.4
|
||||||
|
|
||||||
proxy-from-env@1.1.0: {}
|
|
||||||
|
|
||||||
pump@3.0.3:
|
pump@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.5
|
end-of-stream: 1.4.5
|
||||||
@@ -9584,23 +9483,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
svelte: 5.39.12
|
svelte: 5.39.12
|
||||||
|
|
||||||
svelte@4.2.20:
|
|
||||||
dependencies:
|
|
||||||
'@ampproject/remapping': 2.3.0
|
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
|
||||||
'@types/estree': 1.0.8
|
|
||||||
acorn: 8.15.0
|
|
||||||
aria-query: 5.3.2
|
|
||||||
axobject-query: 4.1.0
|
|
||||||
code-red: 1.0.4
|
|
||||||
css-tree: 2.3.1
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
is-reference: 3.0.3
|
|
||||||
locate-character: 3.0.0
|
|
||||||
magic-string: 0.30.19
|
|
||||||
periscopic: 3.1.0
|
|
||||||
|
|
||||||
svelte@5.39.12:
|
svelte@5.39.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
@@ -10270,3 +10152,7 @@ snapshots:
|
|||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zimmerframe@1.1.4: {}
|
zimmerframe@1.1.4: {}
|
||||||
|
|
||||||
|
zod@3.25.76: {}
|
||||||
|
|
||||||
|
zod@4.3.5: {}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
hash=$(find build -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum | awk '{print $1}')
|
|
||||||
|
|
||||||
sentry-cli \
|
|
||||||
--url https://glitchtip.coracle.social \
|
|
||||||
--auth-token $GLITCHTIP_AUTH_TOKEN \
|
|
||||||
--api-key $VITE_GLITCHTIP_API_KEY \
|
|
||||||
sourcemaps \
|
|
||||||
--org coracle \
|
|
||||||
--project flotilla \
|
|
||||||
--release $hash \
|
|
||||||
upload \
|
|
||||||
--url-prefix /_app/immutable/ \
|
|
||||||
build/_app/immutable
|
|
||||||
+15
-1
@@ -66,6 +66,10 @@
|
|||||||
--neutral-content: oklch(var(--nc));
|
--neutral-content: oklch(var(--nc));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile [data-tip]::before {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* safe area insets */
|
/* safe area insets */
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
@@ -392,6 +396,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 {
|
||||||
@@ -399,5 +413,5 @@ progress[value]::-webkit-progress-value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat__scroll-down {
|
.chat__scroll-down {
|
||||||
@apply fixed bottom-28 right-4 md:bottom-16;
|
@apply fixed bottom-28 right-4 z-feature md:bottom-16;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,32 +5,13 @@
|
|||||||
import Landing from "@app/components/Landing.svelte"
|
import Landing from "@app/components/Landing.svelte"
|
||||||
import Toast from "@app/components/Toast.svelte"
|
import Toast from "@app/components/Toast.svelte"
|
||||||
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
||||||
import EmailConfirm from "@app/components/EmailConfirm.svelte"
|
import {modals} from "@app/util/modal"
|
||||||
import PasswordReset from "@app/components/PasswordReset.svelte"
|
|
||||||
import {BURROW_URL} from "@app/core/state"
|
|
||||||
import {modals, pushModal} from "@app/util/modal"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Snippet
|
children: Snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
const {children}: Props = $props()
|
const {children}: Props = $props()
|
||||||
|
|
||||||
if (BURROW_URL && !$pubkey) {
|
|
||||||
if ($page.url.pathname === "/confirm-email") {
|
|
||||||
pushModal(EmailConfirm, {
|
|
||||||
email: $page.url.searchParams.get("email"),
|
|
||||||
confirm_token: $page.url.searchParams.get("confirm_token"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($page.url.pathname === "/reset-password") {
|
|
||||||
pushModal(PasswordReset, {
|
|
||||||
email: $page.url.searchParams.get("email"),
|
|
||||||
reset_token: $page.url.searchParams.get("reset_token"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen overflow-hidden">
|
<div class="flex h-screen overflow-hidden">
|
||||||
|
|||||||
@@ -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, uniq, 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"
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
|
|
||||||
const {...props}: Props = $props()
|
const {...props}: Props = $props()
|
||||||
|
|
||||||
const others = remove($pubkey!, props.pubkeys)
|
const others = uniq(remove($pubkey!, props.pubkeys))
|
||||||
const active = $derived($page.params.chat === props.id)
|
const active = $derived($page.params.chat === props.id)
|
||||||
const path = makeChatPath(props.pubkeys)
|
const path = makeChatPath(props.pubkeys)
|
||||||
|
|
||||||
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.",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {tryCatch, uniq} from "@welshman/lib"
|
import {tryCatch, uniq} from "@welshman/lib"
|
||||||
import {fromNostrURI} from "@welshman/util"
|
import {fromNostrURI} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -19,7 +18,7 @@
|
|||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onSubmit = () => goto(makeChatPath([...pubkeys, $pubkey!]))
|
const onSubmit = () => goto(makeChatPath(pubkeys))
|
||||||
|
|
||||||
const addPubkey = (pubkey: string) => {
|
const addPubkey = (pubkey: string) => {
|
||||||
pubkeys = uniq([...pubkeys, pubkey])
|
pubkeys = uniq([...pubkeys, pubkey])
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
{#if isBlock(i)}
|
{#if isBlock(i)}
|
||||||
<ContentLinkBlock value={parsed.value} {event} />
|
<ContentLinkBlock value={parsed.value} {event} />
|
||||||
{:else}
|
{:else}
|
||||||
<ContentLinkInline value={parsed.value} />
|
<ContentLinkInline value={parsed.value} {event} />
|
||||||
{/if}
|
{/if}
|
||||||
{:else if isProfile(parsed)}
|
{:else if isProfile(parsed)}
|
||||||
<ContentMention value={parsed.value} {url} />
|
<ContentMention value={parsed.value} {url} />
|
||||||
|
|||||||
@@ -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,10 +38,10 @@
|
|||||||
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 rounded-box object-contain object-center">
|
||||||
<track kind="captions" />
|
<track kind="captions" />
|
||||||
</video>
|
</video>
|
||||||
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
||||||
@@ -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,27 +1,39 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayUrl} from "@welshman/lib"
|
import {call, displayUrl} from "@welshman/lib"
|
||||||
import {preventDefault} from "@lib/html"
|
import {isRelayUrl} from "@welshman/util"
|
||||||
|
import {preventDefault, stopPropagation} 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, event} = $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]
|
||||||
|
|
||||||
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
return [url, true]
|
||||||
|
})
|
||||||
|
|
||||||
|
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
||||||
<!-- Use a real link so people can copy the href -->
|
<!-- Use a real link so people can copy the href -->
|
||||||
<a href={url} class="link-content whitespace-nowrap" onclick={preventDefault(expand)}>
|
<a
|
||||||
|
href={url}
|
||||||
|
class="link-content whitespace-nowrap"
|
||||||
|
onclick={stopPropagation(preventDefault(expand))}>
|
||||||
<Icon icon={LinkRound} size={3} class="inline-block" />
|
<Icon icon={LinkRound} size={3} class="inline-block" />
|
||||||
{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>
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
{:else if isCashu(parsed) || isInvoice(parsed)}
|
{:else if isCashu(parsed) || isInvoice(parsed)}
|
||||||
<ContentToken value={parsed.value} />
|
<ContentToken value={parsed.value} />
|
||||||
{:else if isLink(parsed)}
|
{:else if isLink(parsed)}
|
||||||
<ContentLinkInline value={parsed.value} />
|
<ContentLinkInline value={parsed.value} {event} />
|
||||||
{:else if isProfile(parsed)}
|
{:else if isProfile(parsed)}
|
||||||
<ContentMention value={parsed.value} {url} />
|
<ContentMention value={parsed.value} {url} />
|
||||||
{:else if isQuote(parsed)}
|
{:else if isQuote(parsed)}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {postJson, sleep} from "@welshman/lib"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
|
||||||
import {pushModal} from "@app/util/modal"
|
|
||||||
import {BURROW_URL} from "@app/core/state"
|
|
||||||
|
|
||||||
const {email, confirm_token} = $props()
|
|
||||||
|
|
||||||
const login = () => {
|
|
||||||
pushModal(LogInPassword, {email}, {path: "/"})
|
|
||||||
}
|
|
||||||
|
|
||||||
let error = $state("")
|
|
||||||
let loading = $state(true)
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
const [res] = await Promise.all([
|
|
||||||
postJson(BURROW_URL + "/user/confirm-email", {email, confirm_token}),
|
|
||||||
sleep(2000),
|
|
||||||
])
|
|
||||||
|
|
||||||
error = res.error
|
|
||||||
loading = false
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="column gap-4">
|
|
||||||
<h1 class="heading">
|
|
||||||
{#if loading}
|
|
||||||
Just a second...
|
|
||||||
{:else if error}
|
|
||||||
Oops!
|
|
||||||
{:else}
|
|
||||||
Success!
|
|
||||||
{/if}
|
|
||||||
</h1>
|
|
||||||
<p class="m-auto max-w-sm text-center">
|
|
||||||
<Spinner {loading}>
|
|
||||||
{#if loading}
|
|
||||||
Hang tight, we're checking your confirmation link.
|
|
||||||
{:else if error}
|
|
||||||
Looks like something went wrong. {error}
|
|
||||||
{:else}
|
|
||||||
You're all set - click below to log in.
|
|
||||||
{/if}
|
|
||||||
</Spinner>
|
|
||||||
</p>
|
|
||||||
<Button class="btn btn-primary" onclick={login} disabled={loading}>Continue to Login</Button>
|
|
||||||
</div>
|
|
||||||
@@ -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(() => {
|
||||||
@@ -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>
|
|
||||||
@@ -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))))
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
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 ProfileEject from "@app/components/ProfileEject.svelte"
|
import KeyRecoveryRequest from "@app/components/KeyRecoveryRequest.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/core/state"
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const startEject = () => pushModal(ProfileEject)
|
const startRecoveryRequest = () => pushModal(KeyRecoveryRequest)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-primary" onclick={startEject}>
|
<Button class="btn btn-primary" onclick={startRecoveryRequest}>
|
||||||
<Icon icon={CheckCircle} />
|
<Icon icon={CheckCircle} />
|
||||||
I want to hold my own keys
|
I want to hold my own keys
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {nsecEncode} from "nostr-tools/nip19"
|
||||||
|
import {encrypt} from "nostr-tools/nip49"
|
||||||
|
import {hexToBytes} from "@welshman/lib"
|
||||||
|
import {preventDefault, downloadText} from "@lib/html"
|
||||||
|
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||||
|
import ArrowDown from "@assets/icons/arrow-down.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secret: string
|
||||||
|
next: () => unknown
|
||||||
|
submitText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {secret, next, submitText = "Continue"}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const cleanupCopy = (copy: string) =>
|
||||||
|
copy
|
||||||
|
.replace(/\n\s*\n\s*/g, "NEWLINE")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.replace(/NEWLINE/g, "\n\n")
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
const downloadKey = () => {
|
||||||
|
const sharedCopy = `
|
||||||
|
Most online services keep track of users by giving them a username and password. This gives the
|
||||||
|
service total control over their users, allowing them to ban them at any time, or sell their activity.
|
||||||
|
|
||||||
|
On Nostr, you control your own identity and social data, through the magic of cryptography. The basic
|
||||||
|
idea is that you have a public key, which acts as your user ID, and a private key which allows you to
|
||||||
|
prove your identity.
|
||||||
|
|
||||||
|
It's very important to keep your private key secret because it grants permanent and complete access to your
|
||||||
|
account.
|
||||||
|
`
|
||||||
|
|
||||||
|
if (usePassword) {
|
||||||
|
if (password.length < 12) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Your password must be at least 12 characters long.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ncryptsec = encrypt(hexToBytes(secret), password)
|
||||||
|
const instructions = `
|
||||||
|
This file contains a backup of your Nostr secret key, downloaded from ${PLATFORM_NAME} and encrypted using
|
||||||
|
a password you chose when you signed up.
|
||||||
|
|
||||||
|
${sharedCopy}
|
||||||
|
|
||||||
|
Your encrypted private key is:
|
||||||
|
|
||||||
|
${ncryptsec}
|
||||||
|
|
||||||
|
To use it to log in to other Nostr apps, find a Nostr Signer app (https://nostrapps.com/#signers is a good
|
||||||
|
place to look), and import your key.
|
||||||
|
`
|
||||||
|
|
||||||
|
downloadText("Nostr Secret Key.txt", cleanupCopy(instructions))
|
||||||
|
} else {
|
||||||
|
const nsec = nsecEncode(hexToBytes(secret))
|
||||||
|
const instructions = `
|
||||||
|
This file contains a backup of your Nostr secret key, downloaded from ${PLATFORM_NAME}.
|
||||||
|
|
||||||
|
${sharedCopy}
|
||||||
|
|
||||||
|
Your private key is:
|
||||||
|
|
||||||
|
${nsec}
|
||||||
|
|
||||||
|
To use it to log in to other Nostr apps, find a Nostr Signer app (https://nostrapps.com/#signers is a good
|
||||||
|
place to look), and import your key.
|
||||||
|
`
|
||||||
|
|
||||||
|
downloadText("Nostr Secret Key.txt", cleanupCopy(instructions))
|
||||||
|
}
|
||||||
|
|
||||||
|
didDownload = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPasswordChange = () => {
|
||||||
|
didDownload = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleUsePassword = () => {
|
||||||
|
usePassword = !usePassword
|
||||||
|
didDownload = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = $state("")
|
||||||
|
let usePassword = $state(false)
|
||||||
|
let didDownload = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Your Keys are Ready!</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
A cryptographic key pair has two parts: your <strong>public key</strong> identifies your
|
||||||
|
account, while your <strong>private key</strong> acts sort of like a master password.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Securing your private key is very important, so make sure to take the time to save your key in a
|
||||||
|
secure place (like a password manager).
|
||||||
|
</p>
|
||||||
|
{#if usePassword}
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
Password*
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon={Key} />
|
||||||
|
<input bind:value={password} onchange={onPasswordChange} class="grow" type="password" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>Passwords should be at least 12 characters long. Write this down!</p>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
{/if}
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<Button class="btn {didDownload ? 'btn-neutral' : 'btn-primary'}" onclick={downloadKey}>
|
||||||
|
Download my key
|
||||||
|
<Icon icon={ArrowDown} />
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-link no-underline" onclick={toggleUsePassword}>
|
||||||
|
{#if usePassword}
|
||||||
|
Nevermind, I want to download the plain version
|
||||||
|
{:else}
|
||||||
|
I want to download an encrypted version
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button disabled={!didDownload} class="btn btn-primary" type="submit">
|
||||||
|
{submitText}
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Client} from "@pomade/core"
|
||||||
|
import {getPubkey} from "@welshman/util"
|
||||||
|
import type {SessionPomade} from "@welshman/app"
|
||||||
|
import {session} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import KeyDownload from "@app/components/KeyDownload.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
|
import {POMADE_SIGNERS} from "@app/core/state"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
peersByPrefix: Map<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
const {peersByPrefix}: Props = $props()
|
||||||
|
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
clientOptions: {secret, peers},
|
||||||
|
} = $session as SessionPomade
|
||||||
|
|
||||||
|
const confirmRecovery = async () => {
|
||||||
|
const otps = input
|
||||||
|
.split(/\n/)
|
||||||
|
.map(x => x.trim())
|
||||||
|
.filter(x => x.match(/^[0-9]{8}$/))
|
||||||
|
|
||||||
|
if (otps.length < 2) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Failed to recover, not enough valid recovery codes were provided.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = await Client.recoverWithChallenge(email, peersByPrefix, otps)
|
||||||
|
|
||||||
|
if (!request.ok) {
|
||||||
|
console.log(request.messages)
|
||||||
|
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to recover: ${request.messages[0]?.payload.message.toLowerCase()}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Client.selectRecovery(request.clientSecret, getPubkey(secret), peers)
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
console.log(result.messages)
|
||||||
|
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to recover: ${result.messages[0]?.payload.message.toLowerCase()}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pushModal(KeyDownload, {secret: result.userSecret, next: clearModals, submitText: "Done"})
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await confirmRecovery()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let input = $state("")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
Recover your Key
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
Take control over your cryptographic identity
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>Your recovery codes have been sent!</p>
|
||||||
|
<p>
|
||||||
|
For security reasons, you may receive three or more emails with recovery codes in them. Please
|
||||||
|
paste <strong>all</strong> recovery codes into the text box below, on separate lines.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
rows={POMADE_SIGNERS.length + 1}
|
||||||
|
class="textarea textarea-bordered leading-4"
|
||||||
|
bind:value={input}></textarea>
|
||||||
|
<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}>Confirm recovery</Spinner>
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Client} from "@pomade/core"
|
||||||
|
import type {SessionPomade} from "@welshman/app"
|
||||||
|
import {session} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import KeyRecoveryConfirm from "@app/components/KeyRecoveryConfirm.svelte"
|
||||||
|
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
clientOptions: {peers},
|
||||||
|
} = $session as SessionPomade
|
||||||
|
|
||||||
|
const requestRecovery = async () => {
|
||||||
|
const {peersByPrefix} = await Client.requestChallenge(email, peers)
|
||||||
|
|
||||||
|
pushModal(KeyRecoveryConfirm, {peersByPrefix})
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await requestRecovery()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
Recover your Key
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
Take control over your cryptographic identity
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
When you signed up, your Nostr secret key was split into multiple pieces and stored on separate
|
||||||
|
third-party servers to keep it safe.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you're ready to take control of your cryptographic identity, click below. We'll confirm your
|
||||||
|
email by sending you some recovery codes.
|
||||||
|
</p>
|
||||||
|
<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}>Request recovery</Spinner>
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -4,24 +4,28 @@
|
|||||||
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
|
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
|
||||||
import {addSession, type Session, makeNip07Session, makeNip55Session} from "@welshman/app"
|
import {addSession, type Session, makeNip07Session, makeNip55Session} from "@welshman/app"
|
||||||
import Widget from "@assets/icons/widget-2.svg?dataurl"
|
import Widget from "@assets/icons/widget-2.svg?dataurl"
|
||||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
|
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
|
||||||
import Compass from "@assets/icons/compass-big.svg?dataurl"
|
import Compass from "@assets/icons/compass-big.svg?dataurl"
|
||||||
|
import Key from "@assets/icons/key.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import SignUp from "@app/components/SignUp.svelte"
|
import SignUp from "@app/components/SignUp.svelte"
|
||||||
import InfoNostr from "@app/components/InfoNostr.svelte"
|
import InfoNostr from "@app/components/InfoNostr.svelte"
|
||||||
import LogInBunker from "@app/components/LogInBunker.svelte"
|
import LogInBunker from "@app/components/LogInBunker.svelte"
|
||||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
import LogInEmail from "@app/components/LogInEmail.svelte"
|
||||||
|
import LogInKey from "@app/components/LogInKey.svelte"
|
||||||
import {pushModal, clearModals} from "@app/util/modal"
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
import {PLATFORM_NAME, BURROW_URL} from "@app/core/state"
|
import {PLATFORM_NAME, POMADE_SIGNERS} from "@app/core/state"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {setChecked} from "@app/util/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
|
|
||||||
let signers: any[] = $state([])
|
let signers: any[] = $state([])
|
||||||
let loading: string | undefined = $state()
|
let loading: string | undefined = $state()
|
||||||
|
|
||||||
|
const hasPomade = POMADE_SIGNERS.length >= 3
|
||||||
|
|
||||||
const disabled = $derived(loading ? true : undefined)
|
const disabled = $derived(loading ? true : undefined)
|
||||||
|
|
||||||
const signUp = () => pushModal(SignUp)
|
const signUp = () => pushModal(SignUp)
|
||||||
@@ -72,10 +76,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginWithPassword = () => pushModal(LogInPassword)
|
const loginWithEmail = () => pushModal(LogInEmail)
|
||||||
|
|
||||||
const loginWithBunker = () => pushModal(LogInBunker)
|
const loginWithBunker = () => pushModal(LogInBunker)
|
||||||
|
|
||||||
|
const loginWithKey = () => pushModal(LogInKey)
|
||||||
|
|
||||||
const hasSigner = $derived(getNip07() || signers.length > 0)
|
const hasSigner = $derived(getNip07() || signers.length > 0)
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@@ -112,39 +118,37 @@
|
|||||||
Log in with {app.name}
|
Log in with {app.name}
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
{#if BURROW_URL && !hasSigner}
|
{#if hasPomade && !hasSigner}
|
||||||
<Button {disabled} onclick={loginWithPassword} class="btn btn-primary">
|
<Button {disabled} onclick={loginWithEmail} class="btn btn-primary">
|
||||||
{#if loading === "password"}
|
<Icon icon={Letter} />
|
||||||
<span class="loading loading-spinner mr-3"></span>
|
Log in with Email
|
||||||
{:else}
|
|
||||||
<Icon icon={Key} />
|
|
||||||
{/if}
|
|
||||||
Log in with Password
|
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button
|
<Button
|
||||||
onclick={loginWithBunker}
|
onclick={loginWithBunker}
|
||||||
{disabled}
|
{disabled}
|
||||||
class="btn {hasSigner || BURROW_URL ? 'btn-neutral' : 'btn-primary'}">
|
class="btn {hasSigner || hasPomade ? 'btn-neutral' : 'btn-primary'}">
|
||||||
<Icon icon={Cpu} />
|
<Icon icon={Cpu} />
|
||||||
Log in with Remote Signer
|
Log in with Remote Signer
|
||||||
</Button>
|
</Button>
|
||||||
{#if BURROW_URL && hasSigner}
|
{#if hasPomade && hasSigner}
|
||||||
<Button {disabled} onclick={loginWithPassword} class="btn">
|
<Button {disabled} onclick={loginWithEmail} class="btn">
|
||||||
{#if loading === "password"}
|
<Icon icon={Letter} />
|
||||||
<span class="loading loading-spinner mr-3"></span>
|
Log in with Email
|
||||||
{:else}
|
|
||||||
<Icon icon={Key} />
|
|
||||||
{/if}
|
|
||||||
Log in with Password
|
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !hasSigner || !BURROW_URL}
|
{#if !hasSigner}
|
||||||
|
<Button {disabled} onclick={loginWithKey} class="btn btn-neutral">
|
||||||
|
<Icon icon={Key} />
|
||||||
|
Log in with Key
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if !hasSigner || !hasPomade}
|
||||||
<Link
|
<Link
|
||||||
external
|
external
|
||||||
{disabled}
|
{disabled}
|
||||||
href="https://nostrapps.com#signers"
|
href="https://nostrapps.com#signers"
|
||||||
class="btn {hasSigner || BURROW_URL ? '' : 'btn-neutral'}">
|
class="btn {hasSigner || hasPomade ? '' : 'btn-neutral'}">
|
||||||
<Icon icon={Compass} />
|
<Icon icon={Compass} />
|
||||||
Browse Signer Apps
|
Browse Signer Apps
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Client} from "@pomade/core"
|
||||||
|
import {loginWithPomade} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
|
import Key from "@assets/icons/key.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import LogInOTP from "@app/components/LogInOTP.svelte"
|
||||||
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
|
import {setChecked} from "@app/util/notifications"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let {email = $bindable("")}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const loginWithOTP = () => pushModal(LogInOTP, {email})
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {ok, options, messages, clientSecret} = await Client.loginWithPassword(email, password)
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
console.error(messages)
|
||||||
|
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Sorry, we were unable to log you in.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [client, peers] = options[0]!
|
||||||
|
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
|
||||||
|
|
||||||
|
if (res.ok && clientOptions) {
|
||||||
|
loginWithPomade(clientOptions.group.group_pk.slice(2), email, clientOptions)
|
||||||
|
pushToast({message: "Successfully logged in!"})
|
||||||
|
setChecked("*")
|
||||||
|
clearModals()
|
||||||
|
} else {
|
||||||
|
console.error(res.messages)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Sorry, we were unable to log you in.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let password = $state("")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Log In</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Log in using your email and password</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Email*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon={Letter} />
|
||||||
|
<input bind:value={email} />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Password*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon={Key} />
|
||||||
|
<input type="password" bind:value={password} />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<p class="text-sm">
|
||||||
|
Forgot your password? <Button class="link" onclick={loginWithOTP}
|
||||||
|
>Log in with a one-time access code</Button
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading || !email || !password}>
|
||||||
|
<Spinner {loading}>Log in</Spinner>
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {bytesToHex} from "@welshman/lib"
|
||||||
|
import {loginWithNip01} from "@welshman/app"
|
||||||
|
import {decrypt} from "nostr-tools/nip49"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import {nsecDecode} from "@lib/util"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Key from "@assets/icons/key.svg?dataurl"
|
||||||
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {clearModals} from "@app/util/modal"
|
||||||
|
import {setChecked} from "@app/util/notifications"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let keyInput = $state("")
|
||||||
|
let password = $state("")
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const isHex = $derived(keyInput.match(/^[0-9a-f]{64}$/i))
|
||||||
|
|
||||||
|
const isNsec = $derived(keyInput.startsWith("nsec1"))
|
||||||
|
|
||||||
|
const isNcryptsec = $derived(keyInput.startsWith("ncryptsec1"))
|
||||||
|
|
||||||
|
const canSubmit = $derived(!loading && (isHex || isNsec || isNcryptsec))
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
let secret: string
|
||||||
|
|
||||||
|
if (isNcryptsec) {
|
||||||
|
secret = bytesToHex(decrypt(keyInput, password))
|
||||||
|
} else if (isNsec) {
|
||||||
|
secret = nsecDecode(keyInput)
|
||||||
|
} else if (isHex) {
|
||||||
|
secret = keyInput.toLowerCase()
|
||||||
|
} else {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Invalid key format. Please enter a hex key, nsec, or ncryptsec.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loginWithNip01(secret)
|
||||||
|
pushToast({message: "Successfully logged in!"})
|
||||||
|
setChecked("*")
|
||||||
|
clearModals()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: isNcryptsec
|
||||||
|
? "Failed to decrypt key. Please check your password."
|
||||||
|
: "Invalid key format. Please check your input.",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Log In with Key</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Enter your nostr private key to log in.</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Your Key*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon={Key} />
|
||||||
|
<input type="password" bind:value={keyInput} placeholder="nsec1..." />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
{#if isNcryptsec}
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Password*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon={Key} />
|
||||||
|
<input type="password" bind:value={password} placeholder="Your password" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
{/if}
|
||||||
|
<div class="card2 card2-sm bg-alt flex flex-col gap-2 text-sm">
|
||||||
|
<strong class="flex items-center gap-2">
|
||||||
|
<Icon icon={Danger} />
|
||||||
|
Please note!
|
||||||
|
</strong>
|
||||||
|
<p>
|
||||||
|
Logging in this way is not a best practice. For better security, please consider using a
|
||||||
|
<Link external href="https://nostrapps.com#signers" class="link">signer app</Link>
|
||||||
|
to keep your keys safe.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={!canSubmit}>
|
||||||
|
<Spinner {loading}>Log in</Spinner>
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
+28
-28
@@ -1,24 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {postJson, sleep} from "@welshman/lib"
|
import {Client} from "@pomade/core"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
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 Button from "@lib/components/Button.svelte"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.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 LogInPassword from "@app/components/LogInPassword.svelte"
|
import LogInOTPConfirm from "@app/components/LogInOTPConfirm.svelte"
|
||||||
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 {BURROW_URL} from "@app/core/state"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
email: string
|
email?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
let {email = $bindable()}: Props = $props()
|
let {email = $bindable("")}: Props = $props()
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
@@ -26,16 +26,15 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [res] = await Promise.all([
|
const {ok, peersByPrefix} = await Client.requestChallenge(email)
|
||||||
postJson(BURROW_URL + "/user/request-reset", {email}),
|
|
||||||
sleep(1000),
|
|
||||||
])
|
|
||||||
|
|
||||||
if (res.error) {
|
if (ok) {
|
||||||
pushToast({message: res.error, theme: "error"})
|
pushModal(LogInOTPConfirm, {email, peersByPrefix})
|
||||||
} else {
|
} else {
|
||||||
pushToast({message: `Password reset email has been sent!`})
|
pushToast({
|
||||||
pushModal(LogInPassword, {email}, {path: "/"})
|
theme: "error",
|
||||||
|
message: "Sorry, we were unable to request a login code.",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
@@ -48,30 +47,31 @@
|
|||||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<div>Reset your password</div>
|
<div>Log In</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Log in using a one-time login code</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<FieldInline disabled={loading}>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Email Address</p>
|
<p>Email*</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#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">
|
||||||
<Icon icon={UserRounded} />
|
<Icon icon={Letter} />
|
||||||
<input bind:value={email} class="grow" />
|
<input bind:value={email} />
|
||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet info()}
|
|
||||||
<p>You'll be sent an email with a password reset link.</p>
|
|
||||||
{/snippet}
|
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
<Button type="submit" class="btn btn-primary" disabled={loading || !email}>
|
||||||
<Spinner {loading}>Request password reset link</Spinner>
|
<Spinner {loading}>Log in</Spinner>
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</form>
|
</form>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Client} from "@pomade/core"
|
||||||
|
import {loginWithPomade} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {clearModals} from "@app/util/modal"
|
||||||
|
import {setChecked} from "@app/util/notifications"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {POMADE_SIGNERS} from "@app/core/state"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
email: string
|
||||||
|
peersByPrefix: Map<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
const {email, peersByPrefix}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
const otps = input
|
||||||
|
.split(/\n/)
|
||||||
|
.map(x => x.trim())
|
||||||
|
.filter(x => x.match(/^[0-9]{8}$/))
|
||||||
|
|
||||||
|
if (otps.length < 2) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Failed to recover, not enough valid recovery codes were provided.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {ok, options, messages, clientSecret} = await Client.loginWithChallenge(
|
||||||
|
email,
|
||||||
|
peersByPrefix,
|
||||||
|
otps,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
console.error(messages)
|
||||||
|
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Sorry, we were unable to log you in.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [client, peers] = options[0]!
|
||||||
|
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
|
||||||
|
|
||||||
|
if (res.ok && clientOptions) {
|
||||||
|
loginWithPomade(clientOptions.group.group_pk.slice(2), email, clientOptions)
|
||||||
|
pushToast({message: "Successfully logged in!"})
|
||||||
|
setChecked("*")
|
||||||
|
clearModals()
|
||||||
|
} else {
|
||||||
|
console.error(res.messages)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Sorry, we were unable to log you in.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = $state("")
|
||||||
|
let loading = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Log In</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Enter the login codes sent to your email</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>Your login codes have been sent!</p>
|
||||||
|
<p>
|
||||||
|
For security reasons, you may receive three or more emails with login codes in them. Please
|
||||||
|
paste <strong>all</strong> login codes into the text box below, on separate lines.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
rows={POMADE_SIGNERS.length + 1}
|
||||||
|
class="textarea textarea-bordered leading-4"
|
||||||
|
bind:value={input}></textarea>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Log In</Spinner>
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {onMount, onDestroy} from "svelte"
|
|
||||||
import {postJson, stripProtocol} from "@welshman/lib"
|
|
||||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
|
||||||
import {normalizeRelayUrl} from "@welshman/util"
|
|
||||||
import {addSession, makeNip46Session} from "@welshman/app"
|
|
||||||
import {preventDefault} from "@lib/html"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
|
||||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
|
||||||
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
|
||||||
import {clearModals, pushModal} from "@app/util/modal"
|
|
||||||
import {setChecked} from "@app/util/notifications"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
|
||||||
import {
|
|
||||||
NIP46_PERMS,
|
|
||||||
BURROW_URL,
|
|
||||||
PLATFORM_URL,
|
|
||||||
PLATFORM_NAME,
|
|
||||||
PLATFORM_LOGO,
|
|
||||||
} from "@app/core/state"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
email?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
let {email = $bindable("")}: Props = $props()
|
|
||||||
|
|
||||||
const clientSecret = makeSecret()
|
|
||||||
|
|
||||||
const startReset = () => pushModal(PasswordResetRequest, {email})
|
|
||||||
|
|
||||||
const abortController = new AbortController()
|
|
||||||
|
|
||||||
const relays = BURROW_URL.startsWith("http://")
|
|
||||||
? [normalizeRelayUrl("ws://" + stripProtocol(BURROW_URL))]
|
|
||||||
: [normalizeRelayUrl(BURROW_URL)]
|
|
||||||
|
|
||||||
const broker = new Nip46Broker({clientSecret, relays})
|
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
|
||||||
loading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await postJson(BURROW_URL + "/session", {email, password, nostrconnect: url})
|
|
||||||
|
|
||||||
if (res.error) {
|
|
||||||
pushToast({message: res.error, theme: "error"})
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
pushToast({message: "Something went wrong, please try again!", theme: "error"})
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = ""
|
|
||||||
let password = $state("")
|
|
||||||
let loading = $state(false)
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
url = await broker.makeNostrconnectUrl({
|
|
||||||
perms: NIP46_PERMS,
|
|
||||||
url: PLATFORM_URL,
|
|
||||||
name: PLATFORM_NAME,
|
|
||||||
image: PLATFORM_LOGO,
|
|
||||||
})
|
|
||||||
|
|
||||||
let response
|
|
||||||
try {
|
|
||||||
response = await broker.waitForNostrconnect(url, abortController.signal)
|
|
||||||
} catch (errorResponse: any) {
|
|
||||||
if (errorResponse?.error) {
|
|
||||||
pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: `Received error from signer: ${errorResponse.error}`,
|
|
||||||
})
|
|
||||||
} else if (errorResponse) {
|
|
||||||
console.error(errorResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
loading = true
|
|
||||||
|
|
||||||
const pubkey = await broker.getPublicKey()
|
|
||||||
const session = makeNip46Session(pubkey, clientSecret, response.event.pubkey, relays)
|
|
||||||
|
|
||||||
addSession({...session, email})
|
|
||||||
broker.cleanup()
|
|
||||||
setChecked("*")
|
|
||||||
clearModals()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
abortController.abort()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
|
||||||
<ModalHeader>
|
|
||||||
{#snippet title()}
|
|
||||||
<div>Log In</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<div>Log in using your email and password</div>
|
|
||||||
{/snippet}
|
|
||||||
</ModalHeader>
|
|
||||||
<FieldInline>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Email</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon={UserRounded} />
|
|
||||||
<input bind:value={email} />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</FieldInline>
|
|
||||||
<FieldInline>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Password</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon={Key} />
|
|
||||||
<input bind:value={password} type="password" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</FieldInline>
|
|
||||||
<p class="text-sm">
|
|
||||||
Your email and password only work to log in to {PLATFORM_NAME}. To use your key on other nostr
|
|
||||||
applications, visit your settings page. <Button class="link" onclick={startReset}
|
|
||||||
>Forgot your password?</Button>
|
|
||||||
</p>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
|
||||||
<Icon icon={AltArrowLeft} />
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading || !email || !password}>
|
|
||||||
<Spinner {loading}>Next</Spinner>
|
|
||||||
<Icon icon={AltArrowRight} />
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</form>
|
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
await logout()
|
await logout()
|
||||||
window.location.href = "/"
|
window.location.href = "/"
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -23,16 +23,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($notifications.size > notificationCount) {
|
||||||
|
playSound()
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationCount = $notifications.size
|
||||||
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
audioElement.load()
|
audioElement.load()
|
||||||
|
|
||||||
notifications.subscribe(notifications => {
|
|
||||||
if (notifications.size > notificationCount) {
|
|
||||||
playSound()
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationCount = notifications.size
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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))))
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {postJson, sleep} from "@welshman/lib"
|
|
||||||
import {preventDefault} from "@lib/html"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
|
||||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
|
||||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
|
||||||
import {pushModal} from "@app/util/modal"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
|
||||||
import {BURROW_URL} from "@app/core/state"
|
|
||||||
|
|
||||||
const {email, reset_token} = $props()
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
|
||||||
loading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [res] = await Promise.all([
|
|
||||||
postJson(BURROW_URL + "/user/confirm-reset", {email, password, reset_token}),
|
|
||||||
sleep(1000),
|
|
||||||
])
|
|
||||||
|
|
||||||
if (res.error) {
|
|
||||||
pushToast({message: res.error, theme: "error"})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "Password reset successfully!"})
|
|
||||||
pushModal(LogInPassword, {email}, {path: "/"})
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let loading = $state(false)
|
|
||||||
let password = $state("")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
|
||||||
<ModalHeader>
|
|
||||||
{#snippet title()}
|
|
||||||
<div>Reset your password</div>
|
|
||||||
{/snippet}
|
|
||||||
</ModalHeader>
|
|
||||||
<FieldInline disabled={loading}>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Email Address</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon={UserRounded} />
|
|
||||||
<input readonly value={email} class="grow" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</FieldInline>
|
|
||||||
<FieldInline disabled={loading}>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>New Password</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon={Key} />
|
|
||||||
<input bind:value={password} class="grow" type="password" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</FieldInline>
|
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
|
||||||
<Spinner {loading}>Reset password</Spinner>
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Client} from "@pomade/core"
|
||||||
|
import type {SessionItem} from "@pomade/core"
|
||||||
|
import {session, isPomadeSession} from "@welshman/app"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
|
||||||
|
type SessionWithPeers = SessionItem & {peers: string[]}
|
||||||
|
|
||||||
|
let sessions = $state<SessionWithPeers[]>([])
|
||||||
|
let deletingSession = $state<string | null>(null)
|
||||||
|
|
||||||
|
const loadSessions = async () => {
|
||||||
|
if (!isPomadeSession($session)) return
|
||||||
|
|
||||||
|
const client = new Client($session.clientOptions)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await client.listSessions()
|
||||||
|
const pubkey = await client.getPubkey()
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
// Group sessions by client pubkey and collect peers
|
||||||
|
const sessionMap = new Map<string, SessionWithPeers>()
|
||||||
|
|
||||||
|
for (const message of result.messages) {
|
||||||
|
if (!message?.payload.items) continue
|
||||||
|
|
||||||
|
const peer = message.event.pubkey
|
||||||
|
|
||||||
|
for (const item of message.payload.items) {
|
||||||
|
const existing = sessionMap.get(item.client)
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing.peers.push(peer)
|
||||||
|
} else if (item.client !== pubkey) {
|
||||||
|
sessionMap.set(item.client, {...item, peers: [peer]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions = Array.from(sessionMap.values())
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
client.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSession = async (sessionItem: SessionWithPeers) => {
|
||||||
|
if (!isPomadeSession($session)) return
|
||||||
|
|
||||||
|
deletingSession = sessionItem.client
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new Client($session.clientOptions)
|
||||||
|
const result = await client.deleteSession(sessionItem.client, sessionItem.peers)
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
pushToast({
|
||||||
|
message: "Session deleted successfully",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove from local list
|
||||||
|
sessions = sessions.filter(s => s.client !== sessionItem.client)
|
||||||
|
} else {
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Failed to delete session",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
client.stop()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Failed to delete session",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
deletingSession = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (timestamp: number) => {
|
||||||
|
const date = new Date(timestamp * 1000)
|
||||||
|
return date.toLocaleDateString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadSessions()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if sessions.length > 0}
|
||||||
|
<div class="flex flex-col gap-4 border-t border-solid border-base-100 pt-4">
|
||||||
|
<strong>Other Sessions</strong>
|
||||||
|
{#each sessions as sessionItem (sessionItem.client)}
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<span>{sessionItem.client.slice(0, 8)}</span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<div class="badge badge-neutral">
|
||||||
|
Created {formatDate(sessionItem.created_at)}
|
||||||
|
</div>
|
||||||
|
<div class="badge badge-neutral">
|
||||||
|
Last active: {formatDate(sessionItem.last_activity)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
class="btn btn-error btn-sm"
|
||||||
|
disabled={deletingSession !== null}
|
||||||
|
onclick={() => deleteSession(sessionItem)}>
|
||||||
|
{#if deletingSession === sessionItem.client}
|
||||||
|
<span class="loading loading-spinner"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon={TrashBin2} />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -62,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">
|
||||||
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" />
|
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" size={10} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#each primarySpaceUrls as url (url)}
|
{#each primarySpaceUrls as url (url)}
|
||||||
@@ -74,11 +74,11 @@
|
|||||||
class="tooltip-right"
|
class="tooltip-right"
|
||||||
onclick={showOtherSpacesMenu}
|
onclick={showOtherSpacesMenu}
|
||||||
notification={otherSpaceNotifications}>
|
notification={otherSpaceNotifications}>
|
||||||
<ImageIcon alt="Other Spaces" src={Widget} />
|
<ImageIcon alt="Other Spaces" src={Widget} size={8} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
|
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
|
||||||
<ImageIcon alt="Add a Space" src={Compass} size={7} />
|
<ImageIcon alt="Add a Space" src={Compass} size={8} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -91,17 +91,21 @@
|
|||||||
href="/settings/profile"
|
href="/settings/profile"
|
||||||
prefix="/settings"
|
prefix="/settings"
|
||||||
class="tooltip-right">
|
class="tooltip-right">
|
||||||
<ImageIcon alt="Settings" src={$userProfile?.picture || UserRounded} class="rounded-full" />
|
{#if $userProfile?.picture}
|
||||||
|
<ImageIcon alt="Settings" src={$userProfile?.picture} class="rounded-full" size={10} />
|
||||||
|
{:else}
|
||||||
|
<ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} />
|
||||||
|
{/if}
|
||||||
</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")}>
|
||||||
<ImageIcon alt="Messages" src={Letter} size={7} />
|
<ImageIcon alt="Messages" src={Letter} size={8} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
|
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
|
||||||
<ImageIcon alt="Search" src={Magnifier} size={7} />
|
<ImageIcon alt="Search" src={Magnifier} size={8} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,32 +114,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">
|
||||||
<ImageIcon alt="Home" src={HomeSmile} size={7} />
|
<ImageIcon alt="Home" src={HomeSmile} size={8} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
title="Messages"
|
title="Messages"
|
||||||
onclick={openChat}
|
onclick={openChat}
|
||||||
notification={$notifications.has("/chat")}>
|
notification={$notifications.has("/chat")}>
|
||||||
<ImageIcon alt="Messages" src={Letter} size={7} />
|
<ImageIcon alt="Messages" src={Letter} size={8} />
|
||||||
</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}>
|
||||||
<ImageIcon alt="Spaces" src={SettingsMinimalistic} size={7} />
|
<ImageIcon alt="Spaces" src={SettingsMinimalistic} size={8} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
|
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
|
||||||
<ImageIcon
|
{#if $userProfile?.picture}
|
||||||
alt="Settings"
|
<ImageIcon alt="Settings" src={$userProfile?.picture} size={10} class="rounded-full" />
|
||||||
src={$userProfile?.picture || Settings}
|
{:else}
|
||||||
size={7}
|
<ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" />
|
||||||
class="rounded-full" />
|
{/if}
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
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>
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -19,6 +19,6 @@
|
|||||||
|
|
||||||
<ImageIcon
|
<ImageIcon
|
||||||
{size}
|
{size}
|
||||||
|
alt=""
|
||||||
class={cx(props.class, "rounded-full")}
|
class={cx(props.class, "rounded-full")}
|
||||||
src={$profile?.picture || UserRounded}
|
src={$profile?.picture || UserRounded} />
|
||||||
alt="Profile picture" />
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {getProfile, loadProfile} from "@welshman/app"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -7,11 +8,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {pubkeys, size = 7}: Props = $props()
|
const {pubkeys, size = 7}: Props = $props()
|
||||||
|
|
||||||
|
for (const pubkey of pubkeys) {
|
||||||
|
loadProfile(pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const visiblePubkeys = $derived.by(() => {
|
||||||
|
const filtered = pubkeys.filter(pubkey => getProfile(pubkey)?.picture)
|
||||||
|
|
||||||
|
return filtered.length > 0 ? filtered : pubkeys.slice(0, 1)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex pr-3">
|
<div class="flex pr-3">
|
||||||
{#each pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
|
{#each visiblePubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
|
||||||
<div class="z-feature -mr-3 inline-block">
|
<div
|
||||||
|
class="z-feature -mr-3 inline-block flex h-8 w-8 items-center justify-center rounded-full bg-base-100">
|
||||||
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {size} />
|
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {size} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -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 ImageIcon from "@lib/components/ImageIcon.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">
|
||||||
<ImageIcon alt="Open in Coracle" 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">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import {clearModals} from "@app/util/modal"
|
import {clearModals} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {PROTECTED} from "@app/core/state"
|
import {PROTECTED} from "@app/core/state"
|
||||||
import {updateProfile} from "../core/commands"
|
import {updateProfile} from "@app/core/commands"
|
||||||
|
|
||||||
const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
|
const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
|
||||||
const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || [])
|
const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || [])
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {postJson} from "@welshman/lib"
|
|
||||||
import {session} from "@welshman/app"
|
|
||||||
import {slideAndFade} from "@lib/transition"
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
|
||||||
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import Field from "@lib/components/Field.svelte"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
|
||||||
import {PLATFORM_NAME, BURROW_URL} from "@app/core/state"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
|
||||||
import {logout} from "@app/core/commands"
|
|
||||||
|
|
||||||
const email = $session?.email
|
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
|
|
||||||
const confirm = async () => {
|
|
||||||
loading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = {email, password, eject: true}
|
|
||||||
const res = await postJson(BURROW_URL + "/user", payload, {method: "delete"})
|
|
||||||
|
|
||||||
if (res.error) {
|
|
||||||
return pushToast({message: res.error, theme: "error"})
|
|
||||||
}
|
|
||||||
|
|
||||||
success = true
|
|
||||||
pushToast({message: "Success! Please check your inbox and continue when you're ready."})
|
|
||||||
|
|
||||||
await logout()
|
|
||||||
} finally {
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reload = () => {
|
|
||||||
loading = true
|
|
||||||
window.location.href = "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
let password = $state("")
|
|
||||||
let success = $state(false)
|
|
||||||
let loading = $state(false)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="column gap-4">
|
|
||||||
<ModalHeader>
|
|
||||||
{#snippet title()}
|
|
||||||
<div>Export your keys</div>
|
|
||||||
{/snippet}
|
|
||||||
</ModalHeader>
|
|
||||||
<p>Here's what the process looks like:</p>
|
|
||||||
<ul class="flex list-inside list-decimal flex-col gap-4">
|
|
||||||
<li>When you're ready, enter your account password below to continue.</li>
|
|
||||||
<li>
|
|
||||||
{PLATFORM_NAME} will send an email to "{email}" with your encrypted private key in it.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Store your "ncryptsec" in a password manager like
|
|
||||||
<Link class="link" external href="https://bitwarden.com/">Bitwarden</Link>. This is the key to
|
|
||||||
your social identity; keep it safe and secret.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Choose a <Link class="link" href="https://nostrapps.com/#signers">signer app</Link> and import
|
|
||||||
your private key into it. Don't forget your account password; you'll need it to decrypt your key.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
Once you export your key, you'll be <strong>logged out</strong> and won't be able to log in using
|
|
||||||
your email and password any more. Going forward, you'll need to use your signer app instead.
|
|
||||||
</p>
|
|
||||||
{#if !success}
|
|
||||||
<div out:slideAndFade>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>To confirm, please enter your password below:</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon={Key} />
|
|
||||||
<input type="password" disabled={loading} bind:value={password} class="grow" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<ModalFooter>
|
|
||||||
<Button class="btn btn-link" disabled={loading || success} onclick={back}>
|
|
||||||
<Icon icon={AltArrowLeft} />
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
{#if success}
|
|
||||||
<Button class="btn btn-primary" disabled={loading} onclick={reload}>
|
|
||||||
<Icon icon={CheckCircle} />
|
|
||||||
<Spinner {loading}>Refresh the page</Spinner>
|
|
||||||
</Button>
|
|
||||||
{:else}
|
|
||||||
<Button class="btn btn-error" disabled={loading} onclick={confirm}>
|
|
||||||
<Icon icon={CheckCircle} />
|
|
||||||
<Spinner {loading}>I understand, send me my private key</Spinner>
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</ModalFooter>
|
|
||||||
</div>
|
|
||||||
@@ -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,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
import {type Instance} from "tippy.js"
|
import {type Instance} from "tippy.js"
|
||||||
@@ -35,6 +36,26 @@
|
|||||||
value = remove(pubkey, value)
|
value = remove(pubkey, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onInput = (e: any) => {
|
||||||
|
if (e.target.value.match(/^[a-f0-9]{64}$/)) {
|
||||||
|
selectPubkey(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {type, data} = nip19.decode(e.target.value) as any
|
||||||
|
|
||||||
|
if (type === "npub") {
|
||||||
|
selectPubkey(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "nprofile") {
|
||||||
|
selectPubkey(data.pubkey)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onKeyDown = (e: Event) => {
|
const onKeyDown = (e: Event) => {
|
||||||
if (instance.onKeyDown(e)) {
|
if (instance.onKeyDown(e)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -80,6 +101,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Search for profiles..."
|
placeholder="Search for profiles..."
|
||||||
bind:value={$term}
|
bind:value={$term}
|
||||||
|
oninput={onInput}
|
||||||
onkeydown={onKeyDown} />
|
onkeydown={onKeyDown} />
|
||||||
</label>
|
</label>
|
||||||
<Tippy
|
<Tippy
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import RelayIcon from "@app/components/RelayIcon.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>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
const canvasRect = canvas.getBoundingClientRect()
|
const canvasRect = canvas.getBoundingClientRect()
|
||||||
|
|
||||||
scale = wrapperRect.width / (canvasRect.width * 10)
|
scale = wrapperRect.width / (canvasRect.width * 10)
|
||||||
height = canvasRect.width * 10 * scale
|
height = canvasRect.height * 10 * scale
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
const relay = deriveRelay(url)
|
const relay = deriveRelay(url)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ImageIcon
|
{#if $relay?.icon}
|
||||||
{size}
|
<ImageIcon {size} alt="" src={$relay?.icon} class={props.class} />
|
||||||
src={$relay?.icon || RemoteControllerMinimalistic}
|
{:else}
|
||||||
alt="Relay image"
|
<ImageIcon size={size - 2} alt="" src={RemoteControllerMinimalistic} class={props.class} />
|
||||||
class={props.class} />
|
{/if}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
import Confirm from "@lib/components/Confirm.svelte"
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import ProfileList from "@app/components/ProfileList.svelte"
|
import RoomMembers from "@app/components/RoomMembers.svelte"
|
||||||
import RoomEdit from "@app/components/RoomEdit.svelte"
|
import RoomEdit from "@app/components/RoomEdit.svelte"
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
import RoomImage from "@app/components/RoomImage.svelte"
|
import RoomImage from "@app/components/RoomImage.svelte"
|
||||||
@@ -67,12 +67,7 @@
|
|||||||
|
|
||||||
const leave = () => handleLoading(leaveRoom)
|
const leave = () => handleLoading(leaveRoom)
|
||||||
|
|
||||||
const showMembers = () =>
|
const showMembers = () => pushModal(RoomMembers, {url, h})
|
||||||
pushModal(ProfileList, {
|
|
||||||
title: "Members",
|
|
||||||
subtitle: `of ${$room?.name || h}`,
|
|
||||||
pubkeys: $members,
|
|
||||||
})
|
|
||||||
|
|
||||||
const startDelete = () =>
|
const startDelete = () =>
|
||||||
pushModal(Confirm, {
|
pushModal(Confirm, {
|
||||||
@@ -139,11 +134,12 @@
|
|||||||
<p>{$room.about}</p>
|
<p>{$room.about}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $members.length > 0}
|
{#if $members.length > 0}
|
||||||
<div class="card2 card2-sm bg-alt flex gap-4">
|
<div class="card2 card2-sm bg-alt flex items-center justify-between gap-4">
|
||||||
<span>Members:</span>
|
<div class="flex items-center gap-4">
|
||||||
<Button onclick={showMembers}>
|
<span>Members:</span>
|
||||||
<ProfileCircles pubkeys={$members} />
|
<ProfileCircles pubkeys={$members} />
|
||||||
</Button>
|
</div>
|
||||||
|
<Button class="btn btn-neutral btn-sm" onclick={showMembers}>View All</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
@@ -109,32 +112,25 @@
|
|||||||
<p>Icon</p>
|
<p>Icon</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-grow items-center justify-between gap-4">
|
||||||
<div class="flex items-center gap-4">
|
{#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="" class="rounded-lg" />
|
||||||
<ImageIcon src={imagePreview} alt="Room icon preview" />
|
|
||||||
</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>
|
|
||||||
{: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>
|
||||||
|
{: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-xs">
|
||||||
|
<Icon icon={StickerSmileSquare} size={4} />
|
||||||
|
<span class="hidden sm:inline">Select</span>
|
||||||
|
</IconPickerButton>
|
||||||
|
<label class="btn btn-neutral btn-xs cursor-pointer">
|
||||||
|
<Icon icon={UploadMinimalistic} size={4} />
|
||||||
|
<span class="hidden sm:inline">Upload</span>
|
||||||
|
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -146,9 +142,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}
|
||||||
@@ -166,41 +160,22 @@
|
|||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
<FieldInline>
|
<strong class="md:hidden">Permissions</strong>
|
||||||
{#snippet label()}
|
<div class="flex items-center gap-2">
|
||||||
<strong>Restricted</strong>
|
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
|
||||||
{/snippet}
|
<span class="text-sm opacity-75">Only allow members to send messages</span>
|
||||||
{#snippet input()}
|
</div>
|
||||||
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-sm opacity-75">Only allow members to send messages</span>
|
<input type="checkbox" class="checkbox" bind:checked={values.isPrivate} />
|
||||||
{/snippet}
|
<span class="text-sm opacity-75">Only allow members to read messages</span>
|
||||||
</FieldInline>
|
</div>
|
||||||
<FieldInline>
|
<div class="flex items-center gap-2">
|
||||||
{#snippet label()}
|
<input type="checkbox" class="checkbox" bind:checked={values.isHidden} />
|
||||||
<strong>Private</strong>
|
<span class="text-sm opacity-75">Hide this group from non-members</span>
|
||||||
{/snippet}
|
</div>
|
||||||
{#snippet input()}
|
<div class="flex items-center gap-2">
|
||||||
<input type="checkbox" class="checkbox" bind:checked={values.isPrivate} />
|
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
|
||||||
<span class="text-sm opacity-75">Only allow members to read messages</span>
|
<span class="text-sm opacity-75">Ignore requests to join</span>
|
||||||
{/snippet}
|
</div>
|
||||||
</FieldInline>
|
|
||||||
<FieldInline>
|
|
||||||
{#snippet label()}
|
|
||||||
<strong>Hidden</strong>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<input type="checkbox" class="checkbox" bind:checked={values.isHidden} />
|
|
||||||
<span class="text-sm opacity-75">Hide this group from non-members</span>
|
|
||||||
{/snippet}
|
|
||||||
</FieldInline>
|
|
||||||
<FieldInline>
|
|
||||||
{#snippet label()}
|
|
||||||
<strong>Closed</strong>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
|
|
||||||
<span class="text-sm opacity-75">Ignore requests to join</span>
|
|
||||||
{/snippet}
|
|
||||||
</FieldInline>
|
|
||||||
{@render footer({loading})}
|
{@render footer({loading})}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $room.picture}
|
{#if $room.picture}
|
||||||
<ImageIcon src={$room.picture} {size} alt="Room icon" />
|
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon={Hashtag} {size} />
|
<Icon icon={Hashtag} {size} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -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,6 +39,26 @@
|
|||||||
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-md">
|
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md">
|
||||||
@@ -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,95 +1,111 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {postJson} from "@welshman/lib"
|
import type {ClientOptions} from "@pomade/core"
|
||||||
import {preventDefault} from "@lib/html"
|
import type {Profile} from "@welshman/util"
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
import {
|
||||||
|
makeProfile,
|
||||||
|
makeSecret,
|
||||||
|
getPubkey,
|
||||||
|
RELAYS,
|
||||||
|
MESSAGING_RELAYS,
|
||||||
|
makeEvent,
|
||||||
|
} from "@welshman/util"
|
||||||
|
import {loginWithNip01, loginWithPomade, publishThunk} from "@welshman/app"
|
||||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
|
import {getKey, setKey} from "@lib/implicit"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import LogIn from "@app/components/LogIn.svelte"
|
import LogIn from "@app/components/LogIn.svelte"
|
||||||
import InfoNostr from "@app/components/InfoNostr.svelte"
|
import InfoNostr from "@app/components/InfoNostr.svelte"
|
||||||
|
import SignUpKey from "@app/components/SignUpKey.svelte"
|
||||||
|
import SignUpEmail from "@app/components/SignUpEmail.svelte"
|
||||||
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
||||||
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
import SignUpComplete from "@app/components/SignUpComplete.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {BURROW_URL, PLATFORM_NAME} from "@app/core/state"
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {initProfile} from "@app/core/commands"
|
||||||
|
import {
|
||||||
|
POMADE_SIGNERS,
|
||||||
|
PLATFORM_NAME,
|
||||||
|
INDEXER_RELAYS,
|
||||||
|
DEFAULT_RELAYS,
|
||||||
|
DEFAULT_MESSAGING_RELAYS,
|
||||||
|
} from "@app/core/state"
|
||||||
|
|
||||||
|
setKey("signup.email", "")
|
||||||
|
setKey("signup.secret", makeSecret())
|
||||||
|
setKey("signup.profile", makeProfile())
|
||||||
|
setKey("signup.clientOptions", undefined)
|
||||||
|
|
||||||
|
const hasPomade = POMADE_SIGNERS.length >= 3
|
||||||
|
|
||||||
const login = () => pushModal(LogIn)
|
const login = () => pushModal(LogIn)
|
||||||
|
|
||||||
const signupPassword = async () => {
|
const completeSignup = () => {
|
||||||
loading = true
|
// Add default outbox/inbox relays
|
||||||
|
publishThunk({
|
||||||
|
event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}),
|
||||||
|
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS],
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
// Add default messaging relays
|
||||||
const res = await postJson(BURROW_URL + "/user", {email, password})
|
publishThunk({
|
||||||
|
event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}),
|
||||||
|
relays: DEFAULT_RELAYS,
|
||||||
|
})
|
||||||
|
|
||||||
if (res.error) {
|
// Save the user's profile
|
||||||
pushToast({message: res.error, theme: "error"})
|
initProfile(getKey<Profile>("signup.profile")!)
|
||||||
} else {
|
|
||||||
pushModal(SignUpSuccess, {email}, {replaceState: true})
|
// Don't show any notifications for old content
|
||||||
}
|
setChecked("*")
|
||||||
} finally {
|
|
||||||
loading = false
|
// Go to the dashboard
|
||||||
}
|
clearModals()
|
||||||
}
|
}
|
||||||
|
|
||||||
const usePassword = () => {
|
const flows = {
|
||||||
if (BURROW_URL) {
|
email: {
|
||||||
signupPassword()
|
start: () => pushModal(SignUpEmail, {next: flows.email.profile}),
|
||||||
}
|
profile: () => pushModal(SignUpProfile, {next: flows.email.complete}),
|
||||||
|
complete: () => pushModal(SignUpComplete, {next: flows.email.finalize}),
|
||||||
|
finalize: () => {
|
||||||
|
const email = getKey<string>("signup.email")!
|
||||||
|
const secret = getKey<string>("signup.secret")!
|
||||||
|
const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
|
||||||
|
|
||||||
|
loginWithPomade(getPubkey(secret), email, clientOptions)
|
||||||
|
completeSignup()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nostr: {
|
||||||
|
start: () => pushModal(SignUpProfile, {next: flows.nostr.key}),
|
||||||
|
key: () => pushModal(SignUpKey, {next: flows.nostr.complete}),
|
||||||
|
complete: () => pushModal(SignUpComplete, {next: flows.nostr.finalize}),
|
||||||
|
finalize: () => {
|
||||||
|
const secret = getKey<string>("signup.secret")!
|
||||||
|
|
||||||
|
loginWithNip01(secret)
|
||||||
|
completeSignup()
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = () => pushModal(SignUpProfile)
|
|
||||||
|
|
||||||
let email = $state("")
|
|
||||||
let password = $state("")
|
|
||||||
let loading = $state(false)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(usePassword)}>
|
<div class="column gap-4">
|
||||||
<h1 class="heading">Sign up with Nostr</h1>
|
<h1 class="heading">Sign up with Nostr</h1>
|
||||||
<p class="m-auto max-w-sm text-center">
|
<p class="m-auto max-w-sm text-center">
|
||||||
{PLATFORM_NAME} is built using the
|
{PLATFORM_NAME} is built using the
|
||||||
<Button class="link" onclick={() => pushModal(InfoNostr)}>nostr protocol</Button>, which gives
|
<Button class="link" onclick={() => pushModal(InfoNostr)}>nostr protocol</Button>, which gives
|
||||||
users control over their digital identity using <strong>cryptographic key pairs</strong>.
|
users control over their digital identity using <strong>cryptographic key pairs</strong>.
|
||||||
</p>
|
</p>
|
||||||
{#if BURROW_URL}
|
{#if hasPomade}
|
||||||
<FieldInline>
|
<Button onclick={flows.email.start} class="btn btn-primary">
|
||||||
{#snippet label()}
|
<Icon icon={Letter} />
|
||||||
<p>Email</p>
|
Sign up with email
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon={UserRounded} />
|
|
||||||
<input bind:value={email} />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</FieldInline>
|
|
||||||
<FieldInline>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Password</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon={Key} />
|
|
||||||
<input bind:value={password} type="password" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</FieldInline>
|
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading || !email || !password}>
|
|
||||||
<Spinner {loading}>Sign Up</Spinner>
|
|
||||||
<Icon icon={AltArrowRight} />
|
|
||||||
</Button>
|
</Button>
|
||||||
<p class="text-sm opacity-75">
|
|
||||||
Note that your email and password will only work to log in to {PLATFORM_NAME}. To use your key
|
|
||||||
on other nostr applications, you can create a nostr key yourself, or export your key from {PLATFORM_NAME}
|
|
||||||
later.
|
|
||||||
</p>
|
|
||||||
<Divider>Or</Divider>
|
|
||||||
{/if}
|
{/if}
|
||||||
<Button onclick={next} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
<Button onclick={flows.nostr.start} class="btn {hasPomade ? 'btn-neutral' : 'btn-primary'}">
|
||||||
<Icon icon={Key} />
|
<Icon icon={Key} />
|
||||||
Generate a key
|
Generate a key
|
||||||
</Button>
|
</Button>
|
||||||
@@ -97,4 +113,4 @@
|
|||||||
Already have an account?
|
Already have an account?
|
||||||
<Button class="link" onclick={login}>Log in instead</Button>
|
<Button class="link" onclick={login}>Log in instead</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Profile} from "@welshman/util"
|
|
||||||
import {createProfile, PROFILE, makeEvent} from "@welshman/util"
|
|
||||||
import {publishThunk, loginWithNip01} 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 HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
||||||
@@ -9,34 +6,14 @@
|
|||||||
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 {clearModals} from "@app/util/modal"
|
|
||||||
import {PROTECTED} from "@app/core/state"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
secret: string
|
next: () => void
|
||||||
profile: Profile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {secret, profile}: Props = $props()
|
const {next}: Props = $props()
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const next = () => {
|
|
||||||
const template = createProfile(profile)
|
|
||||||
|
|
||||||
// Start out protected by default
|
|
||||||
template.tags.push(PROTECTED)
|
|
||||||
|
|
||||||
const event = makeEvent(PROFILE, template)
|
|
||||||
|
|
||||||
// Log in first, then publish
|
|
||||||
loginWithNip01(secret)
|
|
||||||
|
|
||||||
// Don't publish anywhere yet, wait until they join a space
|
|
||||||
publishThunk({event, relays: []})
|
|
||||||
|
|
||||||
clearModals()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Client} from "@pomade/core"
|
||||||
|
import {choice} from "@welshman/lib"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
|
import Key from "@assets/icons/key.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import {getKey, setKey} from "@lib/implicit"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.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 SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
next: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {next}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
if (password.trim().length < 12) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Password must be at least 12 characters long.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
let client: Client | undefined = undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const secret = getKey<string>("signup.secret")!
|
||||||
|
const {clientOptions, ...registerRes} = await Client.register(2, 3, secret)
|
||||||
|
|
||||||
|
if (!registerRes.ok) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Failed to register! Please try again.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
client = new Client(clientOptions)
|
||||||
|
|
||||||
|
const setupRes = await client.setupRecovery(email, password)
|
||||||
|
|
||||||
|
if (!setupRes.ok) {
|
||||||
|
const message = setupRes.messages[0]?.payload.message || "Please try again."
|
||||||
|
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to register! ${message}.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const challengeRes = await Client.requestChallenge(email, [choice(clientOptions.peers)])
|
||||||
|
|
||||||
|
if (!challengeRes.ok) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to request confirmation code! Please try again.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setKey("signup.email", email)
|
||||||
|
setKey("signup.clientOptions", clientOptions)
|
||||||
|
|
||||||
|
pushModal(SignUpEmailConfirm, {next})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Failed to register! Please try again.",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
client?.stop()
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let email = $state("")
|
||||||
|
let password = $state("")
|
||||||
|
let loading = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Sign up with Email</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Keep your keys safe using multi-signer key sharing</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
Under the hood, nostr uses "cryptographic keypairs" to help you prove that your identity is
|
||||||
|
actually you.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you you're not ready to take control of your keys though, that's ok! We'll keep them safe
|
||||||
|
until you are.
|
||||||
|
</p>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Email*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon={Letter} />
|
||||||
|
<input bind:value={email} />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Password*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon={Key} />
|
||||||
|
<input type="password" bind:value={password} />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" type="submit" disabled={loading || !email || !password}>
|
||||||
|
<Spinner {loading}>Continue</Spinner>
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {sleep} from "@welshman/lib"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import {getKey} from "@lib/implicit"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
next: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {next}: Props = $props()
|
||||||
|
|
||||||
|
const email = getKey<string>("signup.email")
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
// Just pretend we're validating, they clearly got a code from somewhere
|
||||||
|
await sleep(800)
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
let challenge = $state("")
|
||||||
|
let loading = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Verify your Email Address</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Enter the one-time confirmation code sent to your email</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Confirmation Code*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon={Key} />
|
||||||
|
<input bind:value={challenge} />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<p class="text-sm">
|
||||||
|
We just sent a one-time confirmation code to {email}. Once you receive it, you can enter it
|
||||||
|
above.
|
||||||
|
</p>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading || !challenge}>
|
||||||
|
<Spinner {loading}>Log In</Spinner>
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -1,168 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {nsecEncode} from "nostr-tools/nip19"
|
import {getKey} from "@lib/implicit"
|
||||||
import {encrypt} from "nostr-tools/nip49"
|
import KeyDownload from "@app/components/KeyDownload.svelte"
|
||||||
import {hexToBytes} from "@welshman/lib"
|
|
||||||
import {makeSecret} from "@welshman/signer"
|
|
||||||
import type {Profile} from "@welshman/util"
|
|
||||||
import {preventDefault, downloadText} from "@lib/html"
|
|
||||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
|
||||||
import ArrowDown from "@assets/icons/arrow-down.svg?dataurl"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Field from "@lib/components/Field.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
|
||||||
import SignUpComplete from "@app/components/SignUpComplete.svelte"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
|
||||||
import {pushModal} from "@app/util/modal"
|
|
||||||
import {PLATFORM_NAME} from "@app/core/state"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
profile: Profile
|
next: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {profile}: Props = $props()
|
const {next}: Props = $props()
|
||||||
|
|
||||||
const secret = makeSecret()
|
const secret = getKey<string>("signup.secret")!
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
|
|
||||||
const cleanupCopy = (copy: string) =>
|
|
||||||
copy
|
|
||||||
.replace(/\n\s*\n\s*/g, "NEWLINE")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.replace(/NEWLINE/g, "\n\n")
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
const downloadKey = () => {
|
|
||||||
const sharedCopy = `
|
|
||||||
Most online services keep track of users by giving them a username and password. This gives the
|
|
||||||
service total control over their users, allowing them to ban them at any time, or sell their activity.
|
|
||||||
|
|
||||||
On Nostr, you control your own identity and social data, through the magic of cryptography. The basic
|
|
||||||
idea is that you have a public key, which acts as your user ID, and a private key which allows you to
|
|
||||||
prove your identity.
|
|
||||||
|
|
||||||
It's very important to keep your private key secret because it grants permanent and complete access to your
|
|
||||||
account.
|
|
||||||
`
|
|
||||||
|
|
||||||
if (usePassword) {
|
|
||||||
if (password.length < 12) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Your password must be at least 12 characters long.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const ncryptsec = encrypt(hexToBytes(secret), password)
|
|
||||||
const instructions = `
|
|
||||||
This file contains a backup of your Nostr secret key, downloaded from ${PLATFORM_NAME} and encrypted using
|
|
||||||
a password you chose when you signed up.
|
|
||||||
|
|
||||||
${sharedCopy}
|
|
||||||
|
|
||||||
Your encrypted private key is:
|
|
||||||
|
|
||||||
${ncryptsec}
|
|
||||||
|
|
||||||
To use it to log in to other Nostr apps, find a Nostr Signer app (https://nostrapps.com/#signers is a good
|
|
||||||
place to look), and import your key.
|
|
||||||
`
|
|
||||||
|
|
||||||
downloadText("Nostr Secret Key.txt", cleanupCopy(instructions))
|
|
||||||
} else {
|
|
||||||
const nsec = nsecEncode(hexToBytes(secret))
|
|
||||||
const instructions = `
|
|
||||||
This file contains a backup of your Nostr secret key, downloaded from ${PLATFORM_NAME}.
|
|
||||||
|
|
||||||
${sharedCopy}
|
|
||||||
|
|
||||||
Your private key is:
|
|
||||||
|
|
||||||
${nsec}
|
|
||||||
|
|
||||||
To use it to log in to other Nostr apps, find a Nostr Signer app (https://nostrapps.com/#signers is a good
|
|
||||||
place to look), and import your key.
|
|
||||||
`
|
|
||||||
|
|
||||||
downloadText("Nostr Secret Key.txt", cleanupCopy(instructions))
|
|
||||||
}
|
|
||||||
|
|
||||||
didDownload = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = () => {
|
|
||||||
pushModal(SignUpComplete, {profile, secret})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPasswordChange = () => {
|
|
||||||
didDownload = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleUsePassword = () => {
|
|
||||||
usePassword = !usePassword
|
|
||||||
didDownload = false
|
|
||||||
}
|
|
||||||
|
|
||||||
let password = $state("")
|
|
||||||
let usePassword = $state(false)
|
|
||||||
let didDownload = $state(false)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
<KeyDownload {secret} {next} />
|
||||||
<ModalHeader>
|
|
||||||
{#snippet title()}
|
|
||||||
<div>Your Keys are Ready!</div>
|
|
||||||
{/snippet}
|
|
||||||
</ModalHeader>
|
|
||||||
<p>
|
|
||||||
A cryptographic key pair has two parts: your <strong>public key</strong> identifies your
|
|
||||||
account, while your <strong>private key</strong> acts sort of like a master password.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Securing your private key is very important, so make sure to take the time to save your key in a
|
|
||||||
secure place (like a password manager).
|
|
||||||
</p>
|
|
||||||
{#if usePassword}
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
Password*
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon={Key} />
|
|
||||||
<input bind:value={password} onchange={onPasswordChange} class="grow" type="password" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<p>Passwords should be at least 12 characters long. Write this down!</p>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
{/if}
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<Button class="btn {didDownload ? 'btn-neutral' : 'btn-primary'}" onclick={downloadKey}>
|
|
||||||
Download my key
|
|
||||||
<Icon icon={ArrowDown} />
|
|
||||||
</Button>
|
|
||||||
<Button class="btn btn-link no-underline" onclick={toggleUsePassword}>
|
|
||||||
{#if usePassword}
|
|
||||||
Nevermind, I want to download the plain version
|
|
||||||
{:else}
|
|
||||||
I want to download an encrypted version
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button class="btn btn-link" onclick={back}>
|
|
||||||
<Icon icon={AltArrowLeft} />
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
<Button disabled={!didDownload} class="btn btn-primary" type="submit">
|
|
||||||
Continue
|
|
||||||
<Icon icon={AltArrowRight} />
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</form>
|
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Profile} from "@welshman/util"
|
import type {Profile} from "@welshman/util"
|
||||||
import {makeProfile} from "@welshman/util"
|
|
||||||
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"
|
||||||
|
import {getKey, setKey} from "@lib/implicit"
|
||||||
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 ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||||
import SignUpKey from "@app/components/SignUpKey.svelte"
|
|
||||||
import {pushModal} from "@app/util/modal"
|
|
||||||
|
|
||||||
const initialValues = {
|
type Props = {
|
||||||
profile: makeProfile(),
|
next: () => void
|
||||||
shouldBroadcast: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {next}: Props = $props()
|
||||||
|
|
||||||
|
const profile = getKey<Profile>("signup.profile")!
|
||||||
|
|
||||||
|
const initialValues = {profile, shouldBroadcast: false}
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onsubmit = (values: {profile: Profile}) => pushModal(SignUpKey, values)
|
const onsubmit = ({profile}: {profile: Profile}) => {
|
||||||
|
setKey("signup.profile", profile)
|
||||||
|
next()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
|
||||||
import {pushModal} from "@app/util/modal"
|
|
||||||
|
|
||||||
const {email} = $props()
|
|
||||||
|
|
||||||
const login = () => pushModal(LogInPassword)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="column gap-4">
|
|
||||||
<h1 class="heading">Success!</h1>
|
|
||||||
<p class="m-auto max-w-sm text-center">
|
|
||||||
A confirmation email has been sent to {email}.
|
|
||||||
</p>
|
|
||||||
<p>Once you've confirmed your account you'll be redirected to the login page.</p>
|
|
||||||
<Button class="btn btn-primary" onclick={login}>Back to Login</Button>
|
|
||||||
</div>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {spec, prop, avg} from "@welshman/lib"
|
import {spec, avg} from "@welshman/lib"
|
||||||
import {session, SessionMethod, signerLog, SignerLogEntryStatus} from "@welshman/app"
|
import {session, SessionMethod, signerLog} from "@welshman/app"
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||||
@@ -9,18 +9,20 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import LogOut from "@app/components/LogOut.svelte"
|
import LogOut from "@app/components/LogOut.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import PomadeSessions from "@app/components/PomadeSessions.svelte"
|
||||||
|
|
||||||
const {Pending, Success, Failure} = SignerLogEntryStatus
|
const finished = $derived($signerLog.filter(x => x.finished_at))
|
||||||
const pending = $derived($signerLog.filter(spec({status: Pending})).length)
|
const pending = $derived($signerLog.filter(x => !x.finished_at))
|
||||||
const success = $derived($signerLog.filter(spec({status: Success})).length)
|
const failure = $derived(finished.filter(spec({ok: false})))
|
||||||
const failure = $derived($signerLog.filter(spec({status: Failure})).length)
|
const success = $derived(finished.filter(spec({ok: true})))
|
||||||
const recent = $derived($signerLog.slice(-10))
|
const recent = $derived($signerLog.filter(x => x.started_at < Date.now() - 5000).slice(-10))
|
||||||
const recentAvg = $derived(avg(recent.map(prop("duration"))))
|
const recentFinished = $derived(recent.filter(x => x.finished_at))
|
||||||
const recentPending = $derived(recent.filter(spec({status: Pending})).length)
|
const recentPending = $derived(recent.filter(x => !x.finished_at))
|
||||||
const recentSuccess = $derived(recent.filter(spec({status: Success})).length)
|
const recentAvg = $derived(avg(recentFinished.map(x => x.finished_at! - x.started_at)))
|
||||||
const recentFailure = $derived(recent.filter(spec({status: Failure})).length)
|
const recentFailure = $derived(recentFinished.filter(x => !x.ok))
|
||||||
|
const recentSuccess = $derived(recentFinished.filter(x => x.ok))
|
||||||
const isDisconnected = $derived(
|
const isDisconnected = $derived(
|
||||||
recent.length > 0 && recentFailure + recentPending === recent.length,
|
recent.length > 0 && recentFailure.length + recentPending.length === recent.length,
|
||||||
)
|
)
|
||||||
|
|
||||||
const logout = () => pushModal(LogOut)
|
const logout = () => pushModal(LogOut)
|
||||||
@@ -34,11 +36,13 @@
|
|||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
{#if isDisconnected}
|
{#if isDisconnected}
|
||||||
<Icon icon={CloseCircle} class="text-error" size={4} /> Disconnected
|
<Icon icon={CloseCircle} class="text-error" size={4} /> Disconnected
|
||||||
{:else if recentFailure > 3}
|
{:else if recentFailure.length > 3}
|
||||||
<Icon icon={Danger} class="text-warning" size={4} /> Partial Failure
|
<Icon icon={Danger} class="text-warning" size={4} /> Partial Failure
|
||||||
{:else if recentAvg > 1000 || recentPending > 3}
|
{:else if recentAvg > 1000 || recentPending.length > 3}
|
||||||
<Icon icon={ClockCircle} class="text-warning" size={4} /> Slow connection
|
<Icon icon={ClockCircle} class="text-warning" size={4} /> Slow connection
|
||||||
{:else if recentSuccess === 0 && recentFailure > 0}{:else}
|
{:else if recentSuccess.length === 0 && recentFailure.length > 0}
|
||||||
|
<Icon icon={Danger} class="text-warning" size={4} /> Partial Failure
|
||||||
|
{:else}
|
||||||
<Icon icon={CheckCircle} class="text-success" size={4} /> Ok
|
<Icon icon={CheckCircle} class="text-success" size={4} /> Ok
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
@@ -56,15 +60,19 @@
|
|||||||
{$session.signer}
|
{$session.signer}
|
||||||
{:else if $session.method === SessionMethod.Pubkey}
|
{:else if $session.method === SessionMethod.Pubkey}
|
||||||
public key (readonly)
|
public key (readonly)
|
||||||
|
{:else if $session.method === SessionMethod.Pomade}
|
||||||
|
email and password
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{success} requests succeeded, {failure} failed, {pending} pending
|
{success.length} requests succeeded, {failure.length} failed, {pending.length} pending
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if isDisconnected}
|
{#if isDisconnected}
|
||||||
<Button class="btn btn-outline btn-error" onclick={logout}>Logout to Reconnect</Button>
|
<Button class="btn btn-outline btn-error" onclick={logout}>Logout to Reconnect</Button>
|
||||||
|
{:else}
|
||||||
|
<PomadeSessions />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
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 SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
|
||||||
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.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()
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
const next = async () => {
|
const next = async () => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return pushToast({theme: "error", message: error, timeout: 30_000})
|
return pushModal(SpaceAccessRequest, {url})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Pool.get().get(url).auth.status === AuthStatus.None) {
|
if (Pool.get().get(url).auth.status === AuthStatus.None) {
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
let loading = $state(true)
|
let loading = $state(true)
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
;[error] = await Promise.all([attemptRelayAccess(url), sleep(3000)])
|
;[error] = await Promise.all([attemptRelayAccess(url), sleep(1000)])
|
||||||
loading = false
|
loading = false
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -77,7 +77,11 @@
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
Join Space
|
{#if error}
|
||||||
|
Request Access
|
||||||
|
{:else}
|
||||||
|
Join Space
|
||||||
|
{/if}
|
||||||
<Icon icon={AltArrowRight} />
|
<Icon icon={AltArrowRight} />
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
import Pen from "@assets/icons/pen.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 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 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"
|
||||||
@@ -30,29 +30,33 @@
|
|||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const startEdit = () => pushModal(SpaceEdit, {url, initialValues: $relay})
|
const startEdit = () => pushModal(SpaceEdit, {url, initialValues: $relay || {url}})
|
||||||
</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}
|
||||||
@@ -87,18 +91,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if $userIsAdmin}
|
<ModalFooter>
|
||||||
<ModalFooter>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Icon icon={AltArrowLeft} />
|
||||||
<Icon icon={AltArrowLeft} />
|
Go back
|
||||||
Go back
|
</Button>
|
||||||
</Button>
|
</ModalFooter>
|
||||||
<Button class="btn btn-primary" onclick={startEdit}>
|
|
||||||
<Icon icon={Pen} />
|
|
||||||
Edit Space
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
{:else}
|
|
||||||
<Button class="btn btn-primary" onclick={back}>Got it</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {uniqBy, prop, append, ifLet} from "@welshman/lib"
|
|
||||||
import type {RelayProfile} from "@welshman/util"
|
import type {RelayProfile} from "@welshman/util"
|
||||||
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
||||||
import {manageRelay, relays, fetchRelayProfileDirectly} from "@welshman/app"
|
import {manageRelay, forceLoadRelay} from "@welshman/app"
|
||||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||||
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
@@ -22,10 +21,10 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
initialValues: RelayProfile
|
initialValues: Partial<RelayProfile>
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, initialValues}: Props = $props()
|
const {url, initialValues = {}}: Props = $props()
|
||||||
|
|
||||||
const values = $state(initialValues)
|
const values = $state(initialValues)
|
||||||
|
|
||||||
@@ -57,8 +56,6 @@
|
|||||||
if (imageFile) {
|
if (imageFile) {
|
||||||
const {error, result} = await uploadFile(imageFile, {maxWidth: 128, maxHeight: 128})
|
const {error, result} = await uploadFile(imageFile, {maxWidth: 128, maxHeight: 128})
|
||||||
|
|
||||||
console.log(imageFile, result)
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return pushToast({theme: "error", message: error})
|
return pushToast({theme: "error", message: error})
|
||||||
}
|
}
|
||||||
@@ -73,12 +70,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force-reload the relay
|
|
||||||
ifLet(await fetchRelayProfileDirectly(url), relay => {
|
|
||||||
relays.update($relays => uniqBy(prop("url"), append(relay, $relays)))
|
|
||||||
})
|
|
||||||
|
|
||||||
pushToast({message: "Your changes have been saved!"})
|
pushToast({message: "Your changes have been saved!"})
|
||||||
|
forceLoadRelay(url)
|
||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,17 +138,17 @@
|
|||||||
{#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="" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-sm opacity-75">No icon selected</span>
|
<span class="text-sm opacity-75">No icon selected</span>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
|
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-xs">
|
||||||
<Icon icon={StickerSmileSquare} size={4} />
|
<Icon icon={StickerSmileSquare} size={4} />
|
||||||
Select
|
Select
|
||||||
</IconPickerButton>
|
</IconPickerButton>
|
||||||
<label class="btn btn-neutral btn-sm cursor-pointer">
|
<label class="btn btn-neutral btn-xs cursor-pointer">
|
||||||
<Icon icon={UploadMinimalistic} size={4} />
|
<Icon icon={UploadMinimalistic} size={4} />
|
||||||
Upload
|
Upload
|
||||||
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
|
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
|
||||||
@@ -172,7 +165,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="" />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon={SettingsMinimalistic} />
|
<Icon icon={SettingsMinimalistic} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import QRCode from "@app/components/QRCode.svelte"
|
import QRCode from "@app/components/QRCode.svelte"
|
||||||
import {clip} from "@app/util/toast"
|
import {clip} from "@app/util/toast"
|
||||||
import {PLATFORM_URL, deriveRelayAuthError} from "@app/core/state"
|
import {PLATFORM_URL} from "@app/core/state"
|
||||||
|
import {deriveRelayAuthError} from "@app/core/commands"
|
||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -15,18 +15,14 @@
|
|||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const tryJoin = async () => {
|
|
||||||
await addSpaceMembership(url)
|
|
||||||
|
|
||||||
broadcastUserData([url])
|
|
||||||
clearModals()
|
|
||||||
}
|
|
||||||
|
|
||||||
const join = async () => {
|
const join = async () => {
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tryJoin()
|
await addSpaceMembership(url)
|
||||||
|
|
||||||
|
broadcastUserData([url])
|
||||||
|
clearModals()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 card2-sm 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,19 +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 SquareTopDown from "@assets/icons/square-top-down.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"
|
||||||
@@ -32,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"
|
||||||
@@ -42,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"
|
||||||
@@ -65,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}]),
|
||||||
@@ -83,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)
|
||||||
|
|
||||||
@@ -155,13 +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})
|
||||||
<Icon icon={SquareTopDown} size={4} class="opacity-50" />
|
</Button>
|
||||||
</Link>
|
|
||||||
</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} />
|
||||||
+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>
|
||||||
@@ -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>
|
||||||
+73
-74
@@ -1,6 +1,6 @@
|
|||||||
import {nwc} from "@getalby/sdk"
|
import {nwc} from "@getalby/sdk"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {get} from "svelte/store"
|
import {get, derived} from "svelte/store"
|
||||||
import type {Override, MakeOptional} from "@welshman/lib"
|
import type {Override, MakeOptional} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
first,
|
first,
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
DELETE,
|
DELETE,
|
||||||
REPORT,
|
REPORT,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
INBOX_RELAYS,
|
MESSAGING_RELAYS,
|
||||||
RELAYS,
|
RELAYS,
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
REACTION,
|
REACTION,
|
||||||
@@ -46,17 +46,14 @@ import {
|
|||||||
APP_DATA,
|
APP_DATA,
|
||||||
isSignedEvent,
|
isSignedEvent,
|
||||||
makeEvent,
|
makeEvent,
|
||||||
displayProfile,
|
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
makeList,
|
makeList,
|
||||||
addToListPublicly,
|
addToListPublicly,
|
||||||
removeFromListByPredicate,
|
removeFromListByPredicate,
|
||||||
getTag,
|
getTag,
|
||||||
getListTags,
|
getListTags,
|
||||||
getRelayTags,
|
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
toNostrURI,
|
toNostrURI,
|
||||||
getRelaysFromList,
|
|
||||||
RelayMode,
|
RelayMode,
|
||||||
getAddress,
|
getAddress,
|
||||||
getTagValue,
|
getTagValue,
|
||||||
@@ -79,21 +76,20 @@ import {
|
|||||||
session,
|
session,
|
||||||
repository,
|
repository,
|
||||||
publishThunk,
|
publishThunk,
|
||||||
profilesByPubkey,
|
|
||||||
tagEvent,
|
tagEvent,
|
||||||
tagEventForReaction,
|
tagEventForReaction,
|
||||||
userRelaySelections,
|
|
||||||
userInboxRelaySelections,
|
|
||||||
nip44EncryptToSelf,
|
nip44EncryptToSelf,
|
||||||
dropSession,
|
dropSession,
|
||||||
tagEventForComment,
|
tagEventForComment,
|
||||||
tagEventForQuote,
|
tagEventForQuote,
|
||||||
waitForThunkError,
|
waitForThunkError,
|
||||||
getPubkeyRelays,
|
getPubkeyRelays,
|
||||||
userBlossomServers,
|
userBlossomServerList,
|
||||||
shouldUnwrap,
|
shouldUnwrap,
|
||||||
|
getThunkError,
|
||||||
} 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 +101,14 @@ import {
|
|||||||
userSpaceUrls,
|
userSpaceUrls,
|
||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
getSetting,
|
getSetting,
|
||||||
userInboxRelays,
|
userGroupList,
|
||||||
userGroupSelections,
|
|
||||||
shouldIgnoreError,
|
shouldIgnoreError,
|
||||||
|
stripPrefix,
|
||||||
|
relaysMostlyRestricted,
|
||||||
|
deriveSocket,
|
||||||
} 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)])
|
||||||
@@ -214,42 +204,6 @@ export const removeRoomMembership = async (url: string, h: string) => {
|
|||||||
return publishThunk({event, relays})
|
return publishThunk({event, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
|
|
||||||
const list = get(userRelaySelections) || makeList({kind: RELAYS})
|
|
||||||
const tags = getRelayTags(getListTags(list)).filter(t => normalizeRelayUrl(t[1]) !== url)
|
|
||||||
|
|
||||||
if (read && write) {
|
|
||||||
tags.push(["r", url])
|
|
||||||
} else if (read) {
|
|
||||||
tags.push(["r", url, "read"])
|
|
||||||
} else if (write) {
|
|
||||||
tags.push(["r", url, "write"])
|
|
||||||
}
|
|
||||||
|
|
||||||
return publishThunk({
|
|
||||||
event: makeEvent(list.kind, {tags}),
|
|
||||||
relays: [url, ...INDEXER_RELAYS, ...Router.get().FromUser().getUrls(), ...get(userSpaceUrls)],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
|
||||||
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
|
|
||||||
|
|
||||||
// Only update inbox policies if they already exist or we're adding them
|
|
||||||
if (enabled || getRelaysFromList(list).includes(url)) {
|
|
||||||
const tags = getRelayTags(getListTags(list)).filter(t => normalizeRelayUrl(t[1]) !== url)
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
tags.push(["relay", url])
|
|
||||||
}
|
|
||||||
|
|
||||||
return publishThunk({
|
|
||||||
event: makeEvent(list.kind, {tags}),
|
|
||||||
relays: [...INDEXER_RELAYS, ...Router.get().FromUser().getUrls(), ...get(userSpaceUrls)],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Relay access
|
// Relay access
|
||||||
|
|
||||||
export const canEnforceNip70 = async (url: string) => {
|
export const canEnforceNip70 = async (url: string) => {
|
||||||
@@ -291,12 +245,40 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
|||||||
|
|
||||||
if (shouldIgnoreError(error)) return
|
if (shouldIgnoreError(error)) return
|
||||||
|
|
||||||
if (claim) {
|
if (error.includes("invite code")) return "join request rejected"
|
||||||
const ignoreClaimError =
|
|
||||||
error.includes("invalid invite code size") || error.includes("failed to validate invite code")
|
|
||||||
|
|
||||||
if (!ignoreClaimError) return error?.replace(/^\w+: /, "")
|
return stripPrefix(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const deriveRelayAuthError = (url: string, claim = "") => {
|
||||||
|
// Kick off the auth process
|
||||||
|
Pool.get().get(url).auth.attemptAuth(sign)
|
||||||
|
|
||||||
|
// Attempt to join the relay
|
||||||
|
const thunk = publishJoinRequest({url, claim})
|
||||||
|
|
||||||
|
return derived(
|
||||||
|
[thunk, relaysMostlyRestricted, deriveSocket(url)],
|
||||||
|
([$thunk, $relaysMostlyRestricted, $socket]) => {
|
||||||
|
if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) {
|
||||||
|
return stripPrefix($socket.auth.details)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($relaysMostlyRestricted[url]) {
|
||||||
|
return stripPrefix($relaysMostlyRestricted[url])
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = getThunkError($thunk)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const isEmptyInvite = !claim && error.includes("invite code")
|
||||||
|
|
||||||
|
if (!shouldIgnoreError(error) && !isEmptyInvite) {
|
||||||
|
return stripPrefix(error) || "join request rejected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletions
|
// Deletions
|
||||||
@@ -538,11 +520,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 +576,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 +584,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))
|
||||||
@@ -648,7 +635,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)
|
||||||
@@ -727,6 +714,18 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
|||||||
|
|
||||||
// Update Profile
|
// Update Profile
|
||||||
|
|
||||||
|
export const initProfile = (profile: Profile) => {
|
||||||
|
const template = createProfile(profile)
|
||||||
|
|
||||||
|
// Start out protected by default
|
||||||
|
template.tags.push(PROTECTED)
|
||||||
|
|
||||||
|
const event = makeEvent(PROFILE, template)
|
||||||
|
|
||||||
|
// Don't publish anywhere yet, wait until they join a space
|
||||||
|
return publishThunk({event, relays: []})
|
||||||
|
}
|
||||||
|
|
||||||
export const updateProfile = async ({
|
export const updateProfile = async ({
|
||||||
profile,
|
profile,
|
||||||
shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []),
|
shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user