Compare commits

...

83 Commits

Author SHA1 Message Date
Jon Staab 1510f39a8a Bump ios version 2026-01-19 10:07:44 -08:00
Jon Staab bbbe011482 Publish default relay selections on signup 2026-01-16 16:11:45 -08:00
Jon Staab 82ab7a043f Remove glitchtip integration 2026-01-16 15:19:52 -08:00
Jon Staab 798253a50e Bump welshman 2026-01-16 15:07:55 -08:00
Jon Staab 52432ca068 Add sign in with private key 2026-01-16 14:25:44 -08:00
Jon Staab b3f1d8464b Add authentication policy setting 2026-01-16 13:49:35 -08:00
Jon Staab 87bb62b359 Add support for blocked relays 2026-01-16 13:10:48 -08:00
Jon Staab 3f914d02cc Fix signer disconnection flash, nav icon sizes 2026-01-16 11:33:03 -08:00
Jon Staab d1db77d0f5 Bump version 2026-01-16 11:01:24 -08:00
Jon Staab 6aa297c1a4 Rework onboarding flow, add recovery 2026-01-16 11:01:07 -08:00
Jon Staab f3647e9bc1 Use simple OTPs 2026-01-16 11:01:07 -08:00
Jon Staab 5b43c62f2d Remove pomade signers 2026-01-16 11:01:07 -08:00
Jon Staab 23ffb15a8d Fix incorrect secret being downloaded 2026-01-16 11:01:07 -08:00
Jon Staab adb2ce4846 Split key recovery components, bump deps 2026-01-16 11:01:06 -08:00
Jon Staab cdee6ca743 Add pomade key recovery 2026-01-16 11:00:46 -08:00
Jon Staab fe30aa4af2 Fix ContentLinkInline 2026-01-16 11:00:45 -08:00
Jon Staab 9943728eab Add pomade session list 2026-01-16 11:00:00 -08:00
Jon Staab 8ae7cf05cc Fix profile publishing on email sign up 2026-01-16 11:00:00 -08:00
Jon Staab a7c944e8ef Tweak breakpoint for field inline 2026-01-16 11:00:00 -08:00
Jon Staab 102339d7e8 Add link_peers script 2026-01-16 10:59:59 -08:00
Jon Staab 9a0ad0c663 Improve space join flow 2026-01-16 10:59:54 -08:00
Jon Staab f86afc08fa Normalize relay URLs 2026-01-16 10:59:46 -08:00
Jon Staab cd1b328b1b Add pomade signing 2026-01-16 10:59:45 -08:00
Jon Staab 48f2bb1c75 Bump gradle 2026-01-16 10:59:30 -08:00
Jon Staab d416fe913e Fix memory leak, notification badge not showing 2026-01-16 10:59:29 -08:00
Jon Staab 7f8744725c Improve signer status 2026-01-16 10:59:25 -08:00
Jon Staab e5d1b82a9d Fix chat list responsiveness 2026-01-16 10:59:22 -08:00
Jon Staab 619cf2e134 Update default relays 2026-01-16 10:59:16 -08:00
Jon Staab 28b522f015 Report pending signer to user 2026-01-16 10:59:10 -08:00
Jon Staab 39233f261e Force reload relay more simply 2026-01-16 10:59:05 -08:00
Jon Staab 00f0127caf Tweak room edit form 2026-01-16 10:59:03 -08:00
Jon Staab f69b575381 Fix some duplicates in eaches 2026-01-16 10:58:57 -08:00
Jon Staab 986973a605 Accept hex pubkeys/npubs/nprofiles in ProfileMultiSelect 2026-01-16 10:58:55 -08:00
Jon Staab 0d6b4591f1 Hide tooltips on mobile, sort comments ascending, make video embeds rounded 2026-01-16 10:58:40 -08:00
Jon Staab 2c62749d9b Attempt to fix new messages button 2026-01-16 10:56:18 -08:00
Jon Staab 4be4288ef0 Fix phantom notifications on mobile 2025-12-11 10:27:10 -08:00
Jon Staab c7eec167cf Fix scroll down z index 2025-12-08 09:27:38 -08:00
Jon Staab 7bae956ffa Release 1.6.2 2025-12-08 09:22:49 -08:00
Jon Staab a2f59a5b1b Fix some modal bugs 2025-12-08 09:19:41 -08:00
Jon Staab df56af9b0e Bump version 2025-12-05 09:51:15 -08:00
Jon Staab 83f7f9584f Fix duplicate rooms 2025-12-04 17:06:50 -08:00
Jon Staab a2d440e54f Fix dialog z index 2025-12-04 16:01:39 -08:00
Jon Staab 4132e8449b Fix recent missing events in feeds 2025-12-04 15:56:05 -08:00
Jon Staab ee444416e4 Fall back to file name as hash for images 2025-12-04 14:37:59 -08:00
Jon Staab 10c12c3c48 Improve time based chat partitioning 2025-12-04 14:29:12 -08:00
Jon Staab db3775ae99 Fix timezone parsing in AlertAdd 2025-12-04 11:20:54 -08:00
Jon Staab 393acce884 Fix removing non-normalized urls 2025-12-02 17:27:14 -08:00
Jon Staab 68fe663730 Fix chat content bottom offset when keyboard is open 2025-12-02 17:20:10 -08:00
Jon Staab f65a4b0db0 Handle relay urls in content and link within the app 2025-12-02 17:09:56 -08:00
Jon Staab cdfb502e6e Fix skinny profile circles 2025-12-02 13:49:04 -08:00
Jon Staab 1a2c83e49b Bump version 2025-12-02 13:38:03 -08:00
Jon Staab e6c7a675a9 Bump welshman 2025-12-02 13:24:43 -08:00
Jon Staab 69c04f29f4 Tweak zap button 2025-12-02 09:31:38 -08:00
Jon Staab 04c6f9b4fe Add date to chats 2025-12-01 11:09:26 -08:00
Jon Staab 86ec12a9db Tweak some mobile menu components 2025-12-01 11:04:11 -08:00
Jon Staab 72b3111c64 Refine sync 2025-12-01 10:56:37 -08:00
Jon Staab 6709c91779 Fix discover social proof 2025-12-01 10:26:43 -08:00
Jon Staab bb6e7495f5 Add editor props from nostr-editor 2025-12-01 08:45:35 -08:00
Jon Staab df17929681 Fix new messages indicator 2025-11-25 17:06:21 -08:00
Jon Staab e083719ceb Hide nav when keyboard is open 2025-11-25 15:43:21 -08:00
Jon Staab bfdc69f18c Fix chats 2025-11-25 15:05:45 -08:00
Jon Staab e7ae20afb7 Fix content type nav items 2025-11-25 14:13:28 -08:00
Jon Staab 229d92055f Debounce search 2025-11-25 11:55:32 -08:00
Jon Staab 64c77cfd13 Migrate to new welshman stores 2025-11-21 12:40:59 -08:00
Jon Staab 3a63894562 Switch wording to messaging from inbox 2025-11-20 15:12:16 -08:00
Jon Staab 1d272f8b37 Tweak nav icon size 2025-11-14 15:02:23 -08:00
Jon Staab bac433b640 Re-work storage adapter a bit 2025-11-14 14:59:27 -08:00
Jon Staab 62f573eac0 Merge report detail components 2025-11-14 11:36:51 -08:00
Jon Staab b3ea62c53c Remove landlubber link 2025-11-13 17:01:23 -08:00
Jon Staab b0731503a8 Fix indexeddb deletes 2025-11-13 16:39:44 -08:00
Jon Staab 2421c02c24 Add room membership management 2025-11-13 15:25:18 -08:00
Jon Staab 25e868118d Slight optimization 2025-11-13 14:40:02 -08:00
Jon Staab 2880044e0e Add event admin deletion 2025-11-13 14:25:59 -08:00
Jon Staab 5300404b46 Add option to ban users from profile detail dialog 2025-11-13 13:44:52 -08:00
Jon Staab d949d58076 Add space membership management 2025-11-13 13:25:34 -08:00
Jon Staab 997b223e95 Rename space menu components 2025-11-13 10:36:28 -08:00
Jon Staab ba52a97e26 Tweak relay icon size in nav 2025-11-13 10:34:02 -08:00
Jon Staab cc4c7b5fe9 Fix image modal, only show + room if the user is allowed 2025-11-13 10:32:37 -08:00
Jon Staab 8e2ebd11fc remove some alts 2025-11-13 08:59:32 -08:00
Jon Staab 9cae4da9f4 Add lightning invoice payments 2025-11-12 16:24:58 -08:00
Jon Staab c05d7e99e2 remove old icon picker 2025-11-12 14:56:14 -08:00
Jon Staab 2390599e8f Fix relay updating and relay icons 2025-11-11 17:48:24 -08:00
Jon Staab 1a4d45fa9c Upload svgs for room icon 2025-11-11 17:34:15 -08:00
144 changed files with 4622 additions and 4189 deletions
+6 -4
View File
@@ -1,6 +1,6 @@
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
VITE_BURROW_URL=
VITE_POMADE_SIGNERS=
VITE_PLATFORM_URL=https://app.flotilla.social
VITE_PLATFORM_TERMS=https://flotilla.social/terms
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
@@ -10,10 +10,12 @@ VITE_PLATFORM_RELAYS=
VITE_PLATFORM_ACCENT="#7161FF"
VITE_PLATFORM_SECONDARY="#EB5E28"
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_SIGNER_RELAYS=wss://relay.nsec.app/,wss://offchain.pub/
VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
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_GLITCHTIP_API_KEY=
GLITCHTIP_AUTH_TOKEN=
+50
View File
@@ -1,5 +1,55 @@
# 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
* Add space edit form
+34
View File
@@ -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.
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion
versionCode 34
versionName "1.5.3"
versionCode 39
versionName "1.6.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+1 -1
View File
@@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
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'
// NOTE: Do not place your application dependencies here; they belong
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+4 -4
View File
@@ -358,14 +358,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 29;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
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\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -384,14 +384,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 29;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.5.3;
MARKETING_VERSION = 1.6.3;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Executable
+32
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "flotilla",
"version": "1.5.3",
"version": "1.6.3",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -10,14 +10,13 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"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",
"prepare": "husky"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@eslint/js": "^9.37.0",
"@sentry/cli": "^2.56.1",
"@sveltejs/kit": "^2.46.5",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/eslint": "^9.6.1",
@@ -51,25 +50,26 @@
"@capacitor/push-notifications": "^7.0.3",
"@capawesome/capacitor-android-dark-mode-support": "^7.0.0",
"@capawesome/capacitor-badge": "^7.0.1",
"@getalby/lightning-tools": "^6.0.0",
"@getalby/sdk": "^5.1.2",
"@pomade/core": "^0.0.12",
"@poppanator/sveltekit-svg": "^4.2.1",
"@sentry/browser": "^8.55.0",
"@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.26.3",
"@types/qrcode": "^1.5.5",
"@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.6.8",
"@welshman/content": "^0.6.8",
"@welshman/editor": "^0.6.8",
"@welshman/feeds": "^0.6.8",
"@welshman/lib": "^0.6.8",
"@welshman/net": "^0.6.8",
"@welshman/router": "^0.6.8",
"@welshman/signer": "^0.6.8",
"@welshman/store": "^0.6.8",
"@welshman/util": "^0.6.8",
"@welshman/app": "^0.8.1",
"@welshman/content": "^0.8.1",
"@welshman/editor": "^0.8.1",
"@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",
"compressorjs": "^1.2.1",
"daisyui": "^4.12.24",
"date-picker-svelte": "^2.16.0",
@@ -79,7 +79,7 @@
"husky": "^9.1.7",
"idb": "^8.0.3",
"nostr-signer-capacitor-plugin": "^0.0.4",
"nostr-tools": "^2.14.2",
"nostr-tools": "^2.19.4",
"prettier-plugin-tailwindcss": "^0.6.14",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
@@ -88,7 +88,6 @@
},
"pnpm": {
"ignoredBuiltDependencies": [
"@sentry/cli",
"esbuild"
],
"onlyBuiltDependencies": [
+246 -360
View File
@@ -44,15 +44,18 @@ importers:
'@capawesome/capacitor-badge':
specifier: ^7.0.1
version: 7.0.1(@capacitor/core@7.4.3)
'@getalby/lightning-tools':
specifier: ^6.0.0
version: 6.0.0
'@getalby/sdk':
specifier: ^5.1.2
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':
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))
'@sentry/browser':
specifier: ^8.55.0
version: 8.55.0
'@sveltejs/adapter-static':
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)))
@@ -72,35 +75,35 @@ importers:
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))
'@welshman/app':
specifier: ^0.6.8
version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
specifier: ^0.8.1
version: 0.8.1(7cc2d202a89b51b45c1ecb6d486f020b)
'@welshman/content':
specifier: ^0.6.8
version: 0.6.8(typescript@5.9.3)
specifier: ^0.8.1
version: 0.8.1(nostr-tools@2.19.4(typescript@5.9.3))
'@welshman/editor':
specifier: ^0.6.8
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)
specifier: ^0.8.1
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':
specifier: ^0.6.8
version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
specifier: ^0.8.1
version: 0.8.1(c3072638adf29bdf3b0a110142b651da)
'@welshman/lib':
specifier: ^0.6.8
version: 0.6.8
specifier: ^0.8.1
version: 0.8.1
'@welshman/net':
specifier: ^0.6.8
version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
specifier: ^0.8.1
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':
specifier: ^0.6.8
version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
specifier: ^0.8.1
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':
specifier: ^0.6.8
version: 0.6.8(nostr-signer-capacitor-plugin@0.0.4(@capacitor/core@7.4.3))(typescript@5.9.3)(ws@8.18.3)
specifier: ^0.8.1
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':
specifier: ^0.6.8
version: 0.6.8(typescript@5.9.3)(ws@8.18.3)
specifier: ^0.8.1
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':
specifier: ^0.6.8
version: 0.6.8(typescript@5.9.3)
specifier: ^0.8.1
version: 0.8.1(@noble/curves@1.9.7)(@welshman/lib@0.8.1)(nostr-tools@2.19.4(typescript@5.9.3))
compressorjs:
specifier: ^1.2.1
version: 1.2.1
@@ -129,8 +132,8 @@ importers:
specifier: ^0.0.4
version: 0.0.4(@capacitor/core@7.4.3)
nostr-tools:
specifier: ^2.14.2
version: 2.17.0(typescript@5.9.3)
specifier: ^2.19.4
version: 2.19.4(typescript@5.9.3)
prettier-plugin-tailwindcss:
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)
@@ -153,9 +156,6 @@ importers:
'@eslint/js':
specifier: ^9.37.0
version: 9.37.0
'@sentry/cli':
specifier: ^2.56.1
version: 2.56.1
'@sveltejs/kit':
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))
@@ -217,10 +217,6 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
'@antfu/utils@0.7.10':
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
@@ -802,6 +798,15 @@ packages:
peerDependencies:
'@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':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -982,10 +987,17 @@ packages:
resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==}
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':
resolution: {integrity: sha512-dxOmJLJAh6qJ8rsbA5/Bwj7MSI9X3RkxxqmCedl5rfP+yKwNSdfu8i4EiCZN/tk2hNBJb8GHSCcPRNZfwfmEHg==}
engines: {node: '>=14'}
'@getalby/lightning-tools@6.0.0':
resolution: {integrity: sha512-jpTO+7o1N1KhV5qT6qetPK+et6ZQshCzUMCRV8+Ek1NVlVU4ITIqOWRQ3kOrb0PhSxkbGN5G3d60HCi535hbDw==}
engines: {node: '>=14'}
'@getalby/sdk@5.1.2':
resolution: {integrity: sha512-yUF9LhuvdIFOwjV1aG0ryzfwDiGBFk/CRLkRvrrM9dsE38SUjKsf1FDga5jxsKMu80nWcPZR9TiGGASWedoYPA==}
engines: {node: '>=14'}
@@ -1095,6 +1107,10 @@ packages:
'@noble/ciphers@0.5.3':
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':
resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==}
@@ -1117,6 +1133,10 @@ packages:
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
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':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -1139,6 +1159,18 @@ packages:
'@polka/url@1.0.0-next.29':
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':
resolution: {integrity: sha512-w7jl4EVOOF+X+uv2BEUiMDJwds+GfbczwGpcS0+rsjIsKYmqmwMi4ts3bVZR9ZvdFHWy5rS84U+pSBClz6cbBg==}
peerDependencies:
@@ -1326,82 +1358,6 @@ packages:
'@scure/bip39@1.2.1':
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':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
@@ -1692,38 +1648,82 @@ packages:
'@vite-pwa/assets-generator':
optional: true
'@welshman/app@0.6.8':
resolution: {integrity: sha512-bhl18VWA9tzHLY7D+b2xlkc/RbJr03XiA7+otcjzf8X48S4pih/F4TDw1yJbAWOMOx9G3NI6sWLffpZQeSUPiQ==}
'@welshman/app@0.8.1':
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':
resolution: {integrity: sha512-VLek8oOoMMTrEtpIfqFqM9BsbifWYwPC7UiuVuWYqaTSmiAbU3DM2J+tYFcrgnQF8xMnUi/JoVXJ+b2AtpjFrw==}
'@welshman/content@0.8.1':
resolution: {integrity: sha512-DQ08ijfQQojNKWj/LRzcry6IqA45s+xpg25sc2zxP6iXE4Q0cidSoVwvJjPa2PjYWsInC6Zmj/ZQOErWtsyBjQ==}
peerDependencies:
nostr-tools: ^2.19.4
'@welshman/editor@0.6.8':
resolution: {integrity: sha512-QzNX7/Nobkh+bpjFnuW2REVpX7Sa+lj70LDdGmEJpjtXlTKlLuNZzpFLee5F9fSObcKCl1G2xBN3tYbZD3vHUA==}
'@welshman/editor@0.8.1':
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':
resolution: {integrity: sha512-95VRR2QmGrBUyzYgdsMxhntVoOnaEMsMHRsci1/GX1oOFZPJFTiV7e/m/dD/aWVLUQV1hlRxxXosFFtEDkpjIw==}
'@welshman/feeds@0.8.1':
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':
resolution: {integrity: sha512-1Wybkk8+vBdqv9nRhnNwIW9YVbhu3di07A2fUYWAQvldto49X26U8u7EV2CkUsz4iNC/799EBYuelcc6W9oZYw==}
'@welshman/lib@0.8.1':
resolution: {integrity: sha512-s4gRg4NXwDPiXgZVuAaZKS+rpnfYcFfqTqvw44hBTIWa1o12J4k7bqP7Oyi3r6Q901lW/6tvP7TInBWkdhm5Bw==}
engines: {node: '>=12.0.0'}
'@welshman/net@0.6.8':
resolution: {integrity: sha512-Lc1nIckdxW2ILiknowcbaKo+192QWQOBn6FLhFCEUZNyRNEOJYkAgDu4jKn7GXu91xpfJUFnq5KDvvq7hUeHqg==}
'@welshman/router@0.6.8':
resolution: {integrity: sha512-+OJoD2Jm+yFiLc5FYb4/za66639CeIMYk7j4UAzR7n1z/gFQYMDviXqFYbcWHln3fgy4G7UF1HWBoU0sQD8EEw==}
'@welshman/signer@0.6.8':
resolution: {integrity: sha512-lt9Qq89TWyx/zSWgHkeVUX7MBCx86iBCkvzTdUIS7Ad6KfjjcYtsL9wAtfCc+TlvE87okOg97hAOvw18yIwfbw==}
'@welshman/net@0.8.1':
resolution: {integrity: sha512-ugPs8YT6B2WNVA9A443x3t0DAQyLnzj1nueDTAsHLbB9/4nAJDsXy7EKKcw4rMVXyZyaWZlBdOIR59hKhw1uew==}
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-tools: ^2.19.4
'@welshman/store@0.6.8':
resolution: {integrity: sha512-s5s5+tdPyXB1m2vLn2wfo7nx+uNKWBdwCyomk+soWKWEY3LWvg4DAKgQ1gF5hyOcja+UIHOJY9hS3BBEo0DtDA==}
'@welshman/store@0.8.1':
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':
resolution: {integrity: sha512-Q4x3Jm3yIk4zORYOscMuxyC7fJGyZFetE5U4PVYNrvgtSLCtULYKs1y6WkAra4FD7zfAa7lqzTlQq4uIZWzdkA==}
'@welshman/util@0.8.1':
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':
resolution: {integrity: sha512-aKqQ077XnR+oQtHJlrAflaZaL7qZsulWc/i/ZEooar5JiWj1eLt0+Wg28cpa+XLney107wXqneC+oG1IZvxkTA==}
@@ -1758,10 +1758,6 @@ packages:
add-stream@1.0.0:
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:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'}
@@ -2069,9 +2065,6 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
code-red@1.0.4:
resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@@ -2586,9 +2579,6 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@@ -2870,6 +2860,9 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hash-wasm@4.12.0:
resolution: {integrity: sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -2885,10 +2878,6 @@ packages:
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
engines: {node: '>=10'}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
@@ -3518,8 +3507,8 @@ packages:
typescript:
optional: true
nostr-tools@2.17.0:
resolution: {integrity: sha512-lrvHM7cSaGhz7F0YuBvgHMoU2s8/KuThihDoOYk8w5gpVHTy0DeUCAgCN8uLGeuSl5MAWekJr9Dkfo5HClqO9w==}
nostr-tools@2.19.4:
resolution: {integrity: sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==}
peerDependencies:
typescript: '>=5.0.0'
peerDependenciesMeta:
@@ -3659,9 +3648,6 @@ packages:
pend@1.2.0:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
periscopic@3.1.0:
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -3865,10 +3851,6 @@ packages:
process-nextick-args@2.0.1:
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:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
@@ -3931,9 +3913,6 @@ packages:
prosemirror-view@1.41.3:
resolution: {integrity: sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
pump@3.0.3:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
@@ -4381,10 +4360,6 @@ packages:
svelte:
optional: true
svelte@4.2.20:
resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==}
engines: {node: '>=16'}
svelte@5.39.12:
resolution: {integrity: sha512-CEzwxFuEycokU8K8CE/OuwVbmei+ivu2HvBGYIdASfMa1hCRSNr4RRkzNSvbAvu6h+BOig2CsZTAEY+WKvwZpA==}
engines: {node: '>=18'}
@@ -4926,15 +4901,16 @@ packages:
zimmerframe@1.1.4:
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:
'@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': {}
'@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)':
@@ -5718,6 +5694,27 @@ snapshots:
dependencies:
'@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':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@@ -5837,8 +5834,21 @@ snapshots:
'@eslint/core': 0.16.0
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@6.0.0': {}
'@getalby/sdk@5.1.2(typescript@5.9.3)':
dependencies:
'@getalby/lightning-tools': 5.2.1
@@ -6026,6 +6036,8 @@ snapshots:
'@noble/ciphers@0.5.3': {}
'@noble/ciphers@1.3.0': {}
'@noble/curves@1.1.0':
dependencies:
'@noble/hashes': 1.3.1
@@ -6044,6 +6056,8 @@ snapshots:
'@noble/hashes@1.8.0': {}
'@noble/hashes@2.0.1': {}
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -6065,6 +6079,18 @@ snapshots:
'@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))':
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
@@ -6212,78 +6238,6 @@ snapshots:
'@noble/hashes': 1.3.2
'@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': {}
'@surma/rollup-plugin-off-main-thread@2.2.3':
@@ -6651,32 +6605,26 @@ snapshots:
optionalDependencies:
'@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:
'@types/throttle-debounce': 5.0.2
'@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/lib': 0.6.8
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/router': 0.6.8(typescript@5.9.3)(ws@8.18.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/store': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.8(typescript@5.9.3)
'@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.8.1(c3072638adf29bdf3b0a110142b651da)
'@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/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.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.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.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
svelte: 4.2.20
svelte: 5.39.12
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:
'@braintree/sanitize-url': 7.1.1
nostr-tools: 2.17.0(typescript@5.9.3)
transitivePeerDependencies:
- typescript
nostr-tools: 2.19.4(typescript@5.9.3)
'@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:
'@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/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/util': 0.6.8(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-tools: 2.17.0(typescript@5.9.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))
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.19.4(typescript@5.9.3)
tippy.js: 6.3.7
transitivePeerDependencies:
- '@tiptap/extension-image'
@@ -6705,78 +6653,62 @@ snapshots:
- prosemirror-state
- prosemirror-view
- 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:
'@welshman/lib': 0.6.8
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/router': 0.6.8(typescript@5.9.3)(ws@8.18.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/util': 0.6.8(typescript@5.9.3)
'@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/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.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))
trava: 1.2.1
transitivePeerDependencies:
- nostr-signer-capacitor-plugin
- typescript
- ws
'@welshman/lib@0.6.8':
'@welshman/lib@0.8.1':
dependencies:
'@scure/base': 1.2.6
'@types/events': 3.0.3
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:
'@welshman/lib': 0.6.8
'@welshman/util': 0.6.8(typescript@5.9.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))
events: 3.3.0
isomorphic-ws: 5.0.0(ws@8.18.3)
transitivePeerDependencies:
- typescript
- 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:
'@welshman/lib': 0.6.8
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.8(typescript@5.9.3)
transitivePeerDependencies:
- typescript
- ws
'@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))':
dependencies:
'@noble/curves': 1.9.7
'@noble/hashes': 1.8.0
'@welshman/lib': 0.6.8
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.8(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/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.17.0(typescript@5.9.3)
transitivePeerDependencies:
- typescript
- ws
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)':
dependencies:
'@welshman/lib': 0.6.8
'@welshman/net': 0.6.8(typescript@5.9.3)(ws@8.18.3)
'@welshman/util': 0.6.8(typescript@5.9.3)
svelte: 4.2.20
transitivePeerDependencies:
- typescript
- ws
'@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))':
dependencies:
'@noble/curves': 1.9.7
'@types/ws': 8.18.1
'@welshman/lib': 0.6.8
'@welshman/lib': 0.8.1
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
transitivePeerDependencies:
- typescript
'@xml-tools/parser@1.0.11':
dependencies:
@@ -6803,12 +6735,6 @@ snapshots:
add-stream@1.0.0: {}
agent-base@6.0.2:
dependencies:
debug: 4.4.3
transitivePeerDependencies:
- supports-color
aggregate-error@3.1.0:
dependencies:
clean-stack: 2.2.0
@@ -7121,14 +7047,6 @@ snapshots:
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:
dependencies:
color-name: 1.1.3
@@ -7770,10 +7688,6 @@ snapshots:
estree-walker@2.0.2: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
esutils@2.0.3: {}
events-universal@1.0.1:
@@ -8077,6 +7991,8 @@ snapshots:
dependencies:
has-symbols: 1.1.0
hash-wasm@4.12.0: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@@ -8089,13 +8005,6 @@ snapshots:
dependencies:
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: {}
ico-endec@0.1.6: {}
@@ -8625,7 +8534,7 @@ snapshots:
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:
'@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
light-bolt11-decoder: 3.2.0
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-model: 1.25.3
prosemirror-state: 1.4.3
@@ -8657,7 +8566,7 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
nostr-tools@2.17.0(typescript@5.9.3):
nostr-tools@2.19.4(typescript@5.9.3):
dependencies:
'@noble/ciphers': 0.5.3
'@noble/curves': 1.2.0
@@ -8797,12 +8706,6 @@ snapshots:
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: {}
picomatch@2.3.1: {}
@@ -8917,8 +8820,6 @@ snapshots:
process-nextick-args@2.0.1: {}
progress@2.0.3: {}
prompts@2.4.2:
dependencies:
kleur: 3.0.3
@@ -9027,8 +8928,6 @@ snapshots:
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
proxy-from-env@1.1.0: {}
pump@3.0.3:
dependencies:
end-of-stream: 1.4.5
@@ -9584,23 +9483,6 @@ snapshots:
optionalDependencies:
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:
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -10270,3 +10152,7 @@ snapshots:
yocto-queue@0.1.0: {}
zimmerframe@1.1.4: {}
zod@3.25.76: {}
zod@4.3.5: {}
-15
View File
@@ -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
View File
@@ -66,6 +66,10 @@
--neutral-content: oklch(var(--nc));
}
.mobile [data-tip]::before {
display: none !important;
}
/* safe area insets */
@layer components {
@@ -392,6 +396,16 @@ progress[value]::-webkit-progress-value {
@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__compose {
@@ -399,5 +413,5 @@ progress[value]::-webkit-progress-value {
}
.chat__scroll-down {
@apply fixed bottom-28 right-4 md:bottom-16;
@apply fixed bottom-28 right-4 z-feature md:bottom-16;
}
+6 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
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 type {Filter} from "@welshman/util"
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
@@ -13,7 +13,7 @@
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.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 {createAlert} from "@app/core/commands"
import {canSendPushNotifications} from "@app/util/push"
@@ -37,7 +37,7 @@
hideSpaceField = false,
}: Props = $props()
const timezoneOffset = parseInt(TIMEZONE.slice(3)) / 100
const timezoneOffset = parseInt(TIMEZONE.split(":")?.[0] || "00")
const minute = randomInt(0, 59)
const hour = (17 - timezoneOffset) % 24
const WEEKLY = `0 ${minute} ${hour} * * 1`
@@ -45,7 +45,9 @@
let loading = $state(false)
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()
+7 -7
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import {sleep} from "@welshman/lib"
import {getTagValue, getAddress} from "@welshman/util"
import {sleep, filter} from "@welshman/lib"
import {getTagValue, getAddress, RelayMode} from "@welshman/util"
import {isRelayFeed, findFeed} from "@welshman/feeds"
import {getPubkeyRelays, pubkey} from "@welshman/app"
import Inbox from "@assets/icons/inbox.svg?dataurl"
import Bell from "@assets/icons/bell.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
@@ -12,10 +13,9 @@
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {
alerts,
dmAlert,
alertsById,
deriveAlertStatus,
userInboxRelays,
getAlertFeed,
userSettingsValues,
} from "@app/core/state"
@@ -33,7 +33,7 @@
const dmStatus = $derived($dmAlert ? deriveAlertStatus(getAddress($dmAlert.event)) : undefined)
const filteredAlerts = $derived(
$alerts.filter(alert => {
filter(alert => {
const feed = getAlertFeed(alert)
// Skip non-feeds and DM alerts
@@ -43,7 +43,7 @@
if (url) return findFeed(feed, f => isRelayFeed(f) && f.includes(url))
return true
}),
}, $alertsById.values()),
)
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
@@ -59,7 +59,7 @@
if ($dmAlert) {
deleteAlert($dmAlert)
} else {
if ($userInboxRelays.length === 0) {
if (getPubkeyRelays($pubkey!, RelayMode.Messaging).length === 0) {
return uncheckDmAlert("Please set up your messaging relays before enabling alerts.")
}
+1 -20
View File
@@ -5,32 +5,13 @@
import Landing from "@app/components/Landing.svelte"
import Toast from "@app/components/Toast.svelte"
import PrimaryNav from "@app/components/PrimaryNav.svelte"
import EmailConfirm from "@app/components/EmailConfirm.svelte"
import PasswordReset from "@app/components/PasswordReset.svelte"
import {BURROW_URL} from "@app/core/state"
import {modals, pushModal} from "@app/util/modal"
import {modals} from "@app/util/modal"
interface Props {
children: Snippet
}
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>
<div class="flex h-screen overflow-hidden">
+35 -48
View File
@@ -28,8 +28,8 @@
tagPubkey,
sendWrapped,
mergeThunks,
loadInboxRelaySelections,
inboxRelaySelectionsByPubkey,
loadMessagingRelayList,
messagingRelayListsByPubkey,
} from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -43,36 +43,31 @@
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.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 ChatCompose from "@app/components/ChatCompose.svelte"
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte"
import {
INDEXER_RELAYS,
userSettingsValues,
deriveChat,
splitChatId,
PLATFORM_NAME,
} from "@app/core/state"
import {INDEXER_RELAYS, userSettingsValues, PLATFORM_NAME, deriveChat} from "@app/core/state"
import {pushModal} from "@app/util/modal"
import {prependParent} from "@app/core/commands"
import {pushToast} from "@app/util/toast"
type Props = {
id: string
pubkeys: string[]
info?: Snippet
}
const {id, info}: Props = $props()
const {pubkeys, info}: Props = $props()
const chat = deriveChat(id)
const pubkeys = splitChatId(id)
const chat = deriveChat(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 = () =>
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) => {
parent = event
@@ -183,7 +178,7 @@
onMount(() => {
for (const pubkey of others) {
loadInboxRelaySelections(pubkey, INDEXER_RELAYS, true)
loadMessagingRelayList(pubkey, INDEXER_RELAYS, true)
}
const observer = new ResizeObserver(() => {
@@ -208,19 +203,17 @@
<PageBar>
{#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}
<div class="row-2">
<ProfileCircle pubkey={$pubkey!} size={5} />
<ProfileName pubkey={$pubkey!} />
</div>
{:else if others.length === 1}
{@const pubkey = others[0]}
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
<Button onclick={onClick} class="row-2">
<ProfileCircle {pubkey} size={5} />
<ProfileName {pubkey} />
</Button>
<div class="row-2">
<ProfileCircle pubkey={others[0]} size={5} />
<ProfileName pubkey={others[0]} />
</div>
{:else}
<div class="flex items-center gap-2">
<ProfileCircles pubkeys={others} size={5} />
@@ -235,55 +228,49 @@
{/if}
</p>
</div>
{#if others.length > 2}
<Button onclick={showMembers} class="btn btn-link hidden sm:block"
>Show all members</Button>
{/if}
{/if}
</div>
</Button>
{/snippet}
{#snippet action()}
<div>
{#if remove($pubkey, missingInboxes).length > 0}
{@const count = remove($pubkey, missingInboxes).length}
{@const label = count > 1 ? "inboxes are" : "inbox is"}
<div
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
data-tip="{count} {label} not configured.">
<Icon icon={Danger} />
{count}
</div>
{/if}
</div>
{#if remove($pubkey, missingRelayLists).length > 0}
{@const count = remove($pubkey, missingRelayLists).length}
{@const label = count > 1 ? "lists are" : "list is"}
<div
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
data-tip="{count} messaging {label} not configured.">
<Icon icon={Danger} />
{count}
</div>
{/if}
{/snippet}
</PageBar>
<PageContent class="flex flex-col-reverse gap-2 pt-4">
<div bind:this={dynamicPadding}></div>
{#if missingInboxes.includes($pubkey!)}
{#if missingRelayLists.includes($pubkey!)}
<div class="py-12">
<div class="card2 col-2 m-auto max-w-md items-center text-center">
<p class="row-2 text-lg text-error">
<Icon icon={Danger} />
Your inbox is not configured.
Your messaging relays are not configured.
</p>
<p>
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>
</div>
</div>
{:else if missingInboxes.length > 0}
{:else if missingRelayLists.length > 0}
<div class="py-12">
<div class="card2 col-2 m-auto max-w-md items-center text-center">
<p class="row-2 text-lg text-error">
<Icon icon={Danger} />
{missingInboxes.length}
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured.
{missingRelayLists.length} messaging
{missingRelayLists.length > 1 ? "lists are" : "list is"} not configured.
</p>
<p>
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>
</div>
</div>
+8 -5
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import {onMount} from "svelte"
import {page} from "$app/stores"
import {remove} from "@welshman/lib"
import {remove, uniq, formatTimestamp} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {pubkey, loadInboxRelaySelections} from "@welshman/app"
import {pubkey, loadMessagingRelayList} from "@welshman/app"
import {fade} from "@lib/transition"
import Link from "@lib/components/Link.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
@@ -21,13 +21,13 @@
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 path = makeChatPath(props.pubkeys)
onMount(() => {
for (const pk of others) {
loadInboxRelaySelections(pk)
loadMessagingRelayList(pk)
}
})
</script>
@@ -59,13 +59,16 @@
{/if}
</div>
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
<span class="opacity-50">
<span class="opacity-70">
{#if props.messages[0].pubkey === $pubkey}
You:
{/if}
</span>
{props.messages[0].content}
</p>
<p class="text-xs opacity-70">
{formatTimestamp(props.messages[0].created_at)}
</p>
</div>
</div>
</Link>
+25
View File
@@ -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>
+4 -3
View File
@@ -1,5 +1,6 @@
<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 Check from "@assets/icons/check.svg?dataurl"
import Bell from "@assets/icons/bell.svg?dataurl"
@@ -11,7 +12,7 @@
import {setChecked} from "@app/util/notifications"
import {pushModal} from "@app/util/modal"
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"
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
@@ -22,7 +23,7 @@
}
const enableAlerts = async () => {
if ($userInboxRelays.length === 0) {
if (getPubkeyRelays($pubkey!, RelayMode.Messaging).length === 0) {
return pushToast({
theme: "error",
message: "Please set up your messaging relays before enabling alerts.",
+10 -10
View File
@@ -46,20 +46,20 @@
</script>
<div class="col-2">
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
<Icon size={4} icon={SmileCircle} />
Send Reaction
</Button>
<Button class="btn btn-neutral w-full" onclick={sendReply}>
<Icon size={4} icon={Reply} />
Send Reply
<Button class="btn btn-neutral" onclick={showInfo}>
<Icon size={4} icon={Code2} />
Message Info
</Button>
<Button class="btn btn-neutral w-full" onclick={copyText}>
<Icon size={4} icon={Copy} />
Copy Text
</Button>
<Button class="btn btn-neutral" onclick={showInfo}>
<Icon size={4} icon={Code2} />
Message Details
<Button class="btn btn-neutral w-full" onclick={sendReply}>
<Icon size={4} icon={Reply} />
Send Reply
</Button>
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
<Icon size={4} icon={SmileCircle} />
Send Reaction
</Button>
</div>
+1 -2
View File
@@ -5,7 +5,6 @@
import {goto} from "$app/navigation"
import {tryCatch, uniq} from "@welshman/lib"
import {fromNostrURI} from "@welshman/util"
import {pubkey} from "@welshman/app"
import {preventDefault} from "@lib/html"
import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
@@ -19,7 +18,7 @@
const back = () => history.back()
const onSubmit = () => goto(makeChatPath([...pubkeys, $pubkey!]))
const onSubmit = () => goto(makeChatPath(pubkeys))
const addPubkey = (pubkey: string) => {
pubkeys = uniq([...pubkeys, pubkey])
+1 -1
View File
@@ -169,7 +169,7 @@
{#if isBlock(i)}
<ContentLinkBlock value={parsed.value} {event} />
{:else}
<ContentLinkInline value={parsed.value} />
<ContentLinkInline value={parsed.value} {event} />
{/if}
{:else if isProfile(parsed)}
<ContentMention value={parsed.value} {url} />
+13 -5
View File
@@ -1,17 +1,25 @@
<script lang="ts">
import {ellipsize, displayUrl, postJson} from "@welshman/lib"
import {dufflepud} from "@app/core/state"
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
import {isRelayUrl} from "@welshman/util"
import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
import {pushModal} from "@app/util/modal"
import {dufflepud, PLATFORM_URL} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
const {value, event} = $props()
let hideImage = $state(false)
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 json = await postJson(dufflepud("link/preview"), {url})
@@ -30,10 +38,10 @@
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
</script>
<Link external href={url} class="my-2 block">
<Link {external} {href} class="my-2 block">
<div class="overflow-hidden rounded-box">
{#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" />
</video>
{: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">
{#if preview.image && !hideImage}
<img
alt="Link preview"
alt=""
onerror={onError}
src={preview.image}
class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
@@ -21,7 +21,8 @@
.map(tagsFromIMeta)
.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 nonce = getTagValue("decryption-nonce", meta)
const algorithm = getTagValue("encryption-algorithm", meta)
+18 -6
View File
@@ -1,27 +1,39 @@
<script lang="ts">
import {displayUrl} from "@welshman/lib"
import {preventDefault} from "@lib/html"
import {call, displayUrl} from "@welshman/lib"
import {isRelayUrl} from "@welshman/util"
import {preventDefault, stopPropagation} from "@lib/html"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
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 [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>
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
<!-- 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" />
{displayUrl(url)}
</a>
{: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" />
{displayUrl(url)}
</Link>
+1 -1
View File
@@ -116,7 +116,7 @@
{:else if isCashu(parsed) || isInvoice(parsed)}
<ContentToken value={parsed.value} />
{:else if isLink(parsed)}
<ContentLinkInline value={parsed.value} />
<ContentLinkInline value={parsed.value} {event} />
{:else if isProfile(parsed)}
<ContentMention value={parsed.value} {url} />
{:else if isQuote(parsed)}
-52
View File
@@ -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>
+2 -2
View File
@@ -3,7 +3,7 @@
import {max, formatTimestampRelative} from "@welshman/lib"
import {COMMENT} from "@welshman/util"
import {load} from "@welshman/net"
import {deriveEvents} from "@welshman/store"
import {deriveArray, deriveEventsById} from "@welshman/store"
import type {TrustedEvent} from "@welshman/util"
import {repository} from "@welshman/app"
import {notifications} from "@app/util/notifications"
@@ -13,7 +13,7 @@
const {url, path, event}: {url: string; path: string; event: TrustedEvent} = $props()
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)))
onMount(() => {
+2 -2
View File
@@ -4,6 +4,7 @@
import {LOCALE, secondsToDate} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util"
import {tracker} from "@welshman/app"
import FileText from "@assets/icons/file-text.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl"
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
@@ -11,7 +12,6 @@
import FieldInline from "@lib/components/FieldInline.svelte"
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import {trackerStore} from "@app/core/state"
import {clip} from "@app/util/toast"
type Props = {
@@ -23,7 +23,7 @@
const relays = url ? [url] : Router.get().Event(event).getUrls()
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 json = JSON.stringify(event, null, 2)
const copyLink = () => clip(nevent1)
+36 -5
View File
@@ -3,21 +3,23 @@
import type {Snippet} from "svelte"
import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util"
import {COMMENT} from "@welshman/util"
import {pubkey, relaysByUrl} from "@welshman/app"
import {COMMENT, ManagementMethod} from "@welshman/util"
import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl"
import {setKey} from "@lib/implicit"
import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import Icon from "@lib/components/Icon.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 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 {pushToast} from "@app/util/toast"
import {makeSpaceChatPath} from "@app/util/routes"
type Props = {
@@ -31,8 +33,9 @@
const {url, noun, event, onClick, customActions}: Props = $props()
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})
@@ -47,6 +50,26 @@
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
onMount(() => {
@@ -84,5 +107,13 @@
Report Content
</Button>
</li>
{#if $userIsAdmin}
<li>
<Button class="text-error" onclick={showAdminDelete}>
<Icon size={4} icon={TrashBin2} />
Delete {noun}
</Button>
</li>
{/if}
{/if}
</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>
+9 -6
View File
@@ -2,7 +2,7 @@
import {now, DAY, uniq, sum} from "@welshman/lib"
import type {Zap, TrustedEvent} 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 Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -16,11 +16,14 @@
const {url, event, ...props}: Props = $props()
const zaps = deriveEventsMapped<Zap>(repository, {
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
itemToEvent: item => item.response,
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
})
const zaps = deriveArray(
deriveItemsByKey<Zap>({
repository,
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 zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
+3 -3
View File
@@ -7,13 +7,13 @@
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.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 {pushModal} from "@app/util/modal"
const back = () => history.back()
const startEject = () => pushModal(ProfileEject)
const startRecoveryRequest = () => pushModal(KeyRecoveryRequest)
</script>
<div class="column gap-4">
@@ -44,7 +44,7 @@
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button class="btn btn-primary" onclick={startEject}>
<Button class="btn btn-primary" onclick={startRecoveryRequest}>
<Icon icon={CheckCircle} />
I want to hold my own keys
</Button>
+160
View File
@@ -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>
+27 -23
View File
@@ -4,24 +4,28 @@
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
import {addSession, type Session, makeNip07Session, makeNip55Session} from "@welshman/app"
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 Compass from "@assets/icons/compass-big.svg?dataurl"
import Key from "@assets/icons/key.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import SignUp from "@app/components/SignUp.svelte"
import InfoNostr from "@app/components/InfoNostr.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 {PLATFORM_NAME, BURROW_URL} from "@app/core/state"
import {PLATFORM_NAME, POMADE_SIGNERS} from "@app/core/state"
import {pushToast} from "@app/util/toast"
import {setChecked} from "@app/util/notifications"
let signers: any[] = $state([])
let loading: string | undefined = $state()
const hasPomade = POMADE_SIGNERS.length >= 3
const disabled = $derived(loading ? true : undefined)
const signUp = () => pushModal(SignUp)
@@ -72,10 +76,12 @@
}
}
const loginWithPassword = () => pushModal(LogInPassword)
const loginWithEmail = () => pushModal(LogInEmail)
const loginWithBunker = () => pushModal(LogInBunker)
const loginWithKey = () => pushModal(LogInKey)
const hasSigner = $derived(getNip07() || signers.length > 0)
onMount(async () => {
@@ -112,39 +118,37 @@
Log in with {app.name}
</Button>
{/each}
{#if BURROW_URL && !hasSigner}
<Button {disabled} onclick={loginWithPassword} class="btn btn-primary">
{#if loading === "password"}
<span class="loading loading-spinner mr-3"></span>
{:else}
<Icon icon={Key} />
{/if}
Log in with Password
{#if hasPomade && !hasSigner}
<Button {disabled} onclick={loginWithEmail} class="btn btn-primary">
<Icon icon={Letter} />
Log in with Email
</Button>
{/if}
<Button
onclick={loginWithBunker}
{disabled}
class="btn {hasSigner || BURROW_URL ? 'btn-neutral' : 'btn-primary'}">
class="btn {hasSigner || hasPomade ? 'btn-neutral' : 'btn-primary'}">
<Icon icon={Cpu} />
Log in with Remote Signer
</Button>
{#if BURROW_URL && hasSigner}
<Button {disabled} onclick={loginWithPassword} class="btn">
{#if loading === "password"}
<span class="loading loading-spinner mr-3"></span>
{:else}
<Icon icon={Key} />
{/if}
Log in with Password
{#if hasPomade && hasSigner}
<Button {disabled} onclick={loginWithEmail} class="btn">
<Icon icon={Letter} />
Log in with Email
</Button>
{/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
external
{disabled}
href="https://nostrapps.com#signers"
class="btn {hasSigner || BURROW_URL ? '' : 'btn-neutral'}">
class="btn {hasSigner || hasPomade ? '' : 'btn-neutral'}">
<Icon icon={Compass} />
Browse Signer Apps
</Link>
+2 -1
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import {onMount, onDestroy} from "svelte"
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 {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte"
+116
View File
@@ -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>
+128
View File
@@ -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>
@@ -1,24 +1,24 @@
<script lang="ts">
import {postJson, sleep} from "@welshman/lib"
import {Client} from "@pomade/core"
import {preventDefault} from "@lib/html"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.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 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 {pushToast} from "@app/util/toast"
import {BURROW_URL} from "@app/core/state"
interface Props {
email: string
email?: string
}
let {email = $bindable()}: Props = $props()
let {email = $bindable("")}: Props = $props()
const back = () => history.back()
@@ -26,16 +26,15 @@
loading = true
try {
const [res] = await Promise.all([
postJson(BURROW_URL + "/user/request-reset", {email}),
sleep(1000),
])
const {ok, peersByPrefix} = await Client.requestChallenge(email)
if (res.error) {
pushToast({message: res.error, theme: "error"})
if (ok) {
pushModal(LogInOTPConfirm, {email, peersByPrefix})
} else {
pushToast({message: `Password reset email has been sent!`})
pushModal(LogInPassword, {email}, {path: "/"})
pushToast({
theme: "error",
message: "Sorry, we were unable to request a login code.",
})
}
} finally {
loading = false
@@ -48,30 +47,31 @@
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
<ModalHeader>
{#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}
</ModalHeader>
<FieldInline disabled={loading}>
<FieldInline>
{#snippet label()}
<p>Email Address</p>
<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} class="grow" />
<Icon icon={Letter} />
<input bind:value={email} />
</label>
{/snippet}
{#snippet info()}
<p>You'll be sent an email with a password reset link.</p>
{/snippet}
</FieldInline>
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<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}>Request password reset link</Spinner>
<Button type="submit" class="btn btn-primary" disabled={loading || !email}>
<Spinner {loading}>Log in</Spinner>
<Icon icon={AltArrowRight} />
</Button>
</ModalFooter>
</form>
+110
View File
@@ -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>
-156
View File
@@ -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>
+1
View File
@@ -17,6 +17,7 @@
await logout()
window.location.href = "/"
} catch (e) {
console.error(e)
loading = false
}
}
+1
View File
@@ -34,6 +34,7 @@
target: element,
props: {
onClose: closeModals,
fullscreen: options.fullscreen,
children: createRawSnippet(() => ({
render: () => "<div></div>",
setup: (target: Element) => {
@@ -23,16 +23,16 @@
}
}
$effect(() => {
if ($notifications.size > notificationCount) {
playSound()
}
notificationCount = $notifications.size
})
onMount(() => {
audioElement.load()
notifications.subscribe(notifications => {
if (notifications.size > notificationCount) {
playSound()
}
notificationCount = notifications.size
})
})
</script>
+7 -14
View File
@@ -1,19 +1,16 @@
<script lang="ts">
import cx from "classnames"
import type {Snippet} from "svelte"
import * as nip19 from "nostr-tools/nip19"
import {formatTimestamp} from "@welshman/lib"
import {getListTags, getPubkeyTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {Router} from "@welshman/router"
import {userMutes} from "@welshman/app"
import Link from "@lib/components/Link.svelte"
import {userMuteList} from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Profile from "@app/components/Profile.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import {entityLink} from "@app/core/state"
import {goToEvent} from "@app/util/routes"
const {
event,
@@ -31,14 +28,11 @@
class?: string
} = $props()
const relays = Router.get().Event(event).getUrls()
const nevent = nip19.neventEncode({id: event.id, relays})
const ignoreMute = () => {
muted = false
}
let muted = $state(getPubkeyTagValues(getListTags($userMutes)).includes(event.pubkey))
let muted = $state(getPubkeyTagValues(getListTags($userMuteList)).includes(event.pubkey))
</script>
<div class="flex flex-col gap-2 {restProps.class}">
@@ -59,12 +53,11 @@
<Profile pubkey={event.pubkey} {url} />
{/if}
{/if}
<Link
external
href={entityLink(nevent)}
class={cx("text-sm opacity-75", {"text-xs": minimal})}>
<Button
class={cx("text-sm opacity-75", {"text-xs": minimal})}
onclick={() => goToEvent(event)}>
{formatTimestamp(event.created_at)}
</Link>
</Button>
</div>
{@render children()}
{/if}
@@ -3,7 +3,7 @@
import {sum} from "@welshman/lib"
import type {Zap, TrustedEvent} 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 Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -14,11 +14,14 @@
const content = getTagValue("summary", props.event.tags)
const fakeEvent = {content, tags: props.event.tags}
const zaps = deriveEventsMapped<Zap>(repository, {
filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
itemToEvent: item => item.response,
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
})
const zaps = deriveArray(
deriveItemsByKey<Zap>({
repository,
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 zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
-73
View File
@@ -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>
+130
View File
@@ -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}
+22 -16
View File
@@ -62,7 +62,7 @@
<PrimaryNavItemSpace {url} />
{:else}
<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>
<Divider />
{#each primarySpaceUrls as url (url)}
@@ -74,11 +74,11 @@
class="tooltip-right"
onclick={showOtherSpacesMenu}
notification={otherSpaceNotifications}>
<ImageIcon alt="Other Spaces" src={Widget} />
<ImageIcon alt="Other Spaces" src={Widget} size={8} />
</PrimaryNavItem>
{/if}
<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>
{/each}
</div>
@@ -91,17 +91,21 @@
href="/settings/profile"
prefix="/settings"
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
title="Messages"
onclick={openChat}
class="tooltip-right"
notification={$notifications.has("/chat")}>
<ImageIcon alt="Messages" src={Letter} size={7} />
<ImageIcon alt="Messages" src={Letter} size={8} />
</PrimaryNavItem>
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
<ImageIcon alt="Search" src={Magnifier} size={7} />
<ImageIcon alt="Search" src={Magnifier} size={8} />
</PrimaryNavItem>
</div>
</div>
@@ -110,32 +114,34 @@
{@render children?.()}
<!-- 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
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="flex gap-2 sm:gap-6">
<PrimaryNavItem title="Home" href="/home">
<ImageIcon alt="Home" src={HomeSmile} size={7} />
<ImageIcon alt="Home" src={HomeSmile} size={8} />
</PrimaryNavItem>
<PrimaryNavItem
title="Messages"
onclick={openChat}
notification={$notifications.has("/chat")}>
<ImageIcon alt="Messages" src={Letter} size={7} />
<ImageIcon alt="Messages" src={Letter} size={8} />
</PrimaryNavItem>
{#if PLATFORM_RELAYS.length !== 1}
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
<ImageIcon alt="Spaces" src={SettingsMinimalistic} size={7} />
<ImageIcon alt="Spaces" src={SettingsMinimalistic} size={8} />
</PrimaryNavItem>
{/if}
</div>
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
<ImageIcon
alt="Settings"
src={$userProfile?.picture || Settings}
size={7}
class="rounded-full" />
{#if $userProfile?.picture}
<ImageIcon alt="Settings" src={$userProfile?.picture} size={10} class="rounded-full" />
{:else}
<ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" />
{/if}
</PrimaryNavItem>
</div>
</div>
@@ -5,7 +5,11 @@
import {makeSpacePath, goToSpace} from "@app/util/routes"
import {notifications} from "@app/util/notifications"
const {url} = $props()
type Props = {
url: string
}
const {url}: Props = $props()
const onClick = () => goToSpace(url)
</script>
+7 -11
View File
@@ -3,17 +3,13 @@
import {load} from "@welshman/net"
import {Router} from "@welshman/router"
import type {Filter} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {deriveArray, deriveEventsById} from "@welshman/store"
import {formatTimestampRelative} from "@welshman/lib"
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 ProfileSpaces from "@app/components/ProfileSpaces.svelte"
import {
deriveGroupSelections,
getSpaceUrlsFromGroupSelections,
MESSAGE_KINDS,
} from "@app/core/state"
import {deriveGroupList, getSpaceUrlsFromGroupList, MESSAGE_KINDS} from "@app/core/state"
import {goToEvent} from "@app/util/routes"
import {pushModal} from "@app/util/modal"
@@ -24,9 +20,9 @@
const {pubkey, url}: Props = $props()
const filters: Filter[] = [{authors: [pubkey], limit: 1}]
const events = deriveEvents(repository, {filters})
const selections = deriveGroupSelections(pubkey)
const spaceUrls = $derived(getSpaceUrlsFromGroupSelections($selections))
const events = deriveArray(deriveEventsById({repository, filters}))
const groupList = deriveGroupList(pubkey)
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
const viewEvent = () => goToEvent($events[0]!)
@@ -34,7 +30,7 @@
onMount(async () => {
// 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({
+2 -2
View File
@@ -19,6 +19,6 @@
<ImageIcon
{size}
alt=""
class={cx(props.class, "rounded-full")}
src={$profile?.picture || UserRounded}
alt="Profile picture" />
src={$profile?.picture || UserRounded} />
+14 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import {getProfile, loadProfile} from "@welshman/app"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
type Props = {
@@ -7,11 +8,22 @@
}
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>
<div class="flex pr-3">
{#each pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
<div class="z-feature -mr-3 inline-block">
{#each visiblePubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
<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} />
</div>
{/each}
+4 -2
View File
@@ -7,8 +7,9 @@
DELETE,
isReplaceable,
getAddress,
RelayMode,
} from "@welshman/util"
import {pubkey, publishThunk, repository} from "@welshman/app"
import {pubkey, publishThunk, repository, derivePubkeyRelays} 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"
@@ -19,12 +20,13 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {pushToast} from "@app/util/toast"
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 confirmText = $state("")
const CONFIRM_TEXT = "permanently delete my nostr account"
const userWriteRelays = derivePubkeyRelays($pubkey!, RelayMode.Write)
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
const showProgress = $derived(progress !== undefined)
+82 -4
View File
@@ -1,19 +1,29 @@
<script lang="ts">
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 Code2 from "@assets/icons/code-2.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 ImageIcon from "@lib/components/ImageIcon.svelte"
import Link from "@lib/components/Link.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import Button from "@lib/components/Button.svelte"
import Popover from "@lib/components/Popover.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Profile from "@app/components/Profile.svelte"
import ProfileInfo from "@app/components/ProfileInfo.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import ProfileBadges from "@app/components/ProfileBadges.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 {pushToast} from "@app/util/toast"
import {makeChatPath} from "@app/util/routes"
export type Props = {
@@ -23,15 +33,83 @@
const {pubkey, url}: Props = $props()
const profile = deriveProfile(pubkey, removeUndefined([url]))
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const back = () => history.back()
const chatPath = makeChatPath([pubkey])
const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event})
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>
<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} />
<ProfileBadges {pubkey} {url} />
<ModalFooter>
@@ -41,7 +119,7 @@
</Button>
<div class="flex gap-2">
<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
</Link>
<Button onclick={openChat} class="btn btn-primary">
+1 -1
View File
@@ -7,7 +7,7 @@
import {clearModals} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state"
import {updateProfile} from "../core/commands"
import {updateProfile} from "@app/core/commands"
const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || [])
-111
View File
@@ -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>
+6 -8
View File
@@ -2,8 +2,6 @@
import type {Snippet} from "svelte"
import {load} from "@welshman/net"
import {NOTE} from "@welshman/util"
import {fly} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
interface Props {
@@ -24,16 +22,16 @@
<div class="col-4">
<div class="flex flex-col gap-2">
{#await events}
<p class="center my-12 flex">
<Spinner loading />
<p class="center flex min-h-6">
<span class="loading loading-spinner"></span>
</p>
{:then events}
{#each events as event (event.id)}
<div in:fly>
<NoteItem {url} {event} />
</div>
<NoteItem {url} {event} />
{:else}
{@render fallback?.()}
<div class="min-h-6">
{@render fallback?.()}
</div>
{/each}
{/await}
</div>
@@ -1,4 +1,5 @@
<script lang="ts">
import * as nip19 from "nostr-tools/nip19"
import {writable} from "svelte/store"
import type {Writable} from "svelte/store"
import {type Instance} from "tippy.js"
@@ -35,6 +36,26 @@
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) => {
if (instance.onKeyDown(e)) {
e.preventDefault()
@@ -80,6 +101,7 @@
type="text"
placeholder="Search for profiles..."
bind:value={$term}
oninput={onInput}
onkeydown={onKeyDown} />
</label>
<Tippy
+3 -3
View File
@@ -8,7 +8,7 @@
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte"
import {makeSpacePath} from "@app/util/routes"
import {deriveGroupSelections, getSpaceUrlsFromGroupSelections} from "@app/core/state"
import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/core/state"
type Props = {
pubkey: string
@@ -16,8 +16,8 @@
const {pubkey}: Props = $props()
const selections = deriveGroupSelections(pubkey)
const spaceUrls = $derived(getSpaceUrlsFromGroupSelections($selections))
const groupList = deriveGroupList(pubkey)
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
const back = () => history.back()
</script>
+1 -1
View File
@@ -21,7 +21,7 @@
const canvasRect = canvas.getBoundingClientRect()
scale = wrapperRect.width / (canvasRect.width * 10)
height = canvasRect.width * 10 * scale
height = canvasRect.height * 10 * scale
}
})
</script>
+22 -19
View File
@@ -2,7 +2,7 @@
import cx from "classnames"
import {onMount} 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 {
REPORT,
REACTION,
@@ -15,14 +15,14 @@
DELETE,
} 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 {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app"
import {isMobile, preventDefault, stopPropagation} from "@lib/html"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.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 {pushModal} from "@app/util/modal"
@@ -46,19 +46,22 @@
children,
}: Props = $props()
const reports = deriveEvents(repository, {
filters: [{kinds: [REPORT], "#e": [event.id]}],
})
const reports = deriveArray(
deriveEventsById({repository, filters: [{kinds: [REPORT], "#e": [event.id]}]}),
)
const reactions = deriveEvents(repository, {
filters: [{kinds: [REACTION], "#e": [event.id]}],
})
const reactions = deriveArray(
deriveEventsById({repository, filters: [{kinds: [REACTION], "#e": [event.id]}]}),
)
const zaps = deriveEventsMapped<Zap>(repository, {
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
itemToEvent: item => item.response,
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
})
const zaps = deriveArray(
deriveItemsByKey<Zap>({
repository,
getKey: zap => zap.response.id,
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
}),
)
const onReactionClick = (events: TrustedEvent[]) => {
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 groupedReactions = $derived(
groupBy(
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(() => {
const controller = new AbortController()
@@ -137,7 +140,7 @@
data-tip={tooltip}
class={cx(
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,
"border-neutral-content/20": !isOwn,
+5 -5
View File
@@ -14,8 +14,8 @@
const relay = deriveRelay(url)
</script>
<ImageIcon
{size}
src={$relay?.icon || RemoteControllerMinimalistic}
alt="Relay image"
class={props.class} />
{#if $relay?.icon}
<ImageIcon {size} alt="" src={$relay?.icon} class={props.class} />
{:else}
<ImageIcon size={size - 2} alt="" src={RemoteControllerMinimalistic} class={props.class} />
{/if}
+7 -13
View File
@@ -1,21 +1,19 @@
<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 Icon from "@lib/components/Icon.svelte"
import RelayName from "@app/components/RelayName.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
import {deriveSpaceMembers, deriveUserRooms} from "@app/core/state"
import {deriveGroupListPubkeys, deriveUserRooms} from "@app/core/state"
type Props = {
url: string
}
const {url}: Props = $props()
const relay = deriveRelay(url)
const rooms = deriveUserRooms(url)
const members = deriveSpaceMembers(url)
const favorited = deriveGroupListPubkeys(url)
</script>
<div class="col-4 text-left">
@@ -25,11 +23,7 @@
<div class="avatar relative">
<div
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}
<img alt="" src={$relay.icon} />
{:else}
<Icon icon={Ghost} size={5} />
{/if}
<RelayIcon {url} />
</div>
</div>
{#if $rooms.includes(url)}
@@ -49,10 +43,10 @@
</div>
<RelayDescription {url} />
</div>
{#if $members.length > 0}
{#if $favorited.size > 0}
<div class="row-2 card2 card2-sm bg-alt">
Members:
<ProfileCircles pubkeys={$members} />
Favorited By:
<ProfileCircles pubkeys={Array.from($favorited)} />
</div>
{/if}
</div>
+46
View File
@@ -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>
+93
View File
@@ -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>
+132
View File
@@ -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>
+7 -11
View File
@@ -18,7 +18,7 @@
import Confirm from "@lib/components/Confirm.svelte"
import ModalFooter from "@lib/components/ModalFooter.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 RoomName from "@app/components/RoomName.svelte"
import RoomImage from "@app/components/RoomImage.svelte"
@@ -67,12 +67,7 @@
const leave = () => handleLoading(leaveRoom)
const showMembers = () =>
pushModal(ProfileList, {
title: "Members",
subtitle: `of ${$room?.name || h}`,
pubkeys: $members,
})
const showMembers = () => pushModal(RoomMembers, {url, h})
const startDelete = () =>
pushModal(Confirm, {
@@ -139,11 +134,12 @@
<p>{$room.about}</p>
{/if}
{#if $members.length > 0}
<div class="card2 card2-sm bg-alt flex gap-4">
<span>Members:</span>
<Button onclick={showMembers}>
<div class="card2 card2-sm bg-alt flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<span>Members:</span>
<ProfileCircles pubkeys={$members} />
</Button>
</div>
<Button class="btn btn-neutral btn-sm" onclick={showMembers}>View All</Button>
</div>
{/if}
<ModalFooter>
+51 -76
View File
@@ -6,7 +6,7 @@
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.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 Icon from "@lib/components/Icon.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte"
@@ -30,7 +30,7 @@
const room = $state.snapshot(values)
if (imageFile) {
const {error, result} = await uploadFile(imageFile)
const {error, result} = await uploadFile(imageFile, {maxWidth: 128, maxHeight: 128})
if (error) {
return pushToast({theme: "error", message: error})
@@ -38,8 +38,6 @@
room.picture = result.url
room.pictureMeta = result.tags
} else if (selectedIcon) {
room.picture = selectedIcon
}
const createMessage = await waitForThunkError(createRoom(url, room))
@@ -76,29 +74,34 @@
let loading = $state(false)
let imageFile = $state<File | undefined>()
let imagePreview = $state(initialValues.picture)
let selectedIcon = $state<string | undefined>()
const handleImageUpload = async (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (file && file.type.startsWith("image/")) {
selectedIcon = undefined
imageFile = await compressFile(file, {maxWidth: 64, maxHeight: 64})
const reader = new FileReader()
reader.onload = e => {
imageFile = file
imagePreview = e.target?.result as string
}
reader.readAsDataURL(imageFile)
reader.readAsDataURL(file)
}
}
const handleIconSelect = (iconUrl: string) => {
imageFile = undefined
imagePreview = undefined
selectedIcon = iconUrl
imagePreview = iconUrl
const parts = iconUrl.split(",")
const imageData = atob(parts[1])
const result = new Uint8Array(imageData.length)
for (let n = 0; n < imageData.length; n++) {
result[n] = imageData.charCodeAt(n)
}
imageFile = new File([result], `icon.svg`, {type: "image/svg+xml"})
}
</script>
@@ -109,32 +112,25 @@
<p>Icon</p>
{/snippet}
{#snippet input()}
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
{#if imagePreview}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<ImageIcon src={imagePreview} alt="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 class="flex flex-grow items-center justify-between gap-4">
{#if imagePreview}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<ImageIcon src={imagePreview} alt="" class="rounded-lg" />
</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>
{/snippet}
@@ -146,9 +142,7 @@
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
{#if imagePreview}
<ImageIcon src={imagePreview} alt="Room icon preview" />
{:else if selectedIcon}
<Icon icon={selectedIcon} class="h-8 w-8" />
<ImageIcon src={imagePreview} alt="" class="rounded-lg" />
{:else}
<Icon icon={Hashtag} />
{/if}
@@ -166,41 +160,22 @@
</label>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<strong>Restricted</strong>
{/snippet}
{#snippet input()}
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
<span class="text-sm opacity-75">Only allow members to send messages</span>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<strong>Private</strong>
{/snippet}
{#snippet input()}
<input type="checkbox" class="checkbox" bind:checked={values.isPrivate} />
<span class="text-sm opacity-75">Only allow members to read messages</span>
{/snippet}
</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>
<strong class="md:hidden">Permissions</strong>
<div class="flex items-center gap-2">
<input type="checkbox" class="checkbox" bind:checked={values.isRestricted} />
<span class="text-sm opacity-75">Only allow members to send messages</span>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" class="checkbox" bind:checked={values.isPrivate} />
<span class="text-sm opacity-75">Only allow members to read messages</span>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" class="checkbox" bind:checked={values.isHidden} />
<span class="text-sm opacity-75">Hide this group from non-members</span>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
<span class="text-sm opacity-75">Ignore requests to join</span>
</div>
{@render footer({loading})}
</form>
+1 -1
View File
@@ -16,7 +16,7 @@
</script>
{#if $room.picture}
<ImageIcon src={$room.picture} {size} alt="Room icon" />
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
{:else}
<Icon icon={Hashtag} {size} />
{/if}
+37 -3
View File
@@ -1,15 +1,19 @@
<script lang="ts">
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 TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import Confirm from "@lib/components/Confirm.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 {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {deriveUserIsSpaceAdmin} from "@app/core/state"
type Props = {
url: string
@@ -19,9 +23,11 @@
const {url, event, onClick}: Props = $props()
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const report = () => {
onClick()
pushModal(EventReport, {url, event})
pushModal(Report, {url, event})
}
const showInfo = () => {
@@ -33,6 +39,26 @@
onClick()
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>
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md">
@@ -56,5 +82,13 @@
Report Content
</Button>
</li>
{#if $userIsAdmin}
<li>
<Button class="text-error" onclick={showAdminDelete}>
<Icon size={4} icon={TrashBin2} />
Delete Message
</Button>
</li>
{/if}
{/if}
</ul>
+12 -12
View File
@@ -58,12 +58,12 @@
{#if event.pubkey === $pubkey}
<Button class="btn btn-neutral text-error" onclick={showDelete}>
<Icon size={4} icon={TrashBin2} />
Delete
Delete Message
</Button>
{/if}
<Button class="btn btn-neutral" onclick={showInfo}>
<Icon size={4} icon={Code2} />
Show JSON
Message Info
</Button>
{#if path}
<Link class="btn btn-neutral" href={path}>
@@ -71,18 +71,18 @@
View Details
</Link>
{/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}
<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} />
Zap
Send Zap
</ZapButton>
{/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>
+109
View File
@@ -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>
+77
View File
@@ -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>
+84 -68
View File
@@ -1,95 +1,111 @@
<script lang="ts">
import {postJson} from "@welshman/lib"
import {preventDefault} from "@lib/html"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import type {ClientOptions} from "@pomade/core"
import type {Profile} from "@welshman/util"
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 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 FieldInline from "@lib/components/FieldInline.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 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 SignUpSuccess from "@app/components/SignUpSuccess.svelte"
import {pushModal} from "@app/util/modal"
import {BURROW_URL, PLATFORM_NAME} from "@app/core/state"
import {pushToast} from "@app/util/toast"
import SignUpComplete from "@app/components/SignUpComplete.svelte"
import {setChecked} from "@app/util/notifications"
import {pushModal, clearModals} from "@app/util/modal"
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 signupPassword = async () => {
loading = true
const completeSignup = () => {
// Add default outbox/inbox relays
publishThunk({
event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}),
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS],
})
try {
const res = await postJson(BURROW_URL + "/user", {email, password})
// Add default messaging relays
publishThunk({
event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}),
relays: DEFAULT_RELAYS,
})
if (res.error) {
pushToast({message: res.error, theme: "error"})
} else {
pushModal(SignUpSuccess, {email}, {replaceState: true})
}
} finally {
loading = false
}
// Save the user's profile
initProfile(getKey<Profile>("signup.profile")!)
// Don't show any notifications for old content
setChecked("*")
// Go to the dashboard
clearModals()
}
const usePassword = () => {
if (BURROW_URL) {
signupPassword()
}
const flows = {
email: {
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>
<form class="column gap-4" onsubmit={preventDefault(usePassword)}>
<div class="column gap-4">
<h1 class="heading">Sign up with Nostr</h1>
<p class="m-auto max-w-sm text-center">
{PLATFORM_NAME} is built using the
<Button class="link" onclick={() => pushModal(InfoNostr)}>nostr protocol</Button>, which gives
users control over their digital identity using <strong>cryptographic key pairs</strong>.
</p>
{#if BURROW_URL}
<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>
<Button type="submit" class="btn btn-primary" disabled={loading || !email || !password}>
<Spinner {loading}>Sign Up</Spinner>
<Icon icon={AltArrowRight} />
{#if hasPomade}
<Button onclick={flows.email.start} class="btn btn-primary">
<Icon icon={Letter} />
Sign up with email
</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}
<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} />
Generate a key
</Button>
@@ -97,4 +113,4 @@
Already have an account?
<Button class="link" onclick={login}>Log in instead</Button>
</div>
</form>
</div>
+2 -25
View File
@@ -1,7 +1,4 @@
<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 AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
@@ -9,34 +6,14 @@
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {clearModals} from "@app/util/modal"
import {PROTECTED} from "@app/core/state"
type Props = {
secret: string
profile: Profile
next: () => void
}
const {secret, profile}: Props = $props()
const {next}: Props = $props()
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>
<form class="column gap-4" onsubmit={preventDefault(next)}>
+144
View File
@@ -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>
+6 -160
View File
@@ -1,168 +1,14 @@
<script lang="ts">
import {nsecEncode} from "nostr-tools/nip19"
import {encrypt} from "nostr-tools/nip49"
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"
import {getKey} from "@lib/implicit"
import KeyDownload from "@app/components/KeyDownload.svelte"
type Props = {
profile: Profile
next: () => void
}
const {profile}: Props = $props()
const {next}: Props = $props()
const secret = makeSecret()
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)
const secret = getKey<string>("signup.secret")!
</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">
Continue
<Icon icon={AltArrowRight} />
</Button>
</ModalFooter>
</form>
<KeyDownload {secret} {next} />
+13 -7
View File
@@ -1,23 +1,29 @@
<script lang="ts">
import type {Profile} from "@welshman/util"
import {makeProfile} from "@welshman/util"
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 Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
import SignUpKey from "@app/components/SignUpKey.svelte"
import {pushModal} from "@app/util/modal"
const initialValues = {
profile: makeProfile(),
shouldBroadcast: false,
type Props = {
next: () => void
}
const {next}: Props = $props()
const profile = getKey<Profile>("signup.profile")!
const initialValues = {profile, shouldBroadcast: false}
const back = () => history.back()
const onsubmit = (values: {profile: Profile}) => pushModal(SignUpKey, values)
const onsubmit = ({profile}: {profile: Profile}) => {
setKey("signup.profile", profile)
next()
}
</script>
<div class="flex flex-col gap-4">
-18
View File
@@ -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>
+24 -16
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {spec, prop, avg} from "@welshman/lib"
import {session, SessionMethod, signerLog, SignerLogEntryStatus} from "@welshman/app"
import {spec, avg} from "@welshman/lib"
import {session, SessionMethod, signerLog} from "@welshman/app"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
@@ -9,18 +9,20 @@
import Button from "@lib/components/Button.svelte"
import LogOut from "@app/components/LogOut.svelte"
import {pushModal} from "@app/util/modal"
import PomadeSessions from "@app/components/PomadeSessions.svelte"
const {Pending, Success, Failure} = SignerLogEntryStatus
const pending = $derived($signerLog.filter(spec({status: Pending})).length)
const success = $derived($signerLog.filter(spec({status: Success})).length)
const failure = $derived($signerLog.filter(spec({status: Failure})).length)
const recent = $derived($signerLog.slice(-10))
const recentAvg = $derived(avg(recent.map(prop("duration"))))
const recentPending = $derived(recent.filter(spec({status: Pending})).length)
const recentSuccess = $derived(recent.filter(spec({status: Success})).length)
const recentFailure = $derived(recent.filter(spec({status: Failure})).length)
const finished = $derived($signerLog.filter(x => x.finished_at))
const pending = $derived($signerLog.filter(x => !x.finished_at))
const failure = $derived(finished.filter(spec({ok: false})))
const success = $derived(finished.filter(spec({ok: true})))
const recent = $derived($signerLog.filter(x => x.started_at < Date.now() - 5000).slice(-10))
const recentFinished = $derived(recent.filter(x => x.finished_at))
const recentPending = $derived(recent.filter(x => !x.finished_at))
const recentAvg = $derived(avg(recentFinished.map(x => x.finished_at! - x.started_at)))
const recentFailure = $derived(recentFinished.filter(x => !x.ok))
const recentSuccess = $derived(recentFinished.filter(x => x.ok))
const isDisconnected = $derived(
recent.length > 0 && recentFailure + recentPending === recent.length,
recent.length > 0 && recentFailure.length + recentPending.length === recent.length,
)
const logout = () => pushModal(LogOut)
@@ -34,11 +36,13 @@
<span class="flex items-center gap-2">
{#if isDisconnected}
<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
{:else if recentAvg > 1000 || recentPending > 3}
{:else if recentAvg > 1000 || recentPending.length > 3}
<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
{/if}
</span>
@@ -56,15 +60,19 @@
{$session.signer}
{:else if $session.method === SessionMethod.Pubkey}
public key (readonly)
{:else if $session.method === SessionMethod.Pomade}
email and password
{/if}
</p>
<p>
{success} requests succeeded, {failure} failed, {pending} pending
{success.length} requests succeeded, {failure.length} failed, {pending.length} pending
</p>
</div>
</div>
{#if isDisconnected}
<Button class="btn btn-outline btn-error" onclick={logout}>Logout to Reconnect</Button>
{:else}
<PomadeSessions />
{/if}
</div>
{/if}
+8 -4
View File
@@ -10,10 +10,10 @@
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
import SpaceJoinConfirm, {confirmSpaceJoin} from "@app/components/SpaceJoinConfirm.svelte"
import {attemptRelayAccess} from "@app/core/commands"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
const {url} = $props()
@@ -21,7 +21,7 @@
const next = async () => {
if (error) {
return pushToast({theme: "error", message: error, timeout: 30_000})
return pushModal(SpaceAccessRequest, {url})
}
if (Pool.get().get(url).auth.status === AuthStatus.None) {
@@ -35,7 +35,7 @@
let loading = $state(true)
onMount(async () => {
;[error] = await Promise.all([attemptRelayAccess(url), sleep(3000)])
;[error] = await Promise.all([attemptRelayAccess(url), sleep(1000)])
loading = false
})
</script>
@@ -77,7 +77,11 @@
Go back
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
Join Space
{#if error}
Request Access
{:else}
Join Space
{/if}
<Icon icon={AltArrowRight} />
</Button>
</ModalFooter>
+28 -32
View File
@@ -6,12 +6,12 @@
import Pen from "@assets/icons/pen.svg?dataurl"
import ShieldUser from "@assets/icons/shield-user.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 Link from "@lib/components/Link.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Button from "@lib/components/Button.svelte"
import RelayName from "@app/components/RelayName.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import SpaceEdit from "@app/components/SpaceEdit.svelte"
import SpaceRelayStatus from "@app/components/SpaceRelayStatus.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte"
@@ -30,29 +30,33 @@
const back = () => history.back()
const startEdit = () => pushModal(SpaceEdit, {url, initialValues: $relay})
const startEdit = () => pushModal(SpaceEdit, {url, initialValues: $relay || {url}})
</script>
<div class="column gap-4">
<div class="relative flex gap-4">
<div class="relative">
<div class="avatar relative">
<div
class="center !flex h-16 w-16 min-w-16 rounded-full border-2 border-solid border-base-300 bg-base-300">
{#if $relay?.icon}
<img alt="" src={$relay.icon} />
{:else}
<Icon icon={Ghost} size={6} />
{/if}
<div class="flex justify-between">
<div class="relative flex gap-4">
<div class="relative">
<div class="avatar relative">
<div
class="center !flex h-16 w-16 min-w-16 rounded-full border-2 border-solid border-base-300 bg-base-300">
<RelayIcon {url} size={10} />
</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 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>
{#if $userIsAdmin}
<Button class="btn btn-primary" onclick={startEdit}>
<Icon icon={Pen} />
Edit
</Button>
{/if}
</div>
<RelayDescription {url} />
{#if $relay?.terms_of_service || $relay?.privacy_policy}
@@ -87,18 +91,10 @@
</div>
{/if}
</div>
{#if $userIsAdmin}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<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}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
</ModalFooter>
</div>
+8 -15
View File
@@ -1,8 +1,7 @@
<script lang="ts">
import {uniqBy, prop, append, ifLet} from "@welshman/lib"
import type {RelayProfile} 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 SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -22,10 +21,10 @@
type Props = {
url: string
initialValues: RelayProfile
initialValues: Partial<RelayProfile>
}
const {url, initialValues}: Props = $props()
const {url, initialValues = {}}: Props = $props()
const values = $state(initialValues)
@@ -57,8 +56,6 @@
if (imageFile) {
const {error, result} = await uploadFile(imageFile, {maxWidth: 128, maxHeight: 128})
console.log(imageFile, result)
if (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!"})
forceLoadRelay(url)
clearModals()
}
@@ -145,17 +138,17 @@
{#if imagePreview}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<ImageIcon src={imagePreview} alt="Room icon preview" />
<ImageIcon src={imagePreview} alt="" />
</div>
{:else}
<span class="text-sm opacity-75">No icon selected</span>
{/if}
<div class="flex gap-2">
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-xs">
<Icon icon={StickerSmileSquare} size={4} />
Select
</IconPickerButton>
<label class="btn btn-neutral btn-sm cursor-pointer">
<label class="btn btn-neutral btn-xs cursor-pointer">
<Icon icon={UploadMinimalistic} size={4} />
Upload
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
@@ -172,7 +165,7 @@
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
{#if imagePreview}
<ImageIcon src={imagePreview} alt="Room icon preview" />
<ImageIcon src={imagePreview} alt="" />
{:else}
<Icon icon={SettingsMinimalistic} />
{/if}
+2 -1
View File
@@ -13,7 +13,8 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import QRCode from "@app/components/QRCode.svelte"
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()
+4 -8
View File
@@ -15,18 +15,14 @@
const back = () => history.back()
const tryJoin = async () => {
await addSpaceMembership(url)
broadcastUserData([url])
clearModals()
}
const join = async () => {
loading = true
try {
await tryJoin()
await addSpaceMembership(url)
broadcastUserData([url])
clearModals()
} catch (e) {
loading = false
}
+123
View File
@@ -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>
+78
View File
@@ -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">
import {onMount} from "svelte"
import {derived} from "svelte/store"
import {displayRelayUrl, getTagValue, EVENT_TIME, ZAP_GOAL, THREAD} from "@welshman/util"
import {deriveRelay} from "@welshman/app"
import {some} from "@welshman/lib"
import {displayRelayUrl, getTagValue, EVENT_TIME, ZAP_GOAL, THREAD, REPORT} from "@welshman/util"
import {deriveRelay, pubkey} from "@welshman/app"
import {fly} from "@lib/transition"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.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 SquareTopDown from "@assets/icons/square-top-down.svg?dataurl"
import Exit from "@assets/icons/logout-3.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import Login from "@assets/icons/login-3.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 NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
@@ -32,7 +32,8 @@
import SpaceExit from "@app/components/SpaceExit.svelte"
import SpaceJoin from "@app/components/SpaceJoin.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 Alerts from "@app/components/Alerts.svelte"
import RoomCreate from "@app/components/RoomCreate.svelte"
@@ -42,14 +43,14 @@
ENABLE_ZAPS,
CONTENT_KINDS,
deriveSpaceMembers,
deriveEventsForUrl,
deriveUserRooms,
deriveOtherRooms,
userSpaceUrls,
hasNip29,
alerts,
alertsById,
deriveUserCanCreateRoom,
deriveUserIsSpaceAdmin,
deriveEventsForUrl,
} from "@app/core/state"
import {notifications} from "@app/util/notifications"
import {pushModal} from "@app/util/modal"
@@ -65,8 +66,11 @@
const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url)
const members = deriveSpaceMembers(url)
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(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(
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
@@ -83,12 +87,9 @@
const showDetail = () => pushModal(SpaceDetail, {url}, {replaceState})
const showMembers = () =>
pushModal(
ProfileList,
{url, pubkeys: $members, title: `Members of`, subtitle: displayRelayUrl(url)},
{replaceState},
)
const showMembers = () => pushModal(SpaceMembers, {url}, {replaceState})
const showReports = () => pushModal(SpaceReports, {url}, {replaceState})
const canCreateRoom = deriveUserCanCreateRoom(url)
@@ -155,13 +156,13 @@
</li>
{#if $userIsAdmin}
<li>
<Link external href="https://landlubber.coracle.social">
<Icon icon={Tuning2} />
Manage Space
<Icon icon={SquareTopDown} size={4} class="opacity-50" />
</Link>
<Button onclick={showReports}>
<Icon icon={Danger} />
View Reports ({$reports.length})
</Button>
</li>
{:else if $relay?.pubkey}
{/if}
{#if $relay?.pubkey && $relay.pubkey !== $pubkey}
<li>
<Link href={makeChatPath([$relay.pubkey])}>
<Icon icon={Letter} />
@@ -2,7 +2,7 @@
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Icon from "@lib/components/Icon.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 {makeSpacePath} from "@app/util/routes"
import {pushDrawer} from "@app/util/modal"
@@ -14,7 +14,7 @@
const status = deriveSocketStatus(url)
const openMenu = () => pushDrawer(MenuSpace, {url})
const openMenu = () => pushDrawer(SpaceMenu, {url})
</script>
<Button onclick={openMenu} class="btn btn-neutral btn-sm relative md:hidden">
+35
View File
@@ -0,0 +1,35 @@
<script lang="ts">
import {REPORT, displayRelayUrl} from "@welshman/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ReportItem from "@app/components/ReportItem.svelte"
import {deriveEventsForUrl} from "@app/core/state"
interface Props {
url: string
}
const {url}: Props = $props()
const reports = deriveEventsForUrl(url, [{kinds: [REPORT]}])
const back = () => history.back()
</script>
<div class="column gap-4">
<div class="flex min-w-0 flex-col gap-1">
<h1 class="ellipsize whitespace-nowrap text-2xl font-bold">Reports</h1>
<p class="ellipsize text-sm opacity-75">on {displayRelayUrl(url)}</p>
</div>
{#each $reports as event (event.id)}
<ReportItem {url} {event} />
{/each}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
</ModalFooter>
</div>
+1 -1
View File
@@ -21,7 +21,7 @@
import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal"
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()
+111
View File
@@ -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
View File
@@ -1,6 +1,6 @@
import {nwc} from "@getalby/sdk"
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 {
first,
@@ -31,7 +31,7 @@ import {
DELETE,
REPORT,
PROFILE,
INBOX_RELAYS,
MESSAGING_RELAYS,
RELAYS,
FOLLOWS,
REACTION,
@@ -46,17 +46,14 @@ import {
APP_DATA,
isSignedEvent,
makeEvent,
displayProfile,
normalizeRelayUrl,
makeList,
addToListPublicly,
removeFromListByPredicate,
getTag,
getListTags,
getRelayTags,
getRelayTagValues,
toNostrURI,
getRelaysFromList,
RelayMode,
getAddress,
getTagValue,
@@ -79,21 +76,20 @@ import {
session,
repository,
publishThunk,
profilesByPubkey,
tagEvent,
tagEventForReaction,
userRelaySelections,
userInboxRelaySelections,
nip44EncryptToSelf,
dropSession,
tagEventForComment,
tagEventForQuote,
waitForThunkError,
getPubkeyRelays,
userBlossomServers,
userBlossomServerList,
shouldUnwrap,
getThunkError,
} 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 {
SETTINGS,
@@ -105,13 +101,14 @@ import {
userSpaceUrls,
userSettingsValues,
getSetting,
userInboxRelays,
userGroupSelections,
userGroupList,
shouldIgnoreError,
stripPrefix,
relaysMostlyRestricted,
deriveSocket,
} from "@app/core/state"
import {loadAlertStatuses} from "@app/core/requests"
import {platform, platformName, getPushInfo} from "@app/util/push"
import {preferencesStorageProvider, Collection} from "@src/lib/storage"
// Utils
@@ -122,13 +119,6 @@ export const getPubkeyHints = (pubkey: string) => {
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) => {
if (parent) {
const nevent = nip19.neventEncode({
@@ -156,15 +146,15 @@ export const logout = async () => {
localStorage.clear()
await preferencesStorageProvider.clear()
await Collection.clearAll()
await kv.clear()
await db.clear()
}
// Synchronization
export const broadcastUserData = async (relays: string[]) => {
const authors = [pubkey.get()!]
const kinds = [RELAYS, INBOX_RELAYS, FOLLOWS, PROFILE]
const kinds = [RELAYS, MESSAGING_RELAYS, FOLLOWS, PROFILE]
const events = repository.query([{kinds, authors}])
for (const event of events) {
@@ -177,7 +167,7 @@ export const broadcastUserData = async (relays: string[]) => {
// List updates
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 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) => {
const list = get(userGroupSelections) || makeList({kind: ROOMS})
const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
const list = get(userGroupList) || makeList({kind: ROOMS})
const pred = (t: string[]) => normalizeRelayUrl(t[t[0] === "r" ? 1 : 2]) === url
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
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) => {
const list = get(userGroupSelections) || makeList({kind: ROOMS})
const list = get(userGroupList) || makeList({kind: ROOMS})
const newTags = [
["r", url],
["group", h, url],
@@ -206,7 +196,7 @@ export const addRoomMembership = 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 event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
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})
}
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
export const canEnforceNip70 = async (url: string) => {
@@ -291,12 +245,40 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
if (shouldIgnoreError(error)) return
if (claim) {
const ignoreClaimError =
error.includes("invalid invite code size") || error.includes("failed to validate invite code")
if (error.includes("invite code")) return "join request rejected"
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
@@ -538,11 +520,13 @@ export const createDmAlert = async () => {
shouldUnwrap.set(true)
}
const $pubkey = pubkey.get()!
return createAlert({
description: `for direct messages.`,
feed: makeIntersectionFeed(
feedFromFilters([{kinds: [WRAP], "#p": [pubkey.get()!]}]),
makeRelayFeed(...get(userInboxRelays)),
feedFromFilters([{kinds: [WRAP], "#p": [$pubkey]}]),
makeRelayFeed(...getPubkeyRelays($pubkey, RelayMode.Messaging)),
),
})
}
@@ -592,7 +576,7 @@ export const publishLeaveRequest = (params: LeaveRequestParams) =>
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()
if (!$session?.wallet) {
@@ -600,8 +584,11 @@ export const payInvoice = async (invoice: string) => {
}
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") {
if (msats) throw new Error("Unable to pay zero invoices with webln")
return getWebLn()
.enable()
.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) {
return normalizeBlossomUrl(url)
@@ -727,6 +714,18 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
// 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 ({
profile,
shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []),
+7 -5
View File
@@ -47,10 +47,9 @@ export const makeFeed = ({
element: HTMLElement
onExhausted?: () => void
}) => {
const initialEvents = getEventsForUrl(url, filters)
const seen = new Set(initialEvents.map(e => e.id))
const seen = new Set<string>()
const controller = new AbortController()
const buffer = writable(initialEvents)
const buffer = writable<TrustedEvent[]>([])
const events = writable<TrustedEvent[]>([])
const insertEvent = (event: TrustedEvent) => {
@@ -121,6 +120,10 @@ export const makeFeed = ({
},
})
for (const event of getEventsForUrl(url, filters)) {
insertEvent(event)
}
return {
events,
cleanup: () => {
@@ -144,7 +147,6 @@ export const makeCalendarFeed = ({
}) => {
const interval = int(5, DAY)
const controller = new AbortController()
const initialEvents = getEventsForUrl(url, filters)
let exhaustedScrollers = 0
let backwardWindow = [now() - interval, now()]
@@ -154,7 +156,7 @@ export const makeCalendarFeed = ({
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 start = getStart(event)

Some files were not shown because too many files have changed in this diff Show More