forked from coracle/flotilla
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf1ab5f0ee | |||
| 59568f95f1 | |||
| 75d52e7e17 | |||
| bdb5d3dfaa | |||
| c387b65460 | |||
| 01c4219922 | |||
| 9ca4440038 | |||
| d6cc414f41 | |||
| 7ccb2949a9 | |||
| 8d4e657af5 | |||
| 4886650dfa | |||
| e36e6093e9 | |||
| edd6e5c8fc | |||
| be7a42d951 | |||
| af91fe129b | |||
| 6fcf0e7f12 | |||
| b6defe59a8 | |||
| f618e4e1f3 | |||
| 5253980cdc | |||
| 5931a268cf | |||
| 268028a968 | |||
| d6669f42c1 | |||
| 19d69005a1 | |||
| 9917970760 | |||
| 814c5974c4 | |||
| f5dced433a | |||
| 9e96d5e483 | |||
| 420dfc41f3 | |||
| 23ae530cd4 | |||
| 8dfbc99a34 | |||
| 75bca31c14 | |||
| 0c9109f387 | |||
| 7a17dc772f | |||
| 7bd98270f8 | |||
| c15f57c9a5 | |||
| 0f311c45c0 | |||
| 055d539b88 | |||
| b8e23c47d4 | |||
| 39c72a61ce | |||
| 166bd81310 | |||
| d0565e7c62 | |||
| 7ddc1657ad | |||
| fe789c461d | |||
| cd8d8b548f | |||
| 3b202b31cb | |||
| fd846d41ea | |||
| 3d3ffaf406 | |||
| 85e5413951 | |||
| 9f3bfd5ac0 | |||
| 9d6531c0d5 | |||
| 77d20966ee | |||
| daf5cc84bd | |||
| 167cd045f4 | |||
| c83461688f | |||
| b19881a8a9 | |||
| b6524f4a58 | |||
| 2ee370e78b | |||
| a378ecbad4 | |||
| 72ced31625 | |||
| 6f7a1c690f | |||
| df42ec9915 | |||
| 19d67783fc | |||
| d8c3378e5c | |||
| 73c6b9656c | |||
| 80d44a097a | |||
| a5dfa02771 | |||
| 66f3686ef4 | |||
| a65f6f6323 | |||
| 523c54a1f1 | |||
| 7e3cf94ee8 | |||
| 404dc94c34 | |||
| ea0e1a6c9a | |||
| 880093296e | |||
| e17cda1eff | |||
| 1e0cb93183 | |||
| 14cd49caf3 | |||
| 64916f5d29 | |||
| 7b58cdf855 | |||
| 2e05eee9e7 | |||
| efb0528f76 | |||
| 1ea39c1d56 | |||
| c2aa829334 | |||
| a58fc68235 | |||
| 220f26253d | |||
| 08fef7aa51 | |||
| b8c77c20cd | |||
| aa27a05fa6 | |||
| 9a68101a64 | |||
| dd5384f7e4 | |||
| 71d63ed21a | |||
| de4e1c8677 | |||
| e6e1eb8897 | |||
| 603653574c | |||
| e83a72b426 | |||
| eb5bcd8948 | |||
| 7c46dfb6bc | |||
| dcc6f463a7 | |||
| 86d082b1ab | |||
| 659403c308 | |||
| 1c0e680c17 | |||
| 05f7d128e4 | |||
| dfcb88dcce | |||
| 5890fb64a5 | |||
| f52142bc52 | |||
| f4f60a5333 | |||
| 6a646b3240 | |||
| e5fd172994 | |||
| 7cc2a2f264 | |||
| ad58af8605 | |||
| 5b7985e5d9 | |||
| 6ff798f4e8 | |||
| ed738f64c8 | |||
| cbc4c524c4 | |||
| bf599cb190 | |||
| 06a03f5ab1 |
@@ -1,5 +1,8 @@
|
|||||||
VITE_DEFAULT_PUBKEYS=fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
|
VITE_DEFAULT_PUBKEYS=fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
|
||||||
|
VITE_BURROW_URL=
|
||||||
VITE_PLATFORM_URL=https://flotilla.social
|
VITE_PLATFORM_URL=https://flotilla.social
|
||||||
|
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||||
|
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||||
VITE_PLATFORM_NAME=Flotilla
|
VITE_PLATFORM_NAME=Flotilla
|
||||||
VITE_PLATFORM_LOGO=static/flotilla.png
|
VITE_PLATFORM_LOGO=static/flotilla.png
|
||||||
VITE_PLATFORM_RELAY=
|
VITE_PLATFORM_RELAY=
|
||||||
|
|||||||
+4
-1
@@ -17,6 +17,9 @@ Thumbs.db
|
|||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
# Android
|
||||||
|
.idea
|
||||||
|
|
||||||
# Generated assets
|
# Generated assets
|
||||||
static/favicon.ico
|
static/favicon.ico
|
||||||
static/pwa-64x64.png
|
static/pwa-64x64.png
|
||||||
@@ -24,5 +27,5 @@ static/pwa-192x192.png
|
|||||||
static/pwa-512x512.png
|
static/pwa-512x512.png
|
||||||
static/apple-touch-icon-180x180.png
|
static/apple-touch-icon-180x180.png
|
||||||
static/maskable-icon-512x512.png
|
static/maskable-icon-512x512.png
|
||||||
src/assets
|
src/assets/icons/*.webp
|
||||||
manifest.webmanifest
|
manifest.webmanifest
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
# 0.2.3
|
||||||
|
|
||||||
|
* Add NIP 56 reports for messages and threads
|
||||||
|
* Add ToS and privacy policy
|
||||||
|
* Add avatar fallback icons
|
||||||
|
* Add mark as read to chats
|
||||||
|
* Add send button to chat compose
|
||||||
|
* Accommodate onion URLs
|
||||||
|
* Improve loading and notifications
|
||||||
|
|
||||||
|
# 0.2.2
|
||||||
|
|
||||||
|
* Fix bug with sending messages
|
||||||
|
|
||||||
|
# 0.2.1
|
||||||
|
|
||||||
|
* Improve performance, as well as scrolling and loading
|
||||||
|
* Integrate @welshman/editor
|
||||||
|
* Improve NIP 29 compatibility
|
||||||
|
* Fix incorrect connection errors
|
||||||
|
* Refine notifications
|
||||||
|
* Add room menu to space homepage
|
||||||
|
* Fix storage bugs
|
||||||
|
* Add join space CTA
|
||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "social.flotilla"
|
applicationId "social.flotilla"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 3
|
||||||
versionName "1.0"
|
versionName "0.2.4"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
classpath 'com.android.tools.build:gradle:8.8.0'
|
||||||
classpath 'com.google.gms:google-services:4.4.0'
|
classpath 'com.google.gms:google-services:4.4.0'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
Generated
+483
-160
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.1.0",
|
"version": "0.2.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.1.0",
|
"version": "0.2.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^6.1.2",
|
"@capacitor/android": "^6.1.2",
|
||||||
"@capacitor/cli": "^6.1.2",
|
"@capacitor/cli": "^6.1.2",
|
||||||
@@ -28,18 +28,18 @@
|
|||||||
"@tiptap/extension-text": "^2.6.6",
|
"@tiptap/extension-text": "^2.6.6",
|
||||||
"@tiptap/suggestion": "^2.6.4",
|
"@tiptap/suggestion": "^2.6.4",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.6",
|
"@vite-pwa/sveltekit": "^0.6.6",
|
||||||
"@welshman/app": "~0.0.27",
|
"@welshman/app": "~0.0.37",
|
||||||
"@welshman/content": "~0.0.12",
|
"@welshman/content": "~0.0.15",
|
||||||
"@welshman/dvm": "~0.0.10",
|
"@welshman/dvm": "~0.0.13",
|
||||||
"@welshman/feeds": "~0.0.25",
|
"@welshman/editor": "~0.0.6",
|
||||||
"@welshman/lib": "~0.0.26",
|
"@welshman/feeds": "~0.0.30",
|
||||||
"@welshman/net": "~0.0.36",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/signer": "~0.0.14",
|
"@welshman/net": "~0.0.45",
|
||||||
"@welshman/store": "~0.0.12",
|
"@welshman/signer": "~0.0.19",
|
||||||
"@welshman/util": "~0.0.45",
|
"@welshman/store": "~0.0.15",
|
||||||
|
"@welshman/util": "~0.0.57",
|
||||||
"daisyui": "^4.12.10",
|
"daisyui": "^4.12.10",
|
||||||
"date-picker-svelte": "^2.13.0",
|
"date-picker-svelte": "^2.13.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
@@ -52,11 +52,11 @@
|
|||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"svelte-tiptap": "^1.1.3",
|
"svelte-tiptap": "^1.1.3"
|
||||||
"throttle-debounce": "^5.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
|
"@sentry/cli": "^2.40.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"@types/eslint": "^9.6.0",
|
"@types/eslint": "^9.6.0",
|
||||||
@@ -3120,22 +3120,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@noble/curves": {
|
"node_modules/@noble/curves": {
|
||||||
"version": "1.5.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz",
|
||||||
"integrity": "sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==",
|
"integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "1.4.0"
|
"@noble/hashes": "1.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@noble/hashes": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "1.4.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz",
|
||||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
|
"integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": "^14.21.3 || >=16"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
@@ -3650,6 +3655,158 @@
|
|||||||
"node": ">=14.18"
|
"node": ">=14.18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@sentry/cli": {
|
||||||
|
"version": "2.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.40.0.tgz",
|
||||||
|
"integrity": "sha512-yo+ZfrrpVyu/2Q9r4XI84VeC6xTNzTharSJB2D0BNkreL+c16I1ykG1uc/GmmFnYVBq+HHAaYqXVfSUV14IdHw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"https-proxy-agent": "^5.0.0",
|
||||||
|
"node-fetch": "^2.6.7",
|
||||||
|
"progress": "^2.0.3",
|
||||||
|
"proxy-from-env": "^1.1.0",
|
||||||
|
"which": "^2.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"sentry-cli": "bin/sentry-cli"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@sentry/cli-darwin": "2.40.0",
|
||||||
|
"@sentry/cli-linux-arm": "2.40.0",
|
||||||
|
"@sentry/cli-linux-arm64": "2.40.0",
|
||||||
|
"@sentry/cli-linux-i686": "2.40.0",
|
||||||
|
"@sentry/cli-linux-x64": "2.40.0",
|
||||||
|
"@sentry/cli-win32-i686": "2.40.0",
|
||||||
|
"@sentry/cli-win32-x64": "2.40.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry/cli-darwin": {
|
||||||
|
"version": "2.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.40.0.tgz",
|
||||||
|
"integrity": "sha512-GmPGvPU9tjM1Ps/pkUGQa7rImveo4delb2Dc5l8129i1MyD2ugJ5zjeNhIdBHkaObpuude9rUS7sHC4HTU2Wqw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry/cli-linux-arm": {
|
||||||
|
"version": "2.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.40.0.tgz",
|
||||||
|
"integrity": "sha512-LUdwh3shYXZThkBvmKFUkQvmsCIQu76ZVqU7NXcEWHRF9gITijnSyHKCBPCbcGkb1SqQ92BW/1cJq84Dy0/DRw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux",
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry/cli-linux-arm64": {
|
||||||
|
"version": "2.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.40.0.tgz",
|
||||||
|
"integrity": "sha512-b8gDORhkhP/g1CTYVKzBlbYlmC3BqkgEzAXP8ViFxX1NNS7dK9Hr84cVnDGxhSIfCP8TW1d5V3AGeHwQr5EwEg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux",
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry/cli-linux-i686": {
|
||||||
|
"version": "2.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.40.0.tgz",
|
||||||
|
"integrity": "sha512-sZo3QykQRpMkrz0Eb07ViyK++C6Iir1j7Rpsj/97y5WDncR8TrpGTn6ceuuVRt4clA09/ZIvwuS7amfeKN6jQw==",
|
||||||
|
"cpu": [
|
||||||
|
"x86",
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux",
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry/cli-linux-x64": {
|
||||||
|
"version": "2.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.40.0.tgz",
|
||||||
|
"integrity": "sha512-ctpBFuyk2fP97FkxWTD9olI1BM1cy+rUIfnUqmrjXneTaUi3RFIFBB4koYhh1UT6OCWIRvChRIq40Rd9R3Pw8A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux",
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry/cli-win32-i686": {
|
||||||
|
"version": "2.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.40.0.tgz",
|
||||||
|
"integrity": "sha512-4SYD40zJS7hVbFzAwXvXcVIoc7xsWa6L1RW1SQlt+Woh5MTPk7FMMSGft8021OSGTljiuqQzx4ecnXMO0K/gOw==",
|
||||||
|
"cpu": [
|
||||||
|
"x86",
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sentry/cli-win32-x64": {
|
||||||
|
"version": "2.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.40.0.tgz",
|
||||||
|
"integrity": "sha512-QEW2Ra4Wsr4y6AwcxOk2hL0zMlCK+adTSTaptTMmcm52el8XjdMwsNo7d/416HUYNcND0YZGih7D+KERepyQSw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sentry/core": {
|
"node_modules/@sentry/core": {
|
||||||
"version": "8.35.0",
|
"version": "8.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.35.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.35.0.tgz",
|
||||||
@@ -3784,9 +3941,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/core": {
|
"node_modules/@tiptap/core": {
|
||||||
"version": "2.7.2",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.0.tgz",
|
||||||
"integrity": "sha512-rGAH90LPMR5OIG7vuTDRw8WxDYxPXSxuGtu++mxPF+Bv7V2ijPOy3P1oyV1G3KGoS0pPiNugLh+tVLsElcx/9Q==",
|
"integrity": "sha512-0S3AWx6E2QqwdQqb6z0/q6zq2u9lA9oL3BLyAaITGSC9zt8OwjloS2k1zN6wLa9hp2rO0c0vDnWsTPeFaEaMdw==",
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3814,53 +3972,57 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-code": {
|
"node_modules/@tiptap/extension-code": {
|
||||||
"version": "2.6.6",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.11.0.tgz",
|
||||||
"integrity": "sha512-JrEFKsZiLvfvOFhOnnrpA0TzCuJjDeysfbMeuKUZNV4+DhYOL28d39H1++rEtJAX0LcbBU60oC5/PrlU9SpvRQ==",
|
"integrity": "sha512-2roNZxcny1bGjyZ8x6VmGTuKbwfJyTZ1hiqPc/CRTQ1u42yOhbjF4ziA5kfyUoQlzygZrWH9LR5IMYGzPQ1N3w==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.6.6"
|
"@tiptap/core": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-code-block": {
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
"version": "2.6.6",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.11.0.tgz",
|
||||||
"integrity": "sha512-1YLp/zHMHSkE2xzht8nPR6T4sQJJ3ket798czxWuQEbetFv/l0U/mpiPpYSLObj6oTAoqYZ0kWXZj5eQSpPB8Q==",
|
"integrity": "sha512-8of3qTOLjpveHBrrk8KVliSUVd6R2i2TNrBj0f/21HcFVAy0fP++02p6vI6UPOhwM3+p3CprGdSM48DFCu1rqw==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.6.6",
|
"@tiptap/core": "^2.7.0",
|
||||||
"@tiptap/pm": "^2.6.6"
|
"@tiptap/pm": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-document": {
|
"node_modules/@tiptap/extension-document": {
|
||||||
"version": "2.6.6",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.11.0.tgz",
|
||||||
"integrity": "sha512-6qlH5VWzLHHRVeeciRC6C4ZHpMsAGPNG16EF53z0GeMSaaFD/zU3B239QlmqXmLsAl8bpf8Bn93N0t2ABUvScw==",
|
"integrity": "sha512-9YI0AT3mxyUZD7NHECHyV1uAjQ8KwxOS5ACwvrK1MU8TqY084LmodYNTXPKwpqbr51yvt3qZq1R7UIVu4/22Cg==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.6.6"
|
"@tiptap/core": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-dropcursor": {
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
"version": "2.6.6",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.11.0.tgz",
|
||||||
"integrity": "sha512-O6CeKriA9uyHsg7Ui4z5ZjEWXQxrIL+1zDekffW0wenGC3G4LUsCzAiFS4LSrR9a3u7tnwqGApW10rdkmCGF4w==",
|
"integrity": "sha512-p7tUtlz7KzBa+06+7W2LJ8AEiHG5chdnUIapojZ7SqQCrFRVw70R+orpkzkoictxNNHsun0A9FCUy4rz8L0+nQ==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.6.6",
|
"@tiptap/core": "^2.7.0",
|
||||||
"@tiptap/pm": "^2.6.6"
|
"@tiptap/pm": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-floating-menu": {
|
"node_modules/@tiptap/extension-floating-menu": {
|
||||||
@@ -3881,41 +4043,44 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-gapcursor": {
|
"node_modules/@tiptap/extension-gapcursor": {
|
||||||
"version": "2.6.6",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.0.tgz",
|
||||||
"integrity": "sha512-O2lQ2t0X0Vsbn3yLWxFFHrXY6C2N9Y6ZF/M7LWzpcDTUZeWuhoNkFE/1yOM0h6ZX1DO2A9hNIrKpi5Ny8yx+QA==",
|
"integrity": "sha512-1TVOthPkUYwTQnQwP0BzuIHVz09epOiXJQ3GqgNZsmTehwcMzz2vGCpx1JXhZ5DoMaREHNLCdraXb1n2FdhDNA==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.6.6",
|
"@tiptap/core": "^2.7.0",
|
||||||
"@tiptap/pm": "^2.6.6"
|
"@tiptap/pm": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-hard-break": {
|
"node_modules/@tiptap/extension-hard-break": {
|
||||||
"version": "2.6.6",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.11.0.tgz",
|
||||||
"integrity": "sha512-bsUuyYBrMDEiudx1dOQSr9MzKv13m0xHWrOK+DYxuIDYJb5g+c9un5cK7Js+et/HEYYSPOoH/iTW6h+4I5YeUg==",
|
"integrity": "sha512-7pMgPNk2FnPT0LcWaWNNxOLK3LQnRSYFgrdBGMXec3sy+y3Lit3hM+EZhbZcHpTIQTbWWs+eskh1waRMIt0ZaQ==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.6.6"
|
"@tiptap/core": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-history": {
|
"node_modules/@tiptap/extension-history": {
|
||||||
"version": "2.6.6",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.11.0.tgz",
|
||||||
"integrity": "sha512-tPTzAmPGqMX5Bd5H8lzRpmsaMvB9DvI5Dy2za/VQuFtxgXmDiFVgHRkRXIuluSkPTuANu84XBOQ0cBijqY8x4w==",
|
"integrity": "sha512-eEUEDoOtS17AHVEPbGfZ+x2L5A87SiIsppWYTkpfIH/8EnVQmzu+3i1tcT9cWvHC31d9JTG7TDptVuuHr30TJw==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.6.6",
|
"@tiptap/core": "^2.7.0",
|
||||||
"@tiptap/pm": "^2.6.6"
|
"@tiptap/pm": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-image": {
|
"node_modules/@tiptap/extension-image": {
|
||||||
@@ -3949,15 +4114,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-paragraph": {
|
"node_modules/@tiptap/extension-paragraph": {
|
||||||
"version": "2.6.6",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.11.0.tgz",
|
||||||
"integrity": "sha512-fD/onCr16UQWx+/xEmuFC2MccZZ7J5u4YaENh8LMnAnBXf78iwU7CAcmuc9rfAEO3qiLoYGXgLKiHlh2ZfD4wA==",
|
"integrity": "sha512-xLNC05An3SQq0bVHJtOTLa8As5r6NxDZFpK0NZqO2hTq/fAIRL/9VPeZ8E0tziXULwIvIPp+L0Taw3TvaUkRUg==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.6.6"
|
"@tiptap/core": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-placeholder": {
|
"node_modules/@tiptap/extension-placeholder": {
|
||||||
@@ -3975,41 +4141,43 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-text": {
|
"node_modules/@tiptap/extension-text": {
|
||||||
"version": "2.6.6",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.11.0.tgz",
|
||||||
"integrity": "sha512-e84uILnRzNzcwK1DVQNpXVmBG1Cq3BJipTOIDl1LHifOok7MBjhI/X+/NR0bd3N2t6gmDTWi63+4GuJ5EeDmsg==",
|
"integrity": "sha512-LcyrP+7ZEVx3YaKzjMAeujq+4xRt4mZ3ITGph2CQ4vOKFaMI8bzSR909q18t7Qyyvek0a9VydEU1NHSaq4G5jw==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.6.6"
|
"@tiptap/core": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/pm": {
|
"node_modules/@tiptap/pm": {
|
||||||
"version": "2.7.2",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.0.tgz",
|
||||||
"integrity": "sha512-RiRPlwpuE6IHDJytE0tglbFlWELOaqeyGRGv25wBTjzV1plnqC5B3U65XY/8kKuuLjdd3NpRfR68DXBafusSBg==",
|
"integrity": "sha512-4RU6bpODkMY+ZshzdRFcuUc5jWlMW82LWXR6UOsHK/X/Mav41ZFS0Cyf+hQM6gxxTB09YFIICmGpEpULb+/CuA==",
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-changeset": "^2.2.1",
|
"prosemirror-changeset": "^2.2.1",
|
||||||
"prosemirror-collab": "^1.3.1",
|
"prosemirror-collab": "^1.3.1",
|
||||||
"prosemirror-commands": "^1.6.0",
|
"prosemirror-commands": "^1.6.2",
|
||||||
"prosemirror-dropcursor": "^1.8.1",
|
"prosemirror-dropcursor": "^1.8.1",
|
||||||
"prosemirror-gapcursor": "^1.3.2",
|
"prosemirror-gapcursor": "^1.3.2",
|
||||||
"prosemirror-history": "^1.4.1",
|
"prosemirror-history": "^1.4.1",
|
||||||
"prosemirror-inputrules": "^1.4.0",
|
"prosemirror-inputrules": "^1.4.0",
|
||||||
"prosemirror-keymap": "^1.2.2",
|
"prosemirror-keymap": "^1.2.2",
|
||||||
"prosemirror-markdown": "^1.13.0",
|
"prosemirror-markdown": "^1.13.1",
|
||||||
"prosemirror-menu": "^1.2.4",
|
"prosemirror-menu": "^1.2.4",
|
||||||
"prosemirror-model": "^1.22.3",
|
"prosemirror-model": "^1.23.0",
|
||||||
"prosemirror-schema-basic": "^1.2.3",
|
"prosemirror-schema-basic": "^1.2.3",
|
||||||
"prosemirror-schema-list": "^1.4.1",
|
"prosemirror-schema-list": "^1.4.1",
|
||||||
"prosemirror-state": "^1.4.3",
|
"prosemirror-state": "^1.4.3",
|
||||||
"prosemirror-tables": "^1.4.0",
|
"prosemirror-tables": "^1.6.1",
|
||||||
"prosemirror-trailing-node": "^3.0.0",
|
"prosemirror-trailing-node": "^3.0.0",
|
||||||
"prosemirror-transform": "^1.10.0",
|
"prosemirror-transform": "^1.10.2",
|
||||||
"prosemirror-view": "^1.33.10"
|
"prosemirror-view": "^1.37.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4017,16 +4185,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/suggestion": {
|
"node_modules/@tiptap/suggestion": {
|
||||||
"version": "2.6.4",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.11.0.tgz",
|
||||||
"integrity": "sha512-t4GOEcsVSCwTlugHjZdK5Swe6or/tBej5E3ZWYOFHxkNLDod76Q7hvAeBPYrLeDo6m3sPnxrazfdqSeVclk72g==",
|
"integrity": "sha512-f+KcczhzEEy2f7/0N/RSID+Z6NjxCX6ab26NLfWZxdaEm/J+vQ2Pqh/e5Z59vMfKiC0DJXVcO0rdv2LBh23qDw==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.6.4",
|
"@tiptap/core": "^2.7.0",
|
||||||
"@tiptap/pm": "^2.6.4"
|
"@tiptap/pm": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@trapezedev/gradle-parse": {
|
"node_modules/@trapezedev/gradle-parse": {
|
||||||
@@ -4294,7 +4463,8 @@
|
|||||||
"node_modules/@types/events": {
|
"node_modules/@types/events": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz",
|
||||||
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g=="
|
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/fs-extra": {
|
"node_modules/@types/fs-extra": {
|
||||||
"version": "8.1.5",
|
"version": "8.1.5",
|
||||||
@@ -4396,6 +4566,15 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.5.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
||||||
|
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0.tgz",
|
||||||
@@ -4664,18 +4843,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/app": {
|
"node_modules/@welshman/app": {
|
||||||
"version": "0.0.27",
|
"version": "0.0.37",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/app/-/app-0.0.27.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/app/-/app-0.0.37.tgz",
|
||||||
"integrity": "sha512-o8voN+ldio+LjYathHKhTG0Vx8rZLoHksAPLLg4rzgAjqKIgWetQ4XjU/Fjqv/5rNNjBh6u0Jr5vR1PUkyptOw==",
|
"integrity": "sha512-EhhLx10PE6r/soiuaR0GF+NSH9H3ilTaXwmfx2cHHR1PE2LXXvf1oWMJl0ZPFmYe0VWfNiu98SLbTLYwe1Y4dQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/dvm": "~0.0.10",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@welshman/feeds": "~0.0.25",
|
"@welshman/dvm": "~0.0.13",
|
||||||
"@welshman/lib": "~0.0.24",
|
"@welshman/feeds": "~0.0.30",
|
||||||
"@welshman/net": "~0.0.35",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/signer": "~0.0.14",
|
"@welshman/net": "~0.0.45",
|
||||||
"@welshman/store": "~0.0.12",
|
"@welshman/signer": "~0.0.19",
|
||||||
"@welshman/util": "~0.0.45",
|
"@welshman/store": "~0.0.15",
|
||||||
|
"@welshman/util": "~0.0.54",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.0",
|
||||||
"svelte": "^4.2.18",
|
"svelte": "^4.2.18",
|
||||||
@@ -4683,94 +4863,158 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/content": {
|
"node_modules/@welshman/content": {
|
||||||
"version": "0.0.12",
|
"version": "0.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.15.tgz",
|
||||||
"integrity": "sha512-hdrZkHlDKJx8i8FdEJo4NFlBMRJWDkZHBYCBCbx77fcxPN8nJ2yKCl7bmIM51XwEFRrZMOQrmQswvYuOr8h1DQ==",
|
"integrity": "sha512-y0f0iLIaHUqEJJ0ziRWbGw13mg0tOLTKpHQNgIXJ03PD3xGHBaQ5xPWiOI8XeUt35KgrayvQZHsaqfAsOWkwag==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "^7.0.2",
|
"@braintree/sanitize-url": "^7.0.2",
|
||||||
|
"@welshman/lib": "~0.0.34",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/dvm": {
|
"node_modules/@welshman/dvm": {
|
||||||
"version": "0.0.10",
|
"version": "0.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/dvm/-/dvm-0.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/dvm/-/dvm-0.0.13.tgz",
|
||||||
"integrity": "sha512-9MlwSmsFeczt+tirKWBenOaWRy1QzcXj0V4Ibc4O2ZdpGkybR3AI79uYLwAI+TG/pkEzHqY+1i7OXfjhBXqlyw==",
|
"integrity": "sha512-C8y4s7wDJTJ6DVuzQoRLAhMpFD+kBoRHlc7kCTmjzh62VOmDSY+46xKttK/WaEnypPiPbIbBg3hd3+tO2A9KoQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.21",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@welshman/net": "~0.0.25",
|
"@welshman/lib": "~0.0.34",
|
||||||
"@welshman/util": "~0.0.37",
|
"@welshman/net": "~0.0.43",
|
||||||
|
"@welshman/util": "~0.0.52",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@welshman/dvm/node_modules/@noble/hashes": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@welshman/editor": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-7ZnjrsBX/5Z2OiHStCSBqNlspX/weURcP8yrH9CTcOEqJZfPx5UWfeYmzsbXttvCPBph+Cv9jfHkeVreyLkeKQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^2.9.1",
|
||||||
|
"@tiptap/extension-code": "^2.9.1",
|
||||||
|
"@tiptap/extension-code-block": "^2.9.1",
|
||||||
|
"@tiptap/extension-document": "^2.9.1",
|
||||||
|
"@tiptap/extension-dropcursor": "^2.9.1",
|
||||||
|
"@tiptap/extension-gapcursor": "^2.9.1",
|
||||||
|
"@tiptap/extension-hard-break": "^2.9.1",
|
||||||
|
"@tiptap/extension-history": "^2.9.1",
|
||||||
|
"@tiptap/extension-paragraph": "^2.9.1",
|
||||||
|
"@tiptap/extension-placeholder": "^2.9.1",
|
||||||
|
"@tiptap/extension-text": "^2.9.1",
|
||||||
|
"@tiptap/pm": "^2.9.1",
|
||||||
|
"@tiptap/suggestion": "^2.9.1",
|
||||||
|
"@welshman/lib": "~0.0.36",
|
||||||
|
"@welshman/util": "~0.0.53",
|
||||||
|
"nostr-editor": "github:cesardeazevedo/nostr-editor#a211491c",
|
||||||
|
"nostr-tools": "^2.8.1",
|
||||||
|
"svelte": "^4.0.0",
|
||||||
|
"svelte-tiptap": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@welshman/feeds": {
|
"node_modules/@welshman/feeds": {
|
||||||
"version": "0.0.25",
|
"version": "0.0.30",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/feeds/-/feeds-0.0.25.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/feeds/-/feeds-0.0.30.tgz",
|
||||||
"integrity": "sha512-XoSQjOH71n6gFH5Lgm5szSrXTh82a8B608mDakYHdv7XXtGh2Li12kmy6qYFtQtG+pYUx6O+QrWa7UDwmOKdJw==",
|
"integrity": "sha512-Zcex2uJVeYM55zDI1Dhb5I41lYGD4BURWl95nbFaWbbMYDwoAFIS2cPXBsaGNrITzsz8qByvRs2RnplrmZwSzA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.24",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/util": "~0.0.45"
|
"@welshman/util": "~0.0.54"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/lib": {
|
"node_modules/@welshman/lib": {
|
||||||
"version": "0.0.26",
|
"version": "0.0.37",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.26.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.37.tgz",
|
||||||
"integrity": "sha512-RMOsRBb0YXKfAupx1bmrNxacv13pheYvdc91DxGYbtvraPeP+B5C0RR9cQzLfFkv0neCpMA318fYXeTk1KeXaQ==",
|
"integrity": "sha512-qnEjdGIb/QVIYML0EQgGAhucds00hiX8/4rJ9OcqoRUXPS2cDD47BcgYap+kG/OKSfgSL7EH64voAdoprZtuvg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/base": "^1.1.6",
|
"@scure/base": "^1.1.6",
|
||||||
"@types/events": "^3.0.3",
|
"@types/events": "^3.0.3",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"events": "^3.3.0"
|
||||||
"events": "^3.3.0",
|
|
||||||
"throttle-debounce": "^5.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/net": {
|
"node_modules/@welshman/net": {
|
||||||
"version": "0.0.36",
|
"version": "0.0.45",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.36.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.45.tgz",
|
||||||
"integrity": "sha512-ZRYC9Hl45bI/kfnKd+DSX/RnbDIA4VhEGUpQTGikqCjfmbWxvN8zr3ajvkUMQHhe95VyKyjpjQKPpkJn9g6+MQ==",
|
"integrity": "sha512-sXYmfGdqvrj1ssr5xaSUxmJAFo+ScJtodBpzgya0CTLYorKRGoeQRJsyPWdh5VBVtoldPclzGhfvZ11d8d8Lyw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.25",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/util": "~0.0.45",
|
"@welshman/util": "~0.0.54",
|
||||||
"isomorphic-ws": "^5.0.0",
|
"isomorphic-ws": "^5.0.0",
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/signer": {
|
"node_modules/@welshman/signer": {
|
||||||
"version": "0.0.14",
|
"version": "0.0.19",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/signer/-/signer-0.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/signer/-/signer-0.0.19.tgz",
|
||||||
"integrity": "sha512-aQkKloUpFwtI42hAV7/QpJ0unNouIIG6OXYwBuhFgFDxA3QuE3iVMO/9HYCEpytZkll7+NJ1uWKysMunuy+OeQ==",
|
"integrity": "sha512-+pKkm5HeaSJB6ET456w0zVnbVAjRzYuDagYUns1BQ6Co22nUSp2CntWFFckwbH2eQ9bjdlC65LjRfIP9qhNSrg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.24",
|
"@noble/curves": "^1.7.0",
|
||||||
"@welshman/net": "~0.0.35",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@welshman/util": "~0.0.45",
|
"@welshman/lib": "~0.0.37",
|
||||||
|
"@welshman/net": "~0.0.45",
|
||||||
|
"@welshman/util": "~0.0.54",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.x"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"nostr-signer-capacitor-plugin": "^0.0.3"
|
"nostr-signer-capacitor-plugin": "^0.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@welshman/signer/node_modules/@noble/hashes": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@welshman/store": {
|
"node_modules/@welshman/store": {
|
||||||
"version": "0.0.12",
|
"version": "0.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/store/-/store-0.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/store/-/store-0.0.15.tgz",
|
||||||
"integrity": "sha512-69ONyeKOIG0Ba1tEoRPW4JnRaag7yargRS2WhXtyYsbfvQErivWBWLgM4wJz9Qovd+RxljssUswJfDHnZ2FCNQ==",
|
"integrity": "sha512-xapI9cqmpf6ot90T0Z+gFH2HSAby/N8oyLl7u+JASYbNDS3pkK26SviNlTciMC+VBuJChEr1zX5l8RHuLPtw5Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.23",
|
"@welshman/lib": "~0.0.34",
|
||||||
"@welshman/util": "~0.0.42",
|
"@welshman/util": "~0.0.52",
|
||||||
"svelte": "^4.2.18"
|
"svelte": "^4.2.18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/util": {
|
"node_modules/@welshman/util": {
|
||||||
"version": "0.0.45",
|
"version": "0.0.57",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.45.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.57.tgz",
|
||||||
"integrity": "sha512-6ktWY/LQsBqVYN+PIfT7Aob48QDf4XjXOI1aDJ7tsewaG4FKuMDy9rfHE7/FjVMGhYrKsugaL2GBVFcS4UfRTg==",
|
"integrity": "sha512-YflD6sfqdhIfHioJVlLydvyKOgACFL0dAcWHymlDz/FszIAl2k0XQXKgAjf0lT2uoXfrCdPsfSZwMTW7qUAY6Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "~0.0.24",
|
"@types/ws": "^8.5.13",
|
||||||
|
"@welshman/lib": "~0.0.37",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@xml-tools/parser": {
|
"node_modules/@xml-tools/parser": {
|
||||||
@@ -4832,6 +5076,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/aggregate-error": {
|
"node_modules/aggregate-error": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||||
@@ -7099,6 +7356,7 @@
|
|||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.x"
|
"node": ">=0.8.x"
|
||||||
}
|
}
|
||||||
@@ -8071,6 +8329,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "6",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/husky": {
|
"node_modules/husky": {
|
||||||
"version": "9.1.6",
|
"version": "9.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz",
|
||||||
@@ -8623,6 +8895,7 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
|
||||||
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
|
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
|
||||||
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"ws": "*"
|
"ws": "*"
|
||||||
}
|
}
|
||||||
@@ -9741,8 +10014,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/nostr-editor": {
|
"node_modules/nostr-editor": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/nostr-editor/-/nostr-editor-0.0.3.tgz",
|
"resolved": "git+ssh://git@github.com/cesardeazevedo/nostr-editor.git#a211491c7cfeb792ae58ba91d295fe747c151ded",
|
||||||
"integrity": "sha512-ODfwzebBRweaYt8l0pz8EbV4OqbEKZpDAVdoU+j7ubmfjhqIyk1PcQoikEZ8UasqkBcZjEQMAPl776F8nb55fQ==",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"light-bolt11-decoder": "^3.1.1"
|
"light-bolt11-decoder": "^3.1.1"
|
||||||
@@ -9773,9 +10045,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nostr-tools": {
|
"node_modules/nostr-tools": {
|
||||||
"version": "2.7.2",
|
"version": "2.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
|
||||||
"integrity": "sha512-Bq3Ug0SZFtgtL1+0wCnAe8AJtI7yx/00/a2nUug9SkhfOwlKS92Tef12iCK9FdwXw+oFZWMtRnSwcLayQso+xA==",
|
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
|
||||||
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/ciphers": "^0.5.1",
|
"@noble/ciphers": "^0.5.1",
|
||||||
"@noble/curves": "1.2.0",
|
"@noble/curves": "1.2.0",
|
||||||
@@ -9785,7 +10058,7 @@
|
|||||||
"@scure/bip39": "1.2.1"
|
"@scure/bip39": "1.2.1"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"nostr-wasm": "v0.1.0"
|
"nostr-wasm": "0.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
@@ -10546,6 +10819,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/progress": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prompts": {
|
"node_modules/prompts": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||||
@@ -10587,14 +10870,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-commands": {
|
"node_modules/prosemirror-commands": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.2.tgz",
|
||||||
"integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==",
|
"integrity": "sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==",
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.0.0",
|
"prosemirror-model": "^1.0.0",
|
||||||
"prosemirror-state": "^1.0.0",
|
"prosemirror-state": "^1.0.0",
|
||||||
"prosemirror-transform": "^1.0.0"
|
"prosemirror-transform": "^1.10.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-dropcursor": {
|
"node_modules/prosemirror-dropcursor": {
|
||||||
@@ -10653,15 +10937,42 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-markdown": {
|
"node_modules/prosemirror-markdown": {
|
||||||
"version": "1.13.0",
|
"version": "1.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz",
|
||||||
"integrity": "sha512-UziddX3ZYSYibgx8042hfGKmukq5Aljp2qoBiJRejD/8MH70siQNz5RB1TrdTPheqLMy4aCe4GYNF10/3lQS5g==",
|
"integrity": "sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==",
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/markdown-it": "^14.0.0",
|
||||||
"markdown-it": "^14.0.0",
|
"markdown-it": "^14.0.0",
|
||||||
"prosemirror-model": "^1.20.0"
|
"prosemirror-model": "^1.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-markdown/node_modules/@types/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown/node_modules/@types/markdown-it": {
|
||||||
|
"version": "14.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||||
|
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "^5",
|
||||||
|
"@types/mdurl": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown/node_modules/@types/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/prosemirror-menu": {
|
"node_modules/prosemirror-menu": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz",
|
||||||
@@ -10675,9 +10986,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-model": {
|
"node_modules/prosemirror-model": {
|
||||||
"version": "1.22.3",
|
"version": "1.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.22.3.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.24.1.tgz",
|
||||||
"integrity": "sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==",
|
"integrity": "sha512-YM053N+vTThzlWJ/AtPtF1j0ebO36nvbmDy4U7qA2XQB8JVaQp1FmB9Jhrps8s+z+uxhhVTny4m20ptUvhk0Mg==",
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"orderedmap": "^2.0.0"
|
"orderedmap": "^2.0.0"
|
||||||
@@ -10715,16 +11027,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-tables": {
|
"node_modules/prosemirror-tables": {
|
||||||
"version": "1.4.0",
|
"version": "1.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.2.tgz",
|
||||||
"integrity": "sha512-fxryZZkQG12fSCNuZDrYx6Xvo2rLYZTbKLRd8rglOPgNJGMKIS8uvTt6gGC38m7UCu/ENnXIP9pEz5uDaPc+cA==",
|
"integrity": "sha512-97dKocVLrEVTQjZ4GBLdrrMw7Gv3no8H8yMwf5IRM9OoHrzbWpcH5jJxYgNQIRCtdIqwDctT1HdMHrGTiwp1dQ==",
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-keymap": "^1.1.2",
|
"prosemirror-keymap": "^1.2.2",
|
||||||
"prosemirror-model": "^1.8.1",
|
"prosemirror-model": "^1.24.1",
|
||||||
"prosemirror-state": "^1.3.1",
|
"prosemirror-state": "^1.4.3",
|
||||||
"prosemirror-transform": "^1.2.1",
|
"prosemirror-transform": "^1.10.2",
|
||||||
"prosemirror-view": "^1.13.3"
|
"prosemirror-view": "^1.37.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-trailing-node": {
|
"node_modules/prosemirror-trailing-node": {
|
||||||
@@ -10743,18 +11056,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-transform": {
|
"node_modules/prosemirror-transform": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz",
|
||||||
"integrity": "sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg==",
|
"integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==",
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.21.0"
|
"prosemirror-model": "^1.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-view": {
|
"node_modules/prosemirror-view": {
|
||||||
"version": "1.33.11",
|
"version": "1.37.1",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.33.11.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.37.1.tgz",
|
||||||
"integrity": "sha512-K0z9oMf6EI2ZifS9yW8PUPjEw2o1ZoFAaNzvcuyfcjIzsU6pJMo3tk9r26MyzEsuGHXZwmKPEmrjgFd78biTGA==",
|
"integrity": "sha512-MEAnjOdXU1InxEmhjgmEzQAikaS6lF3hD64MveTPpjOGNTl87iRLA1HupC/DEV6YuK7m4Q9DHFNTjwIVtqz5NA==",
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.20.0",
|
"prosemirror-model": "^1.20.0",
|
||||||
@@ -10762,6 +11077,13 @@
|
|||||||
"prosemirror-transform": "^1.1.0"
|
"prosemirror-transform": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||||
@@ -14068,6 +14390,7 @@
|
|||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+13
-13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.1.0",
|
"version": "0.2.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
|
"@sentry/cli": "^2.40.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"@types/eslint": "^9.6.0",
|
"@types/eslint": "^9.6.0",
|
||||||
@@ -56,18 +57,18 @@
|
|||||||
"@tiptap/extension-text": "^2.6.6",
|
"@tiptap/extension-text": "^2.6.6",
|
||||||
"@tiptap/suggestion": "^2.6.4",
|
"@tiptap/suggestion": "^2.6.4",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.6",
|
"@vite-pwa/sveltekit": "^0.6.6",
|
||||||
"@welshman/app": "~0.0.27",
|
"@welshman/app": "~0.0.37",
|
||||||
"@welshman/content": "~0.0.12",
|
"@welshman/content": "~0.0.15",
|
||||||
"@welshman/dvm": "~0.0.10",
|
"@welshman/dvm": "~0.0.13",
|
||||||
"@welshman/feeds": "~0.0.25",
|
"@welshman/editor": "~0.0.6",
|
||||||
"@welshman/lib": "~0.0.26",
|
"@welshman/feeds": "~0.0.30",
|
||||||
"@welshman/net": "~0.0.36",
|
"@welshman/lib": "~0.0.37",
|
||||||
"@welshman/signer": "~0.0.14",
|
"@welshman/net": "~0.0.45",
|
||||||
"@welshman/store": "~0.0.12",
|
"@welshman/signer": "~0.0.19",
|
||||||
"@welshman/util": "~0.0.45",
|
"@welshman/store": "~0.0.15",
|
||||||
|
"@welshman/util": "~0.0.57",
|
||||||
"daisyui": "^4.12.10",
|
"daisyui": "^4.12.10",
|
||||||
"date-picker-svelte": "^2.13.0",
|
"date-picker-svelte": "^2.13.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
@@ -80,7 +81,6 @@
|
|||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"svelte-tiptap": "^1.1.3",
|
"svelte-tiptap": "^1.1.3"
|
||||||
"throttle-debounce": "^5.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-3
@@ -40,15 +40,14 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Lato;
|
font-family: Lato;
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme] {
|
|
||||||
--base-100: oklch(var(--b1));
|
--base-100: oklch(var(--b1));
|
||||||
--base-200: oklch(var(--b2));
|
--base-200: oklch(var(--b2));
|
||||||
--base-300: oklch(var(--b3));
|
--base-300: oklch(var(--b3));
|
||||||
--base-content: oklch(var(--bc));
|
--base-content: oklch(var(--bc));
|
||||||
--primary: oklch(var(--p));
|
--primary: oklch(var(--p));
|
||||||
|
--primary-content: oklch(var(--pc));
|
||||||
--secondary: oklch(var(--s));
|
--secondary: oklch(var(--s));
|
||||||
|
--secondary-content: oklch(var(--sc));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-alt,
|
.bg-alt,
|
||||||
@@ -120,6 +119,16 @@
|
|||||||
@apply overflow-hidden text-ellipsis;
|
@apply overflow-hidden text-ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-tip]::before {
|
||||||
|
@apply ellipsize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
[data-tip]::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.content-padding-x {
|
.content-padding-x {
|
||||||
@apply px-4 sm:px-8 md:px-12;
|
@apply px-4 sm:px-8 md:px-12;
|
||||||
}
|
}
|
||||||
@@ -239,3 +248,19 @@ emoji-picker {
|
|||||||
--input-font-color: var(--base-content);
|
--input-font-color: var(--base-content);
|
||||||
--outline-color: var(--base-100);
|
--outline-color: var(--base-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* tiptap */
|
||||||
|
|
||||||
|
.tiptap {
|
||||||
|
--tiptap-object-bg: var(--base-100);
|
||||||
|
--tiptap-object-fg: var(--base-content);
|
||||||
|
--tiptap-active-bg: var(--primary);
|
||||||
|
--tiptap-active-fg: var(--primary-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions {
|
||||||
|
--tiptap-object-bg: var(--base-100);
|
||||||
|
--tiptap-object-fg: var(--base-content);
|
||||||
|
--tiptap-active-bg: var(--base-300);
|
||||||
|
--tiptap-active-fg: var(--base-content);
|
||||||
|
}
|
||||||
|
|||||||
+179
-89
@@ -1,13 +1,20 @@
|
|||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {ctx, uniq, sleep, chunk, equals, choice} from "@welshman/lib"
|
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
DELETE,
|
DELETE,
|
||||||
|
REPORT,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
INBOX_RELAYS,
|
INBOX_RELAYS,
|
||||||
RELAYS,
|
RELAYS,
|
||||||
FOLLOWS,
|
FOLLOWS,
|
||||||
REACTION,
|
REACTION,
|
||||||
AUTH_JOIN,
|
AUTH_JOIN,
|
||||||
|
GROUP_JOIN,
|
||||||
|
GROUP_LEAVE,
|
||||||
|
GROUP_CREATE,
|
||||||
|
GROUP_EDIT_META,
|
||||||
|
GROUPS,
|
||||||
|
COMMENT,
|
||||||
isSignedEvent,
|
isSignedEvent,
|
||||||
createEvent,
|
createEvent,
|
||||||
displayProfile,
|
displayProfile,
|
||||||
@@ -15,16 +22,16 @@ import {
|
|||||||
makeList,
|
makeList,
|
||||||
addToListPublicly,
|
addToListPublicly,
|
||||||
removeFromListByPredicate,
|
removeFromListByPredicate,
|
||||||
|
getTag,
|
||||||
getListTags,
|
getListTags,
|
||||||
getRelayTags,
|
getRelayTags,
|
||||||
isShareableRelayUrl,
|
isShareableRelayUrl,
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, EventTemplate, List} from "@welshman/util"
|
import type {TrustedEvent, EventTemplate, List} from "@welshman/util"
|
||||||
import type {SubscribeRequestWithHandlers, Subscription} from "@welshman/net"
|
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||||
import {PublishStatus, AuthStatus, SocketStatus, SubscriptionEvent} from "@welshman/net"
|
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
|
||||||
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
|
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
|
||||||
import type {Nip46Handler} from "@welshman/signer"
|
|
||||||
import {
|
import {
|
||||||
pubkey,
|
pubkey,
|
||||||
signer,
|
signer,
|
||||||
@@ -46,19 +53,20 @@ import {
|
|||||||
nip44EncryptToSelf,
|
nip44EncryptToSelf,
|
||||||
loadRelay,
|
loadRelay,
|
||||||
addSession,
|
addSession,
|
||||||
nip46Perms,
|
clearStorage,
|
||||||
subscribe,
|
dropSession,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
|
import type {Thunk} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
COMMENT,
|
|
||||||
tagRoom,
|
tagRoom,
|
||||||
|
PROTECTED,
|
||||||
userMembership,
|
userMembership,
|
||||||
MEMBERSHIPS,
|
|
||||||
INDEXER_RELAYS,
|
INDEXER_RELAYS,
|
||||||
|
NIP46_PERMS,
|
||||||
loadMembership,
|
loadMembership,
|
||||||
loadSettings,
|
loadSettings,
|
||||||
getDefaultPubkeys,
|
getDefaultPubkeys,
|
||||||
getMembershipUrls,
|
userRoomsByUrl,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
@@ -78,89 +86,92 @@ export const getPubkeyPetname = (pubkey: string) => {
|
|||||||
return display
|
return display
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeMention = (pubkey: string, hints?: string[]) => [
|
export const getThunkError = async (thunk: Thunk) => {
|
||||||
"p",
|
const result = await thunk.result
|
||||||
pubkey,
|
const [{status, message}] = Object.values(result) as any
|
||||||
choice(hints || getPubkeyHints(pubkey)),
|
|
||||||
getPubkeyPetname(pubkey),
|
|
||||||
]
|
|
||||||
|
|
||||||
export const makeIMeta = (url: string, data: Record<string, string>) => [
|
if (status !== PublishStatus.Success) {
|
||||||
"imeta",
|
return message
|
||||||
`url ${url}`,
|
|
||||||
...Object.entries(data).map(([k, v]) => [k, v].join(" ")),
|
|
||||||
]
|
|
||||||
|
|
||||||
export const subscribePersistent = (request: SubscribeRequestWithHandlers) => {
|
|
||||||
let sub: Subscription
|
|
||||||
let done = false
|
|
||||||
|
|
||||||
const start = async () => {
|
|
||||||
// If the subscription gets closed quickly, don't start flapping
|
|
||||||
await Promise.all([
|
|
||||||
sleep(30_000),
|
|
||||||
new Promise(resolve => {
|
|
||||||
sub = subscribe(request)
|
|
||||||
sub.emitter.on(SubscriptionEvent.Complete, resolve)
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!done) {
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
start()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
done = true
|
|
||||||
sub?.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log in
|
// Log in
|
||||||
|
|
||||||
export const loginWithNip46 = async (token: string, handler: Nip46Handler) => {
|
export const loginWithNip46 = async ({
|
||||||
const secret = makeSecret()
|
relays,
|
||||||
const broker = Nip46Broker.get({secret, handler})
|
signerPubkey,
|
||||||
const result = await broker.connect(token, nip46Perms)
|
clientSecret = makeSecret(),
|
||||||
|
connectSecret = "",
|
||||||
|
}: {
|
||||||
|
relays: string[]
|
||||||
|
signerPubkey: string
|
||||||
|
clientSecret?: string
|
||||||
|
connectSecret?: string
|
||||||
|
}) => {
|
||||||
|
const broker = Nip46Broker.get({relays, clientSecret, signerPubkey})
|
||||||
|
const result = await broker.connect(connectSecret, NIP46_PERMS)
|
||||||
|
|
||||||
if (!result) return false
|
// TODO: remove ack result
|
||||||
|
if (!["ack", connectSecret].includes(result)) return false
|
||||||
|
|
||||||
const pubkey = await broker.getPublicKey()
|
const pubkey = await broker.getPublicKey()
|
||||||
|
|
||||||
if (!pubkey) return false
|
if (!pubkey) return false
|
||||||
|
|
||||||
addSession({method: "nip46", pubkey, secret, handler})
|
await loadUserData(pubkey)
|
||||||
|
|
||||||
|
const handler = {relays, pubkey: signerPubkey}
|
||||||
|
|
||||||
|
addSession({method: "nip46", pubkey, secret: clientSecret, handler})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log out
|
||||||
|
|
||||||
|
export const logout = async () => {
|
||||||
|
const $pubkey = pubkey.get()
|
||||||
|
|
||||||
|
if ($pubkey) {
|
||||||
|
dropSession($pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
await clearStorage()
|
||||||
|
|
||||||
|
localStorage.clear()
|
||||||
|
}
|
||||||
|
|
||||||
// Loaders
|
// Loaders
|
||||||
|
|
||||||
export const loadUserData = (
|
export const loadUserData = (
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
request: Partial<SubscribeRequestWithHandlers> = {},
|
request: Partial<SubscribeRequestWithHandlers> = {},
|
||||||
) => {
|
) => {
|
||||||
const promise = Promise.all([
|
const promise = Promise.race([
|
||||||
loadInboxRelaySelections(pubkey, request),
|
sleep(3000),
|
||||||
loadMembership(pubkey, request),
|
Promise.all([
|
||||||
loadSettings(pubkey, request),
|
loadInboxRelaySelections(pubkey, request),
|
||||||
loadProfile(pubkey, request),
|
loadMembership(pubkey, request),
|
||||||
loadFollows(pubkey, request),
|
loadSettings(pubkey, request),
|
||||||
loadMutes(pubkey, request),
|
loadProfile(pubkey, request),
|
||||||
|
loadFollows(pubkey, request),
|
||||||
|
loadMutes(pubkey, request),
|
||||||
|
]),
|
||||||
])
|
])
|
||||||
|
|
||||||
// Load followed profiles slowly in the background without clogging other stuff up
|
// Load followed profiles slowly in the background without clogging other stuff up. Only use a single
|
||||||
|
// indexer relay to avoid too many redundant validations, which slow things down and eat bandwidth
|
||||||
promise.then(async () => {
|
promise.then(async () => {
|
||||||
for (const pubkeys of chunk(50, getDefaultPubkeys())) {
|
for (const pubkeys of chunk(50, getDefaultPubkeys())) {
|
||||||
await sleep(300)
|
const relays = sample(1, INDEXER_RELAYS)
|
||||||
|
|
||||||
|
await sleep(1000)
|
||||||
|
|
||||||
for (const pubkey of pubkeys) {
|
for (const pubkey of pubkeys) {
|
||||||
loadMembership(pubkey)
|
loadMembership(pubkey, {relays})
|
||||||
loadProfile(pubkey)
|
loadProfile(pubkey, {relays})
|
||||||
loadFollows(pubkey)
|
loadFollows(pubkey, {relays})
|
||||||
loadMutes(pubkey)
|
loadMutes(pubkey, {relays})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -185,10 +196,37 @@ export const broadcastUserData = async (relays: string[]) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NIP 29 stuff
|
||||||
|
|
||||||
|
export const nip29 = {
|
||||||
|
createRoom: (url: string, room: string) => {
|
||||||
|
const event = createEvent(GROUP_CREATE, {tags: [tagRoom(room, url)]})
|
||||||
|
|
||||||
|
return publishThunk({event, relays: [url]})
|
||||||
|
},
|
||||||
|
editMeta: (url: string, room: string, meta: Record<string, string>) => {
|
||||||
|
const event = createEvent(GROUP_EDIT_META, {
|
||||||
|
tags: [tagRoom(room, url), ...Object.entries(meta)],
|
||||||
|
})
|
||||||
|
|
||||||
|
return publishThunk({event, relays: [url]})
|
||||||
|
},
|
||||||
|
joinRoom: (url: string, room: string) => {
|
||||||
|
const event = createEvent(GROUP_JOIN, {tags: [tagRoom(room, url)]})
|
||||||
|
|
||||||
|
return publishThunk({event, relays: [url]})
|
||||||
|
},
|
||||||
|
leaveRoom: (url: string, room: string) => {
|
||||||
|
const event = createEvent(GROUP_LEAVE, {tags: [tagRoom(room, url)]})
|
||||||
|
|
||||||
|
return publishThunk({event, relays: [url]})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// List updates
|
// List updates
|
||||||
|
|
||||||
export const addSpaceMembership = async (url: string) => {
|
export const addSpaceMembership = async (url: string) => {
|
||||||
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
const list = get(userMembership) || makeList({kind: GROUPS})
|
||||||
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
|
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
|
||||||
const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||||
|
|
||||||
@@ -196,7 +234,7 @@ export const addSpaceMembership = async (url: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const removeSpaceMembership = async (url: string) => {
|
export const removeSpaceMembership = async (url: string) => {
|
||||||
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
const list = get(userMembership) || makeList({kind: GROUPS})
|
||||||
const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
|
const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
|
||||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||||
const relays = uniq([
|
const relays = uniq([
|
||||||
@@ -208,17 +246,21 @@ export const removeSpaceMembership = async (url: string) => {
|
|||||||
return publishThunk({event, relays})
|
return publishThunk({event, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addRoomMembership = async (url: string, room: string) => {
|
export const addRoomMembership = async (url: string, room: string, name: string) => {
|
||||||
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
const list = get(userMembership) || makeList({kind: GROUPS})
|
||||||
const event = await addToListPublicly(list, tagRoom(room, url)).reconcile(nip44EncryptToSelf)
|
const newTags = [
|
||||||
|
["r", url],
|
||||||
|
["group", room, url, name],
|
||||||
|
]
|
||||||
|
const event = await addToListPublicly(list, ...newTags).reconcile(nip44EncryptToSelf)
|
||||||
const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||||
|
|
||||||
return publishThunk({event, relays})
|
return publishThunk({event, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeRoomMembership = async (url: string, room: string) => {
|
export const removeRoomMembership = async (url: string, room: string) => {
|
||||||
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
const list = get(userMembership) || makeList({kind: GROUPS})
|
||||||
const pred = (t: string[]) => equals(tagRoom(room, url), t)
|
const pred = (t: string[]) => equals(["group", room, url], t.slice(0, 3))
|
||||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||||
const relays = uniq([
|
const relays = uniq([
|
||||||
url,
|
url,
|
||||||
@@ -247,7 +289,7 @@ export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
|
|||||||
url,
|
url,
|
||||||
...INDEXER_RELAYS,
|
...INDEXER_RELAYS,
|
||||||
...ctx.app.router.FromUser().getUrls(),
|
...ctx.app.router.FromUser().getUrls(),
|
||||||
...getMembershipUrls(userMembership.get()),
|
...userRoomsByUrl.get().keys(),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -268,7 +310,7 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
|||||||
relays: [
|
relays: [
|
||||||
...INDEXER_RELAYS,
|
...INDEXER_RELAYS,
|
||||||
...ctx.app.router.FromUser().getUrls(),
|
...ctx.app.router.FromUser().getUrls(),
|
||||||
...getMembershipUrls(userMembership.get()),
|
...userRoomsByUrl.get().keys(),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -288,13 +330,17 @@ export const checkRelayAccess = async (url: string, claim = "") => {
|
|||||||
|
|
||||||
const result = await thunk.result
|
const result = await thunk.result
|
||||||
|
|
||||||
if (result[url].status !== PublishStatus.Success) {
|
if (result[url].status === PublishStatus.Failure) {
|
||||||
const message =
|
const message =
|
||||||
connection.auth.message?.replace(/^.*: /, "") ||
|
connection.auth.message?.replace(/^.*: /, "") ||
|
||||||
result[url].message?.replace(/^.*: /, "") ||
|
result[url].message?.replace(/^.*: /, "") ||
|
||||||
"join request rejected"
|
"join request rejected"
|
||||||
|
|
||||||
return `Failed to join relay (${message})`
|
// If it's a strict NIP 29 relay don't worry about requesting access
|
||||||
|
// TODO: remove this if relay29 ever gets less strict
|
||||||
|
if (message !== "missing group (`h`) tag") {
|
||||||
|
return `Failed to join relay (${message})`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,10 +376,14 @@ export const checkRelayAuth = async (url: string, timeout = 3000) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const attemptRelayAccess = async (url: string, claim = "") => {
|
export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||||
const checks = [checkRelayProfile, checkRelayConnection, checkRelayAccess, checkRelayAuth]
|
const checks = [
|
||||||
|
() => checkRelayConnection(url),
|
||||||
|
() => checkRelayAccess(url, claim),
|
||||||
|
() => checkRelayAuth(url),
|
||||||
|
]
|
||||||
|
|
||||||
for (const check of checks) {
|
for (const check of checks) {
|
||||||
const error = await check(url)
|
const error = await check()
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return error
|
return error
|
||||||
@@ -365,14 +415,60 @@ export const sendWrapped = async ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const makeDelete = ({event}: {event: TrustedEvent}) => {
|
||||||
|
const tags = [["k", String(event.kind)], ...tagEvent(event)]
|
||||||
|
const groupTag = getTag("h", event.tags)
|
||||||
|
|
||||||
|
if (groupTag) {
|
||||||
|
tags.push(PROTECTED)
|
||||||
|
tags.push(groupTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createEvent(DELETE, {tags})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
|
||||||
|
publishThunk({event: makeDelete({event}), relays})
|
||||||
|
|
||||||
|
export type ReportParams = {
|
||||||
|
event: TrustedEvent
|
||||||
|
content: string
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeReport = ({event, reason, content}: ReportParams) => {
|
||||||
|
const tags = [
|
||||||
|
["p", event.pubkey],
|
||||||
|
["e", event.id, reason],
|
||||||
|
]
|
||||||
|
|
||||||
|
return createEvent(REPORT, {content, tags})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publishReport = ({
|
||||||
|
relays,
|
||||||
|
event,
|
||||||
|
reason,
|
||||||
|
content,
|
||||||
|
}: ReportParams & {relays: string[]}) =>
|
||||||
|
publishThunk({event: makeReport({event, reason, content}), relays})
|
||||||
|
|
||||||
export type ReactionParams = {
|
export type ReactionParams = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
content: string
|
content: string
|
||||||
tags?: string[][]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeReaction = ({event, content, tags = []}: ReactionParams) =>
|
export const makeReaction = ({event, content}: ReactionParams) => {
|
||||||
createEvent(REACTION, {content, tags: [...tags, ...tagReactionTo(event)]})
|
const tags = [["k", String(event.kind)], ...tagReactionTo(event)]
|
||||||
|
const groupTag = getTag("h", event.tags)
|
||||||
|
|
||||||
|
if (groupTag) {
|
||||||
|
tags.push(PROTECTED)
|
||||||
|
tags.push(groupTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createEvent(REACTION, {content, tags})
|
||||||
|
}
|
||||||
|
|
||||||
export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) =>
|
export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) =>
|
||||||
publishThunk({event: makeReaction(params), relays})
|
publishThunk({event: makeReaction(params), relays})
|
||||||
@@ -401,19 +497,13 @@ export const makeComment = ({event, content, tags = []}: ReplyParams) => {
|
|||||||
if (seenRoots.size === 0) {
|
if (seenRoots.size === 0) {
|
||||||
tags.push(["K", String(event.kind)])
|
tags.push(["K", String(event.kind)])
|
||||||
tags.push(["E", event.id])
|
tags.push(["E", event.id])
|
||||||
} else {
|
|
||||||
tags.push(["k", String(event.kind)])
|
|
||||||
tags.push(["e", event.id])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tags.push(["k", String(event.kind)])
|
||||||
|
tags.push(["e", event.id])
|
||||||
|
|
||||||
return createEvent(COMMENT, {content, tags})
|
return createEvent(COMMENT, {content, tags})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publishComment = ({relays, ...params}: ReplyParams & {relays: string[]}) =>
|
export const publishComment = ({relays, ...params}: ReplyParams & {relays: string[]}) =>
|
||||||
publishThunk({event: makeComment(params), relays})
|
publishThunk({event: makeComment(params), relays})
|
||||||
|
|
||||||
export const makeDelete = ({event}: {event: TrustedEvent}) =>
|
|
||||||
createEvent(DELETE, {tags: [["k", String(event.kind)], ...tagEvent(event)]})
|
|
||||||
|
|
||||||
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
|
|
||||||
publishThunk({event: makeDelete({event}), relays})
|
|
||||||
|
|||||||
@@ -4,7 +4,26 @@
|
|||||||
import Landing from "@app/components/Landing.svelte"
|
import Landing from "@app/components/Landing.svelte"
|
||||||
import Toast from "@app/components/Toast.svelte"
|
import Toast from "@app/components/Toast.svelte"
|
||||||
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
||||||
import {modals} from "@app/modal"
|
import EmailConfirm from "@app/components/EmailConfirm.svelte"
|
||||||
|
import PasswordReset from "@app/components/PasswordReset.svelte"
|
||||||
|
import {BURROW_URL} from "@app/state"
|
||||||
|
import {modals, pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
if (BURROW_URL && !$pubkey) {
|
||||||
|
if ($page.url.pathname === "/confirm-email") {
|
||||||
|
pushModal(EmailConfirm, {
|
||||||
|
email: $page.url.searchParams.get("email"),
|
||||||
|
confirm_token: $page.url.searchParams.get("confirm_token"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($page.url.pathname === "/reset-password") {
|
||||||
|
pushModal(PasswordReset, {
|
||||||
|
email: $page.url.searchParams.get("email"),
|
||||||
|
reset_token: $page.url.searchParams.get("reset_token"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen overflow-hidden">
|
<div class="flex h-screen overflow-hidden">
|
||||||
|
|||||||
@@ -1,60 +1,63 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import type {Readable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
import {getEditor} from "@app/editor"
|
||||||
import {getPubkeyHints} from "@app/commands"
|
|
||||||
|
|
||||||
export let onSubmit
|
export let onSubmit: any
|
||||||
export let content = ""
|
export let content = ""
|
||||||
|
export let editor: ReturnType<typeof getEditor> | undefined = undefined
|
||||||
|
|
||||||
let editor: Readable<Editor>
|
const uploading = writable(false)
|
||||||
|
|
||||||
|
let element: HTMLElement
|
||||||
|
|
||||||
|
const uploadFiles = () => $editor!.chain().selectFiles().run()
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if ($loading) return
|
if ($uploading) return
|
||||||
|
|
||||||
onSubmit({
|
const content = $editor!.getText({blockSeparator: "\n"}).trim()
|
||||||
content: $editor.getText({blockSeparator: "\n"}),
|
const tags = $editor!.storage.nostr.getEditorTags()
|
||||||
tags: getEditorTags($editor),
|
|
||||||
})
|
|
||||||
|
|
||||||
$editor.chain().clearContent().run()
|
if (!content) return
|
||||||
|
|
||||||
|
onSubmit({content, tags})
|
||||||
|
|
||||||
|
$editor!.chain().clearContent().run()
|
||||||
}
|
}
|
||||||
|
|
||||||
$: loading = $editor?.storage.fileUpload.loading
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor = createEditor(
|
editor = getEditor({autofocus: !isMobile, element, submit, uploading})
|
||||||
getEditorOptions({
|
|
||||||
submit,
|
|
||||||
getPubkeyHints,
|
|
||||||
submitOnEnter: true,
|
|
||||||
autofocus: !isMobile,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
$editor.commands.setContent(content)
|
$editor!.chain().setContent(content).run()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="relative z-feature flex gap-2 p-2"
|
class="relative z-feature flex gap-2 p-2"
|
||||||
on:submit|preventDefault={$loading ? undefined : submit}>
|
on:submit|preventDefault={$uploading ? undefined : submit}>
|
||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
|
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
|
||||||
disabled={$loading}
|
disabled={$uploading}
|
||||||
on:click={$editor.commands.selectFiles}>
|
on:click={uploadFiles}>
|
||||||
{#if $loading}
|
{#if $uploading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon="gallery-send" />
|
<Icon icon="gallery-send" />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<div class="chat-editor flex-grow overflow-hidden">
|
<div class="chat-editor flex-grow overflow-hidden">
|
||||||
<EditorContent editor={$editor} />
|
<div bind:this={element} />
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
||||||
|
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
||||||
|
disabled={$uploading}
|
||||||
|
on:click={submit}>
|
||||||
|
<Icon icon="plain" />
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {sortBy, append} from "@welshman/lib"
|
|
||||||
import type {EventContent, TrustedEvent} from "@welshman/util"
|
|
||||||
import {repository} from "@welshman/app"
|
|
||||||
import {deriveEvents} from "@welshman/store"
|
|
||||||
import ChannelMessage from "@app/components/ChannelMessage.svelte"
|
|
||||||
import ChannelCompose from "@app/components/ChannelCompose.svelte"
|
|
||||||
import {tagRoom, COMMENT} from "@app/state"
|
|
||||||
import {publishComment} from "@app/commands"
|
|
||||||
|
|
||||||
export let url, room, event: TrustedEvent
|
|
||||||
|
|
||||||
const replies = deriveEvents(repository, {
|
|
||||||
filters: [{kinds: [COMMENT], "#E": [event.id]}],
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit = ({content, tags}: EventContent) =>
|
|
||||||
publishComment({
|
|
||||||
event,
|
|
||||||
content,
|
|
||||||
tags: append(tagRoom(room, url), tags),
|
|
||||||
relays: [url],
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="col-2">
|
|
||||||
<div class="overflow-auto pt-3">
|
|
||||||
<ChannelMessage {url} {room} {event} showPubkey isHead inert />
|
|
||||||
{#each sortBy(e => e.created_at, $replies) as reply (reply.id)}
|
|
||||||
<ChannelMessage {url} {room} event={reply} showPubkey inert />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<div class="bottom-0 left-0 right-0">
|
|
||||||
<ChannelCompose {onSubmit} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {readable} from "svelte/store"
|
|
||||||
import {hash} from "@welshman/lib"
|
import {hash} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
@@ -12,104 +11,89 @@
|
|||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import LongPress from "@lib/components/LongPress.svelte"
|
import LongPress from "@lib/components/LongPress.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
||||||
import ReplySummary from "@app/components/ReplySummary.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ChannelConversation from "@app/components/ChannelConversation.svelte"
|
|
||||||
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
|
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
|
||||||
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
|
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
|
||||||
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
|
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
|
||||||
import {colors, tagRoom, deriveEvent, pubkeyLink} from "@app/state"
|
import {colors} from "@app/state"
|
||||||
import {publishDelete, publishReaction} from "@app/commands"
|
import {publishDelete, publishReaction} from "@app/commands"
|
||||||
import {pushDrawer, pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let url, room
|
export let url, room
|
||||||
export let event: TrustedEvent
|
export let event: TrustedEvent
|
||||||
|
export let replyTo: any = undefined
|
||||||
export let showPubkey = false
|
export let showPubkey = false
|
||||||
export let isHead = false
|
|
||||||
export let inert = false
|
export let inert = false
|
||||||
|
|
||||||
const thunk = $thunks[event.id]
|
const thunk = $thunks[event.id]
|
||||||
const profile = deriveProfile(event.pubkey)
|
const profile = deriveProfile(event.pubkey)
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||||
const rootTag = event.tags.find(t => t[0].match(/^e$/i))
|
|
||||||
const rootId = rootTag?.[1]
|
|
||||||
const rootHints = [rootTag?.[2]].filter(Boolean) as string[]
|
|
||||||
const rootEvent = rootId ? deriveEvent(rootId, rootHints) : readable(null)
|
|
||||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
||||||
|
|
||||||
const onClick = () => {
|
const reply = () => replyTo(event)
|
||||||
const root = $rootEvent || event
|
|
||||||
|
|
||||||
pushDrawer(ChannelConversation, {url, room, event: root})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event})
|
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event})
|
||||||
|
|
||||||
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
const reaction = events.find(e => e.pubkey === $pubkey)
|
||||||
|
|
||||||
if (reaction) {
|
if (reaction) {
|
||||||
publishDelete({relays: [url], event: reaction})
|
publishDelete({relays: [url], event: reaction})
|
||||||
} else {
|
} else {
|
||||||
publishReaction({
|
publishReaction({event, content, relays: [url]})
|
||||||
event,
|
|
||||||
content,
|
|
||||||
relays: [url],
|
|
||||||
tags: [tagRoom(room, url)],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LongPress
|
<LongPress
|
||||||
on:click={isMobile || inert ? null : onClick}
|
data-event={event.id}
|
||||||
onLongPress={inert ? null : onLongPress}
|
onLongPress={inert ? null : onLongPress}
|
||||||
class="group relative flex w-full flex-col gap-1 p-2 text-left transition-colors {inert
|
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left">
|
||||||
? 'hover:bg-base-300'
|
|
||||||
: ''}">
|
|
||||||
<div class="flex w-full gap-3 overflow-auto">
|
<div class="flex w-full gap-3 overflow-auto">
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<Link external href={pubkeyLink(event.pubkey)} class="flex items-start">
|
<Button on:click={openProfile} class="flex items-start">
|
||||||
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={10} />
|
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={8} />
|
||||||
</Link>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="w-10 min-w-10 max-w-10" />
|
<div class="w-8 min-w-8 max-w-8" />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="-mt-1 min-w-0 flex-grow pr-1">
|
<div class="min-w-0 flex-grow pr-1">
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Link
|
<Button on:click={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||||
external
|
|
||||||
href={pubkeyLink(event.pubkey)}
|
|
||||||
class="text-sm font-bold"
|
|
||||||
style="color: {colorValue}">
|
|
||||||
{$profileDisplay}
|
{$profileDisplay}
|
||||||
</Link>
|
</Button>
|
||||||
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
|
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<Content {event} />
|
<Content {event} quoteProps={{minimal: true, relays: [url]}} />
|
||||||
{#if thunk}
|
{#if thunk}
|
||||||
<ThunkStatus {thunk} class="mt-2" />
|
<ThunkStatus {thunk} class="mt-2" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-2 ml-12">
|
<div class="row-2 ml-10 mt-1">
|
||||||
{#if !isHead}
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
|
||||||
<ReplySummary relays={[url]} {event} on:click={onClick} />
|
|
||||||
{/if}
|
|
||||||
<ReactionSummary relays={[url]} {event} {onReactionClick} />
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||||
class:group-hover:opacity-100={!isMobile}
|
class:group-hover:opacity-100={!isMobile}
|
||||||
on:click|stopPropagation>
|
on:click|stopPropagation>
|
||||||
<ChannelMessageEmojiButton {url} {room} {event} />
|
<ChannelMessageEmojiButton {url} {room} {event} />
|
||||||
|
{#if replyTo}
|
||||||
|
<Button class="btn join-item btn-xs" on:click={reply}>
|
||||||
|
<Icon icon="reply" size={4} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<ChannelMessageMenuButton {url} {event} />
|
<ChannelMessageMenuButton {url} {event} />
|
||||||
</button>
|
</button>
|
||||||
</LongPress>
|
</LongPress>
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {noop} from "@welshman/lib"
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import {tagRoom} from "@app/state"
|
|
||||||
import {publishReaction} from "@app/commands"
|
import {publishReaction} from "@app/commands"
|
||||||
|
|
||||||
export let url, room, event
|
export let url, room, event
|
||||||
|
|
||||||
|
// Tell svelte-check to shut up
|
||||||
|
noop(room)
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) =>
|
const onEmoji = (emoji: NativeEmoji) =>
|
||||||
publishReaction({
|
publishReaction({event, relays: [url], content: emoji.unicode})
|
||||||
event,
|
|
||||||
relays: [url],
|
|
||||||
content: emoji.unicode,
|
|
||||||
tags: [tagRoom(room, url)],
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<EmojiButton {onEmoji} class="btn join-item btn-xs">
|
<EmojiButton {onEmoji} class="btn join-item btn-xs">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
|
import EventReport from "@app/components/EventReport.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
@@ -10,6 +11,11 @@
|
|||||||
export let event
|
export let event
|
||||||
export let onClick
|
export let onClick
|
||||||
|
|
||||||
|
const report = () => {
|
||||||
|
onClick()
|
||||||
|
pushModal(EventReport, {url, event})
|
||||||
|
}
|
||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(EventInfo, {event})
|
pushModal(EventInfo, {event})
|
||||||
@@ -35,5 +41,12 @@
|
|||||||
Delete Message
|
Delete Message
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" on:click={report}>
|
||||||
|
<Icon size={4} icon="danger" />
|
||||||
|
Report Content
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -5,31 +5,20 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import ChannelConversation from "@app/components/ChannelConversation.svelte"
|
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
||||||
import {publishReaction} from "@app/commands"
|
import {publishReaction} from "@app/commands"
|
||||||
import {pushModal, pushDrawer} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {tagRoom} from "@app/state"
|
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
export let room
|
|
||||||
export let event
|
export let event
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) => {
|
const onEmoji = (emoji: NativeEmoji) => {
|
||||||
history.back()
|
history.back()
|
||||||
publishReaction({
|
publishReaction({event, relays: [url], content: emoji.unicode})
|
||||||
event,
|
|
||||||
relays: [url],
|
|
||||||
content: emoji.unicode,
|
|
||||||
tags: [tagRoom(room, url)],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
||||||
|
|
||||||
const showConversation = () =>
|
|
||||||
pushDrawer(ChannelConversation, {url, room, event}, {replaceState: true})
|
|
||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {event}, {replaceState: true})
|
const showInfo = () => pushModal(EventInfo, {event}, {replaceState: true})
|
||||||
|
|
||||||
const showDelete = () => pushModal(ConfirmDelete, {url, event})
|
const showDelete = () => pushModal(ConfirmDelete, {url, event})
|
||||||
@@ -40,10 +29,6 @@
|
|||||||
<Icon size={4} icon="smile-circle" />
|
<Icon size={4} icon="smile-circle" />
|
||||||
Send Reaction
|
Send Reaction
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-neutral w-full" on:click={showConversation}>
|
|
||||||
<Icon size={4} icon="reply" />
|
|
||||||
View Conversation
|
|
||||||
</Button>
|
|
||||||
<Button class="btn btn-neutral" on:click={showInfo}>
|
<Button class="btn btn-neutral" on:click={showInfo}>
|
||||||
<Icon size={4} icon="code-2" />
|
<Icon size={4} icon="code-2" />
|
||||||
Message Details
|
Message Details
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {GENERAL, channelsById, makeChannelId} from "@app/state"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let room
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if room === GENERAL}
|
||||||
|
general
|
||||||
|
{:else}
|
||||||
|
{$channelsById.get(makeChannelId(url, room))?.name || room}
|
||||||
|
{/if}
|
||||||
@@ -10,7 +10,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {int, nthNe, MINUTE, sortBy, remove} from "@welshman/lib"
|
import type {Readable} from "svelte/store"
|
||||||
|
import type {Editor} from "svelte-tiptap"
|
||||||
|
import {nip19} from "nostr-tools"
|
||||||
|
import {int, nthNe, MINUTE, sortBy, remove, ctx} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {createEvent, DIRECT_MESSAGE, INBOX_RELAYS} from "@welshman/util"
|
import {createEvent, DIRECT_MESSAGE, INBOX_RELAYS} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
@@ -29,10 +32,11 @@
|
|||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ProfileList from "@app/components/ProfileList.svelte"
|
import ProfileList from "@app/components/ProfileList.svelte"
|
||||||
import ChatMessage from "@app/components/ChatMessage.svelte"
|
import ChatMessage from "@app/components/ChatMessage.svelte"
|
||||||
import ChatCompose from "@app/components/ChannelCompose.svelte"
|
import ChatCompose from "@app/components/ChannelCompose.svelte"
|
||||||
import {userSettingValues, deriveChat, splitChatId, PLATFORM_NAME, pubkeyLink} from "@app/state"
|
import {userSettingValues, deriveChat, splitChatId, PLATFORM_NAME} from "@app/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {sendWrapped} from "@app/commands"
|
import {sendWrapped} from "@app/commands"
|
||||||
|
|
||||||
@@ -52,6 +56,15 @@
|
|||||||
const showMembers = () =>
|
const showMembers = () =>
|
||||||
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
|
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
|
||||||
|
|
||||||
|
const replyTo = (event: TrustedEvent) => {
|
||||||
|
const relays = ctx.app.router.Event(event).getUrls()
|
||||||
|
const nevent = nip19.neventEncode({...event, relays})
|
||||||
|
|
||||||
|
$editor.commands.insertNEvent({nevent})
|
||||||
|
$editor.commands.insertContent("\n")
|
||||||
|
$editor.commands.focus()
|
||||||
|
}
|
||||||
|
|
||||||
const onSubmit = async ({content, ...params}: EventContent) => {
|
const onSubmit = async ({content, ...params}: EventContent) => {
|
||||||
// Remove p tags since they result in forking the conversation
|
// Remove p tags since they result in forking the conversation
|
||||||
const tags = [...params.tags.filter(nthNe(0, "p")), ...remove($pubkey!, pubkeys).map(tagPubkey)]
|
const tags = [...params.tags.filter(nthNe(0, "p")), ...remove($pubkey!, pubkeys).map(tagPubkey)]
|
||||||
@@ -64,6 +77,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let loading = true
|
let loading = true
|
||||||
|
let editor: Readable<Editor>
|
||||||
let elements: Element[] = []
|
let elements: Element[] = []
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
@@ -112,10 +126,11 @@
|
|||||||
<div slot="title" class="flex flex-col gap-1 sm:flex-row sm:gap-2">
|
<div slot="title" class="flex flex-col gap-1 sm:flex-row sm:gap-2">
|
||||||
{#if others.length === 1}
|
{#if others.length === 1}
|
||||||
{@const pubkey = others[0]}
|
{@const pubkey = others[0]}
|
||||||
<Link external href={pubkeyLink(pubkey)} class="row-2">
|
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
|
||||||
|
<Button on:click={onClick} class="row-2">
|
||||||
<ProfileCircle {pubkey} size={5} />
|
<ProfileCircle {pubkey} size={5} />
|
||||||
<ProfileName {pubkey} />
|
<ProfileName {pubkey} />
|
||||||
</Link>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ProfileCircles pubkeys={others} size={5} />
|
<ProfileCircles pubkeys={others} size={5} />
|
||||||
@@ -170,7 +185,7 @@
|
|||||||
{#if type === "date"}
|
{#if type === "date"}
|
||||||
<Divider>{value}</Divider>
|
<Divider>{value}</Divider>
|
||||||
{:else}
|
{:else}
|
||||||
<ChatMessage event={assertEvent(value)} {pubkeys} {showPubkey} />
|
<ChatMessage event={assertEvent(value)} {pubkeys} {showPubkey} {replyTo} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<p
|
<p
|
||||||
@@ -185,5 +200,5 @@
|
|||||||
<slot name="info" />
|
<slot name="info" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ChatCompose {onSubmit} />
|
<ChatCompose bind:editor {onSubmit} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
|
import {WRAP} from "@welshman/util"
|
||||||
|
import {repository} from "@welshman/app"
|
||||||
|
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 {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/state"
|
||||||
|
import {clearModals} from "@app/modal"
|
||||||
|
|
||||||
|
export let next
|
||||||
|
|
||||||
|
let loading = false
|
||||||
|
|
||||||
|
const enableChat = async () => {
|
||||||
|
canDecrypt.set(true)
|
||||||
|
|
||||||
|
for (const event of repository.query([{kinds: [WRAP]}])) {
|
||||||
|
ensureUnwrapped(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearModals()
|
||||||
|
goto(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await enableChat()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" on:submit|preventDefault={submit}>
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Enable Messages</div>
|
||||||
|
<div slot="info">Do you want to enable direct messages?</div>
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
By default, direct messages are disabled, since loading them requires
|
||||||
|
{PLATFORM_NAME} to download and decrypt a lot of data.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you'd like to enable them, please make sure your signer is set up to to auto-approve requests
|
||||||
|
to decrypt data.
|
||||||
|
</p>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" on:click={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Enable Messages</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {remove, assoc} from "@welshman/lib"
|
import {remove} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey, loadInboxRelaySelections} from "@welshman/app"
|
import {pubkey, loadInboxRelaySelections} from "@welshman/app"
|
||||||
import {fade} from "@lib/transition"
|
import {fade} from "@lib/transition"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import {makeChatPath} from "@app/routes"
|
import {makeChatPath} from "@app/routes"
|
||||||
import {CHAT_FILTERS, deriveNotification} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
|
|
||||||
export let id: string
|
export let id: string
|
||||||
export let pubkeys: string[]
|
export let pubkeys: string[]
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
const others = remove($pubkey!, pubkeys)
|
const others = remove($pubkey!, pubkeys)
|
||||||
const active = $page.params.chat === id
|
const active = $page.params.chat === id
|
||||||
const path = makeChatPath(pubkeys)
|
const path = makeChatPath(pubkeys)
|
||||||
const notification = deriveNotification(path, CHAT_FILTERS.map(assoc("authors", pubkeys)))
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
for (const pk of others) {
|
for (const pk of others) {
|
||||||
@@ -47,7 +46,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if !active && $notification}
|
{#if !active && $notifications.has(path)}
|
||||||
<div class="h-2 w-2 rounded-full bg-primary" transition:fade />
|
<div class="h-2 w-2 rounded-full bg-primary" transition:fade />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ChatStart from "@app/components/ChatStart.svelte"
|
||||||
|
import {setChecked} from "@app/notifications"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
||||||
|
|
||||||
|
const markAsRead = () => {
|
||||||
|
setChecked("/chat/*")
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="col-2">
|
||||||
|
<Button class="btn btn-primary" on:click={startChat}>
|
||||||
|
<Icon size={4} icon="add-circle" />
|
||||||
|
Start chat
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-neutral" on:click={markAsRead}>
|
||||||
|
<Icon size={4} icon="check-circle" />
|
||||||
|
Mark all read
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
@@ -11,25 +11,27 @@
|
|||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
import LongPress from "@lib/components/LongPress.svelte"
|
import LongPress from "@lib/components/LongPress.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ReplySummary from "@app/components/ReplySummary.svelte"
|
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
||||||
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
||||||
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
||||||
import {colors, pubkeyLink} from "@app/state"
|
import {colors} from "@app/state"
|
||||||
import {makeDelete, makeReaction, sendWrapped} from "@app/commands"
|
import {makeDelete, makeReaction, sendWrapped} from "@app/commands"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let event: TrustedEvent
|
export let event: TrustedEvent
|
||||||
|
export let replyTo: any = undefined
|
||||||
export let pubkeys: string[]
|
export let pubkeys: string[]
|
||||||
export let showPubkey = false
|
export let showPubkey = false
|
||||||
|
|
||||||
const thunk = $thunks[event.id]
|
const thunk = $thunks[event.id]
|
||||||
|
const isOwn = event.pubkey === $pubkey
|
||||||
const profile = deriveProfile(event.pubkey)
|
const profile = deriveProfile(event.pubkey)
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
||||||
@@ -41,6 +43,8 @@
|
|||||||
await sendWrapped({template, pubkeys})
|
await sendWrapped({template, pubkeys})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys})
|
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys})
|
||||||
|
|
||||||
const togglePopover = () => {
|
const togglePopover = () => {
|
||||||
@@ -59,14 +63,15 @@
|
|||||||
<ThunkStatus {thunk} class="mt-1" />
|
<ThunkStatus {thunk} class="mt-1" />
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
|
data-event={event.id}
|
||||||
class="group chat flex items-center justify-end gap-1 px-2"
|
class="group chat flex items-center justify-end gap-1 px-2"
|
||||||
class:chat-start={event.pubkey !== $pubkey}
|
class:chat-start={!isOwn}
|
||||||
class:flex-row-reverse={event.pubkey !== $pubkey}
|
class:flex-row-reverse={!isOwn}
|
||||||
class:chat-end={event.pubkey === $pubkey}>
|
class:chat-end={isOwn}>
|
||||||
<Tippy
|
<Tippy
|
||||||
bind:popover
|
bind:popover
|
||||||
component={ChatMessageMenu}
|
component={ChatMessageMenu}
|
||||||
props={{event, pubkeys, popover}}
|
props={{event, pubkeys, popover, replyTo}}
|
||||||
params={{
|
params={{
|
||||||
interactive: true,
|
interactive: true,
|
||||||
trigger: "manual",
|
trigger: "manual",
|
||||||
@@ -85,27 +90,28 @@
|
|||||||
<Icon icon="menu-dots" size={4} />
|
<Icon icon="menu-dots" size={4} />
|
||||||
</button>
|
</button>
|
||||||
</Tippy>
|
</Tippy>
|
||||||
<div class="flex min-w-0 flex-col" class:items-end={event.pubkey === $pubkey}>
|
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
||||||
<LongPress
|
<LongPress
|
||||||
class="bg-alt chat-bubble mx-1 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
class="bg-alt chat-bubble mx-1 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
||||||
onLongPress={showMobileMenu}>
|
onLongPress={showMobileMenu}>
|
||||||
{#if showPubkey && event.pubkey !== $pubkey}
|
{#if showPubkey}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Link external href={pubkeyLink(event.pubkey)} class="flex items-center gap-1">
|
{#if !isOwn}
|
||||||
<Avatar
|
<Button on:click={openProfile} class="flex items-center gap-1">
|
||||||
src={$profile?.picture}
|
<Avatar
|
||||||
class="border border-solid border-base-content"
|
src={$profile?.picture}
|
||||||
size={4} />
|
class="border border-solid border-base-content"
|
||||||
<div class="flex items-center gap-2">
|
size={4} />
|
||||||
<Link
|
<div class="flex items-center gap-2">
|
||||||
external
|
<Button
|
||||||
href={pubkeyLink(event.pubkey)}
|
on:click={openProfile}
|
||||||
class="text-sm font-bold"
|
class="text-sm font-bold"
|
||||||
style="color: {colorValue}">
|
style="color: {colorValue}">
|
||||||
{$profileDisplay}
|
{$profileDisplay}
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Button>
|
||||||
|
{/if}
|
||||||
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
|
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -114,8 +120,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</LongPress>
|
</LongPress>
|
||||||
<div class="row-2 z-feature -mt-1 ml-4">
|
<div class="row-2 z-feature -mt-1 ml-4">
|
||||||
<ReplySummary {event} />
|
<ReactionSummary {event} {onReactionClick} noTooltip />
|
||||||
<ReactionSummary {event} {onReactionClick} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
export let event
|
export let event
|
||||||
export let pubkeys
|
export let pubkeys
|
||||||
export let popover
|
export let popover
|
||||||
|
export let replyTo
|
||||||
|
|
||||||
|
const reply = () => replyTo(event)
|
||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
popover.hide()
|
popover.hide()
|
||||||
@@ -17,6 +20,11 @@
|
|||||||
|
|
||||||
<div class="join border border-solid border-neutral text-xs">
|
<div class="join border border-solid border-neutral text-xs">
|
||||||
<ChatMessageEmojiButton {event} {pubkeys} />
|
<ChatMessageEmojiButton {event} {pubkeys} />
|
||||||
|
{#if replyTo}
|
||||||
|
<Button class="btn join-item btn-xs" on:click={reply}>
|
||||||
|
<Icon size={4} icon="reply" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<Button class="btn join-item btn-xs" on:click={showInfo}>
|
<Button class="btn join-item btn-xs" on:click={showInfo}>
|
||||||
<Icon size={4} icon="code-2" />
|
<Icon size={4} icon="code-2" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import {
|
import {
|
||||||
parse,
|
parse,
|
||||||
truncate,
|
truncate,
|
||||||
render as renderParsed,
|
renderAsHtml,
|
||||||
isText,
|
isText,
|
||||||
isTopic,
|
isTopic,
|
||||||
isCode,
|
isCode,
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
export let showEntire = false
|
export let showEntire = false
|
||||||
export let hideMedia = false
|
export let hideMedia = false
|
||||||
export let expandMode = "block"
|
export let expandMode = "block"
|
||||||
|
export let quoteProps: Record<string, any> = {}
|
||||||
export let depth = 0
|
export let depth = 0
|
||||||
|
|
||||||
const fullContent = parse(event)
|
const fullContent = parse(event)
|
||||||
@@ -44,18 +45,38 @@
|
|||||||
showEntire = true
|
showEntire = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBoundary = (i: number) => {
|
const isBlock = (i: number) => {
|
||||||
const parsed = fullContent[i]
|
const parsed = fullContent[i]
|
||||||
|
|
||||||
if (!parsed || isNewline(parsed)) return true
|
if (!parsed || hideMedia) return false
|
||||||
if (isText(parsed)) return parsed.value.match(/^\s+$/)
|
|
||||||
|
if (isLink(parsed) && $userSettingValues.show_media && isStartOrEnd(i)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i) && depth < 1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStartAndEnd = (i: number) => Boolean(isBoundary(i - 1) && isBoundary(i + 1))
|
const isBoundary = (i: number) => {
|
||||||
|
const parsed = fullContent[i]
|
||||||
|
|
||||||
const isStartOrEnd = (i: number) => Boolean(isBoundary(i - 1) || isBoundary(i + 1))
|
if (!parsed || isNewline(parsed)) return true
|
||||||
|
if (isText(parsed)) return Boolean(parsed.value.match(/^\s+$/))
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isStart = (i: number) => isBoundary(i - 1)
|
||||||
|
|
||||||
|
const isEnd = (i: number) => isBoundary(i + 1)
|
||||||
|
|
||||||
|
const isStartAndEnd = (i: number) => isStart(i) && isEnd(i)
|
||||||
|
|
||||||
|
const isStartOrEnd = (i: number) => isStart(i) || isEnd(i)
|
||||||
|
|
||||||
const ignoreWarning = () => {
|
const ignoreWarning = () => {
|
||||||
warning = null
|
warning = null
|
||||||
@@ -92,15 +113,17 @@
|
|||||||
style={expandBlock ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
|
style={expandBlock ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
|
||||||
{#each shortContent as parsed, i}
|
{#each shortContent as parsed, i}
|
||||||
{#if isNewline(parsed)}
|
{#if isNewline(parsed)}
|
||||||
<ContentNewline value={parsed.value} />
|
<ContentNewline value={parsed.value.slice(isBlock(i - 1) ? 1 : 0)} />
|
||||||
{:else if isTopic(parsed)}
|
{:else if isTopic(parsed)}
|
||||||
<ContentTopic value={parsed.value} />
|
<ContentTopic value={parsed.value} />
|
||||||
{:else if isCode(parsed)}
|
{:else if isCode(parsed)}
|
||||||
<ContentCode value={parsed.value} isBlock={isStartAndEnd(i)} />
|
<ContentCode
|
||||||
|
value={parsed.value}
|
||||||
|
isBlock={isStartAndEnd(i) || parsed.value.includes("\n")} />
|
||||||
{:else if isCashu(parsed) || isInvoice(parsed)}
|
{:else if isCashu(parsed) || isInvoice(parsed)}
|
||||||
<ContentToken value={parsed.value} />
|
<ContentToken value={parsed.value} />
|
||||||
{:else if isLink(parsed)}
|
{:else if isLink(parsed)}
|
||||||
{#if isStartOrEnd(i) && !hideMedia && $userSettingValues.show_media}
|
{#if isBlock(i)}
|
||||||
<ContentLinkBlock value={parsed.value} />
|
<ContentLinkBlock value={parsed.value} />
|
||||||
{:else}
|
{:else}
|
||||||
<ContentLinkInline value={parsed.value} />
|
<ContentLinkInline value={parsed.value} />
|
||||||
@@ -108,10 +131,10 @@
|
|||||||
{:else if isProfile(parsed)}
|
{:else if isProfile(parsed)}
|
||||||
<ContentMention value={parsed.value} />
|
<ContentMention value={parsed.value} />
|
||||||
{:else if isEvent(parsed) || isAddress(parsed)}
|
{:else if isEvent(parsed) || isAddress(parsed)}
|
||||||
{#if isStartOrEnd(i) && depth < 2 && !hideMedia}
|
{#if isBlock(i)}
|
||||||
<ContentQuote value={parsed.value} {depth} {event}>
|
<ContentQuote {...quoteProps} value={parsed.value} {depth} {event}>
|
||||||
<div slot="note-content" let:event>
|
<div slot="note-content" let:event>
|
||||||
<svelte:self {hideMedia} {event} depth={depth + 1} />
|
<svelte:self {quoteProps} {hideMedia} {event} depth={depth + 1} />
|
||||||
</div>
|
</div>
|
||||||
</ContentQuote>
|
</ContentQuote>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -123,16 +146,19 @@
|
|||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if isEllipsis(parsed) && expandInline}
|
{:else if isEllipsis(parsed) && expandInline}
|
||||||
{@html renderParsed(parsed)}
|
{@html renderAsHtml(parsed)}
|
||||||
<button type="button" class="text-sm underline"> Read more </button>
|
<button type="button" class="text-sm underline"> Read more </button>
|
||||||
{:else}
|
{:else}
|
||||||
{@html renderParsed(parsed)}
|
{@html renderAsHtml(parsed)}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if expandBlock}
|
{#if expandBlock}
|
||||||
<div class="relative z-feature -mt-6 flex justify-center bg-gradient-to-t from-base-100 py-2">
|
<div class="relative z-feature -mt-6 flex justify-center py-2">
|
||||||
<button type="button" class="btn" on:click|stopPropagation|preventDefault={expand}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-neutral"
|
||||||
|
on:click|stopPropagation|preventDefault={expand}>
|
||||||
See more
|
See more
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
export let isBlock
|
export let isBlock
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<code class="link-content w-full" class:block={isBlock}>
|
<code
|
||||||
{value}
|
class="w-full overflow-auto whitespace-pre rounded bg-neutral px-1 text-neutral-content"
|
||||||
|
class:block={isBlock}>
|
||||||
|
{value.trim()}
|
||||||
</code>
|
</code>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link external href={url} class="my-2 flex">
|
<Link external href={url} class="my-2 block">
|
||||||
<div class="overflow-hidden rounded-box leading-[0]">
|
<div class="overflow-hidden rounded-box leading-[0]">
|
||||||
{#if url.match(/\.(mov|webm|mp4)$/)}
|
{#if url.match(/\.(mov|webm|mp4)$/)}
|
||||||
<video controls src={url} class="max-h-96 object-contain object-center">
|
<video controls src={url} class="max-h-96 object-contain object-center">
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:catch}
|
{:catch}
|
||||||
<p class="bg-alt p-12 text-center">
|
<p class="bg-alt p-12 text-center leading-normal">
|
||||||
Unable to load a preview for {url}
|
Unable to load a preview for {url}
|
||||||
</p>
|
</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayProfile} from "@welshman/util"
|
import {displayProfile} from "@welshman/util"
|
||||||
import {deriveProfile} from "@welshman/app"
|
import {deriveProfile} from "@welshman/app"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import {pubkeyLink} from "@app/state"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
const profile = deriveProfile(value.pubkey)
|
const profile = deriveProfile(value.pubkey)
|
||||||
|
|
||||||
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link external href={pubkeyLink(value.pubkey)} class="link-content">
|
<Button on:click={openProfile} class="link-content">
|
||||||
@{displayProfile($profile)}
|
@{displayProfile($profile)}
|
||||||
</Link>
|
</Button>
|
||||||
|
|||||||
@@ -1,46 +1,105 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {nip19} from "nostr-tools"
|
import {nip19} from "nostr-tools"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import {ctx, nthEq} from "@welshman/lib"
|
import {ctx, nthEq} from "@welshman/lib"
|
||||||
import {Address} from "@welshman/util"
|
import {tracker, repository} from "@welshman/app"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import {Address, DIRECT_MESSAGE, MESSAGE, THREAD} from "@welshman/util"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import NoteCard from "@app/components/NoteCard.svelte"
|
import NoteCard from "@app/components/NoteCard.svelte"
|
||||||
import {deriveEvent, entityLink, MESSAGE, THREAD} from "@app/state"
|
import {deriveEvent, entityLink, ROOM} from "@app/state"
|
||||||
import {makeThreadPath} from "@app/routes"
|
import {makeThreadPath, makeRoomPath} from "@app/routes"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let event
|
export let event
|
||||||
export let depth = 0
|
export let depth = 0
|
||||||
|
export let relays: string[] = []
|
||||||
|
export let minimal = false
|
||||||
|
|
||||||
const {id, identifier, kind, pubkey, relays: relayHints = []} = value
|
const {id, identifier, kind, pubkey, relays: relayHints = []} = value
|
||||||
const addr = new Address(kind, pubkey, identifier)
|
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
||||||
const idOrAddress = id || addr.toString()
|
const mergedRelays = [
|
||||||
const relays = ctx.app.router.Quote(event, idOrAddress, relayHints).getUrls()
|
...relays,
|
||||||
const quote = deriveEvent(idOrAddress, relays)
|
...ctx.app.router.Quote(event, idOrAddress, relayHints).getUrls(),
|
||||||
const entity = id ? nip19.neventEncode({id, relays}) : addr.toNaddr()
|
]
|
||||||
|
const quote = deriveEvent(idOrAddress, mergedRelays)
|
||||||
|
const entity = id
|
||||||
|
? nip19.neventEncode({id, relays: mergedRelays})
|
||||||
|
: new Address(kind, pubkey, identifier, mergedRelays).toNaddr()
|
||||||
|
|
||||||
const getLocalHref = (e: TrustedEvent) => {
|
const scrollToEvent = (id: string) => {
|
||||||
const url = e.tags.find(nthEq(0, "~"))?.[2]
|
const element = document.querySelector(`[data-event="${id}"]`) as any
|
||||||
|
|
||||||
if (!url) return
|
if (element) {
|
||||||
if ([MESSAGE, THREAD].includes(e.kind)) return makeThreadPath(url, e.id)
|
element.scrollIntoView({behavior: "smooth"})
|
||||||
|
element.style =
|
||||||
|
"filter: brightness(1.5); transition-property: all; transition-duration: 400ms;"
|
||||||
|
|
||||||
const kind = e.tags.find(nthEq(0, "K"))?.[1]
|
setTimeout(() => {
|
||||||
const id = e.tags.find(nthEq(0, "E"))?.[1]
|
element.style = "transition-property: all; transition-duration: 300ms;"
|
||||||
|
}, 800)
|
||||||
|
|
||||||
if (!id || !kind) return
|
setTimeout(() => {
|
||||||
if ([MESSAGE, THREAD].includes(parseInt(kind))) return makeThreadPath(url, id)
|
element.style = ""
|
||||||
|
}, 800 + 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we found this event on a relay that the user is a member of, redirect internally
|
const openMessage = (url: string, room: string, id: string) => {
|
||||||
$: localHref = $quote ? getLocalHref($quote) : null
|
const event = repository.getEvent(id)
|
||||||
$: href = localHref || entityLink(entity)
|
|
||||||
|
if (event) {
|
||||||
|
goto(makeRoomPath(url, room))
|
||||||
|
|
||||||
|
// TODO: if the event doesn't immediately load, this won't work. Scroll up until it's found
|
||||||
|
setTimeout(() => scrollToEvent(id), 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = (e: Event) => {
|
||||||
|
if ($quote) {
|
||||||
|
if ($quote.kind === DIRECT_MESSAGE) {
|
||||||
|
return scrollToEvent($quote.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [url] = tracker.getRelays($quote.id)
|
||||||
|
const room = $quote.tags.find(nthEq(0, ROOM))?.[1]
|
||||||
|
|
||||||
|
if (url && room) {
|
||||||
|
if ($quote.kind === THREAD) {
|
||||||
|
return goto(makeThreadPath(url, $quote.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($quote.kind === MESSAGE) {
|
||||||
|
return scrollToEvent($quote.id) || openMessage(url, room, $quote.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const kind = $quote.tags.find(nthEq(0, "K"))?.[1]
|
||||||
|
const id = $quote.tags.find(nthEq(0, "E"))?.[1]
|
||||||
|
|
||||||
|
if (id && kind) {
|
||||||
|
if (parseInt(kind) === THREAD) {
|
||||||
|
return goto(makeThreadPath(url, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseInt(kind) === MESSAGE) {
|
||||||
|
return scrollToEvent(id) || openMessage(url, room, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(entityLink(entity))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link external={!localHref} {href} class="my-2 block max-w-full text-left">
|
<Button class="my-2 block max-w-full text-left" on:click={onClick}>
|
||||||
{#if $quote}
|
{#if $quote}
|
||||||
<NoteCard event={$quote} class="bg-alt rounded-box p-4">
|
<NoteCard {minimal} event={$quote} class="bg-alt rounded-box p-4">
|
||||||
<slot name="note-content" event={$quote} {depth} />
|
<slot name="note-content" event={$quote} {depth} />
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -48,4 +107,4 @@
|
|||||||
<Spinner loading>Loading event...</Spinner>
|
<Spinner loading>Loading event...</Spinner>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Link>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<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/modal"
|
||||||
|
import {BURROW_URL} from "@app/state"
|
||||||
|
|
||||||
|
export let email
|
||||||
|
export let confirm_token
|
||||||
|
|
||||||
|
const login = () => {
|
||||||
|
pushModal(LogInPassword, {email}, {path: "/"})
|
||||||
|
}
|
||||||
|
|
||||||
|
let error: string
|
||||||
|
let loading = 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" on:click={login} disabled={loading}>Continue to Login</Button>
|
||||||
|
</div>
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import type {Readable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
|
||||||
import {randomId} from "@welshman/lib"
|
import {randomId} from "@welshman/lib"
|
||||||
import {createEvent, EVENT_DATE, EVENT_TIME} from "@welshman/util"
|
import {createEvent, EVENT_TIME} from "@welshman/util"
|
||||||
import {publishThunk, dateToSeconds} from "@welshman/app"
|
import {publishThunk, dateToSeconds} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
@@ -11,16 +10,18 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
||||||
import {getPubkeyHints} from "@app/commands"
|
import {PROTECTED} from "@app/state"
|
||||||
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
import {getEditor} from "@app/editor"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
|
const uploading = writable(false)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if ($loading) return
|
if ($uploading) return
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -36,16 +37,16 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const kind = isAllDay ? EVENT_DATE : EVENT_TIME
|
const event = createEvent(EVENT_TIME, {
|
||||||
const event = createEvent(kind, {
|
content: $editor.getText({blockSeparator: "\n"}).trim(),
|
||||||
content: $editor.getText({blockSeparator: "\n"}),
|
|
||||||
tags: [
|
tags: [
|
||||||
["d", randomId()],
|
["d", randomId()],
|
||||||
["title", title],
|
["title", title],
|
||||||
["location", location],
|
["location", location],
|
||||||
["start", dateToSeconds(start).toString()],
|
["start", dateToSeconds(start).toString()],
|
||||||
["end", dateToSeconds(end).toString()],
|
["end", dateToSeconds(end).toString()],
|
||||||
...getEditorTags($editor),
|
...$editor.storage.nostr.getEditorTags(),
|
||||||
|
PROTECTED,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -53,17 +54,15 @@
|
|||||||
history.back()
|
history.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
let editor: Readable<Editor>
|
let element: HTMLElement
|
||||||
const isAllDay = false
|
let editor: ReturnType<typeof getEditor>
|
||||||
let title = ""
|
let title = ""
|
||||||
let location = ""
|
let location = ""
|
||||||
let start: Date
|
let start: Date
|
||||||
let end: Date
|
let end: Date
|
||||||
|
|
||||||
$: loading = $editor?.storage.fileUpload.loading
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor = createEditor(getEditorOptions({submit, getPubkeyHints}))
|
editor = getEditor({submit, element, uploading})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -84,13 +83,13 @@
|
|||||||
slot="input"
|
slot="input"
|
||||||
class="relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100">
|
class="relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100">
|
||||||
<div class="input-editor flex-grow overflow-hidden">
|
<div class="input-editor flex-grow overflow-hidden">
|
||||||
<EditorContent editor={$editor} />
|
<div bind:this={element} />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="center btn tooltip"
|
class="center btn tooltip"
|
||||||
on:click={$editor.commands.selectFiles}>
|
on:click={() => $editor.chain().selectFiles().run()}>
|
||||||
{#if $loading}
|
{#if $uploading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon="gallery-send" />
|
<Icon icon="gallery-send" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {nip19} from "nostr-tools"
|
import {nip19} from "nostr-tools"
|
||||||
|
import {ctx} from "@welshman/lib"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -8,10 +9,11 @@
|
|||||||
|
|
||||||
export let event
|
export let event
|
||||||
|
|
||||||
const note1 = nip19.noteEncode(event.id)
|
const relays = ctx.app.router.Event(event).getUrls()
|
||||||
|
const nevent1 = nip19.neventEncode({...event, relays})
|
||||||
const npub1 = nip19.npubEncode(event.pubkey)
|
const npub1 = nip19.npubEncode(event.pubkey)
|
||||||
const json = JSON.stringify(event, null, 2)
|
const json = JSON.stringify(event, null, 2)
|
||||||
const copyId = () => clip(note1)
|
const copyLink = () => clip(nevent1)
|
||||||
const copyPubkey = () => clip(npub1)
|
const copyPubkey = () => clip(npub1)
|
||||||
const copyJson = () => clip(json)
|
const copyJson = () => clip(json)
|
||||||
</script>
|
</script>
|
||||||
@@ -22,11 +24,11 @@
|
|||||||
<div slot="info">The full details of this event are shown below.</div>
|
<div slot="info">The full details of this event are shown below.</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
<p slot="label">Event ID</p>
|
<p slot="label">Event Link</p>
|
||||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
<Icon icon="file" />
|
<Icon icon="file" />
|
||||||
<input type="text" class="ellipsize min-w-0 grow" value={note1} />
|
<input type="text" class="ellipsize min-w-0 grow" value={nevent1} />
|
||||||
<Button on:click={copyId} class="flex items-center">
|
<Button on:click={copyLink} class="flex items-center">
|
||||||
<Icon icon="copy" />
|
<Icon icon="copy" />
|
||||||
</Button>
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {fromPairs} from "@welshman/lib"
|
import {fromPairs} from "@welshman/lib"
|
||||||
import {secondsToDate, getLocale, formatTimestamp, formatTimestampAsDate} from "@welshman/app"
|
import {formatTimestamp, formatTimestampAsDate, formatTimestampAsTime} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
|
|
||||||
const timeFmt = new Intl.DateTimeFormat(getLocale(), {timeStyle: "short"})
|
|
||||||
|
|
||||||
$: meta = fromPairs(event.tags) as Record<string, string>
|
$: meta = fromPairs(event.tags) as Record<string, string>
|
||||||
$: end = parseInt(meta.end)
|
$: end = parseInt(meta.end)
|
||||||
$: start = parseInt(meta.start)
|
$: start = parseInt(meta.start)
|
||||||
$: startDate = secondsToDate(start)
|
|
||||||
$: endDate = secondsToDate(end)
|
|
||||||
$: startDateDisplay = formatTimestampAsDate(start)
|
$: startDateDisplay = formatTimestampAsDate(start)
|
||||||
$: endDateDisplay = formatTimestampAsDate(end)
|
$: endDateDisplay = formatTimestampAsDate(end)
|
||||||
$: isSingleDay = startDateDisplay === endDateDisplay
|
$: isSingleDay = startDateDisplay === endDateDisplay
|
||||||
@@ -21,6 +17,8 @@
|
|||||||
<span>{meta.title || meta.name}</span>
|
<span>{meta.title || meta.name}</span>
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<Icon icon="clock-circle" size={4} />
|
<Icon icon="clock-circle" size={4} />
|
||||||
{timeFmt.format(startDate)} — {isSingleDay ? timeFmt.format(endDate) : formatTimestamp(end)}
|
{formatTimestampAsTime(start)} — {isSingleDay
|
||||||
|
? formatTimestampAsTime(end)
|
||||||
|
: formatTimestamp(end)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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 {pushToast} from "@app/toast"
|
||||||
|
import {publishReport} from "@app/commands"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let event
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const confirm = async () => {
|
||||||
|
if (!reason) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please select a reason for your report.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
await publishReport({event, reason: reason.toLowerCase(), content, relays: [url]})
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
history.back()
|
||||||
|
|
||||||
|
return pushToast({message: "Your report has been sent!"})
|
||||||
|
}
|
||||||
|
|
||||||
|
let reason = ""
|
||||||
|
let content = ""
|
||||||
|
let loading = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" on:submit|preventDefault={confirm}>
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Report Content</div>
|
||||||
|
<div slot="info">Flag inappropriate content.</div>
|
||||||
|
</ModalHeader>
|
||||||
|
<Field>
|
||||||
|
<p slot="label">Reason*</p>
|
||||||
|
<select slot="input" class="select select-bordered" bind:value={reason}>
|
||||||
|
<option disabled selected>Choose a reason</option>
|
||||||
|
<option>Nudity</option>
|
||||||
|
<option>Malware</option>
|
||||||
|
<option>Profanity</option>
|
||||||
|
<option>Illegal</option>
|
||||||
|
<option>Spam</option>
|
||||||
|
<option>Impersonation</option>
|
||||||
|
<option>Other</option>
|
||||||
|
</select>
|
||||||
|
<p slot="info">Please select a reason for your report.</p>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<p slot="label">Details</p>
|
||||||
|
<textarea slot="input" class="textarea textarea-bordered" bind:value={content} />
|
||||||
|
<p slot="info">Please provide any additional details relevant to your report.</p>
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" on:click={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Send Report</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<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} from "@app/commands"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let event
|
||||||
|
|
||||||
|
const reports = deriveEvents(repository, {
|
||||||
|
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
||||||
|
})
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const deleteReport = (report: TrustedEvent) => {
|
||||||
|
publishDelete({event: report, relays: [url]})
|
||||||
|
|
||||||
|
if ($reports.length === 0) {
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getReason = (tags: string[][]) => getTag("e", tags)?.[2] || "other"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Report Details</div>
|
||||||
|
<div slot="info">All reports for this event are shown below.</div>
|
||||||
|
</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} />
|
||||||
|
<span>Reported this event as "{reason}"</span>
|
||||||
|
</div>
|
||||||
|
{#if report.pubkey === $pubkey}
|
||||||
|
<Button class="btn-default btn" on:click={remove}>Delete Report</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if report.content}
|
||||||
|
<p>"{report.content}"</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<Button class="btn btn-primary" on:click={back}>Got it</Button>
|
||||||
|
</div>
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {session} from "@welshman/app"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ProfileEject from "@app/components/ProfileEject.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_NAME} from "@app/state"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const startEject = () => pushModal(ProfileEject)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
@@ -10,21 +19,33 @@
|
|||||||
<div slot="title">What is a private key?</div>
|
<div slot="title">What is a private key?</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
Most software keeps track of users by giving them a username and password. This gives the
|
Most online services keep track of users by giving them a username and password. This gives the
|
||||||
service
|
service <strong>total control</strong> over their users, allowing them to ban them at any time, or
|
||||||
<strong>total control</strong> over their users, allowing them to ban them at any time, or sell their
|
sell their activity.
|
||||||
activity.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
On <Link external href="https://nostr.com/">Nostr</Link>, <strong>you</strong> control your own
|
On <Link external href="https://nostr.com/">Nostr</Link>, <strong>you</strong> control your own
|
||||||
identity and social data, through the magic of crytography. The basic idea is that you have a
|
identity and social data, through the magic of crytography. The basic idea is that you have a
|
||||||
<strong>public key</strong>, which acts as your user id, and a <strong>private key</strong> which
|
<strong>public key</strong>, which acts as your user id, and a
|
||||||
allows you to authenticate any message you send.
|
<strong>private key</strong> which allows you to prove your identity.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
{#if $session?.email}
|
||||||
It's very important to keep private keys safe, but this can sometimes be confusing for
|
<p>
|
||||||
newcomers. This is why {PLATFORM_NAME} supports <strong>remote signer</strong> login. These services
|
It's very important to keep private keys safe, but this can sometimes be tricky, which is why {PLATFORM_NAME}
|
||||||
can store your keys securely for you, giving you access using a username and password.
|
supports a traditional account-based login for new users.
|
||||||
</p>
|
</p>
|
||||||
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
<p>If you'd like to switch to self-custody, please click below to get started.</p>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" on:click={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" on:click={startEject}>
|
||||||
|
<Icon icon="check-circle" />
|
||||||
|
I want to hold my own keys
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
{:else}
|
||||||
|
<Button class="btn btn-primary" on:click={back}>Got it</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Dialog from "@lib/components/Dialog.svelte"
|
import Dialog from "@lib/components/Dialog.svelte"
|
||||||
import CardButton from "@lib/components/CardButton.svelte"
|
import CardButton from "@lib/components/CardButton.svelte"
|
||||||
import LogIn from "@app/components/LogIn.svelte"
|
import LogIn from "@app/components/LogIn.svelte"
|
||||||
import SignUp from "@app/components/SignUp.svelte"
|
import SignUp from "@app/components/SignUp.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME} from "@app/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
const logIn = () => pushModal(LogIn)
|
const logIn = () => pushModal(LogIn)
|
||||||
@@ -33,5 +34,10 @@
|
|||||||
<div slot="info">Just a few questions and you'll be on your way.</div>
|
<div slot="info">Just a few questions and you'll be on your way.</div>
|
||||||
</CardButton>
|
</CardButton>
|
||||||
</Button>
|
</Button>
|
||||||
|
<p class="text-center text-xs opacity-75">
|
||||||
|
By using {PLATFORM_NAME}, you consent to our
|
||||||
|
<Link external class="link" href={PLATFORM_TERMS}>Terms of Service</Link> and
|
||||||
|
<Link external class="link" href={PLATFORM_PRIVACY}>Privacy Policy</Link>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
import SignUp from "@app/components/SignUp.svelte"
|
import SignUp from "@app/components/SignUp.svelte"
|
||||||
import InfoNostr from "@app/components/InfoNostr.svelte"
|
import InfoNostr from "@app/components/InfoNostr.svelte"
|
||||||
import LogInBunker from "@app/components/LogInBunker.svelte"
|
import LogInBunker from "@app/components/LogInBunker.svelte"
|
||||||
|
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||||
import {pushModal, clearModals} from "@app/modal"
|
import {pushModal, clearModals} from "@app/modal"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_NAME, BURROW_URL} from "@app/state"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {loadUserData} from "@app/commands"
|
import {loadUserData} from "@app/commands"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
@@ -18,28 +19,27 @@
|
|||||||
const signUp = () => pushModal(SignUp)
|
const signUp = () => pushModal(SignUp)
|
||||||
|
|
||||||
const withLoading =
|
const withLoading =
|
||||||
(cb: (...args: any[]) => any) =>
|
(s: string, cb: (...args: any[]) => any) =>
|
||||||
async (...args: any[]) => {
|
async (...args: any[]) => {
|
||||||
loading = true
|
loading = s
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cb(...args)
|
await cb(...args)
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSuccess = async (session: Session, relays: string[] = []) => {
|
const onSuccess = async (session: Session, relays: string[] = []) => {
|
||||||
addSession(session)
|
|
||||||
|
|
||||||
await loadUserData(session.pubkey, {relays})
|
await loadUserData(session.pubkey, {relays})
|
||||||
|
|
||||||
|
addSession(session)
|
||||||
pushToast({message: "Successfully logged in!"})
|
pushToast({message: "Successfully logged in!"})
|
||||||
setChecked("*")
|
setChecked("*")
|
||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginWithNip07 = withLoading(async () => {
|
const loginWithNip07 = withLoading("nip07", async () => {
|
||||||
const pubkey = await getNip07()?.getPublicKey()
|
const pubkey = await getNip07()?.getPublicKey()
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const loginWithSigner = withLoading(async (app: any) => {
|
const loginWithNip55 = withLoading("nip55", async (app: any) => {
|
||||||
const signer = new Nip55Signer(app.packageName)
|
const signer = new Nip55Signer(app.packageName)
|
||||||
const pubkey = await signer.getPubkey()
|
const pubkey = await signer.getPubkey()
|
||||||
|
|
||||||
@@ -66,19 +66,18 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loginWithPassword = () => pushModal(LogInPassword)
|
||||||
|
|
||||||
const loginWithBunker = () => pushModal(LogInBunker)
|
const loginWithBunker = () => pushModal(LogInBunker)
|
||||||
|
|
||||||
let loading = false
|
|
||||||
let signers: any[] = []
|
let signers: any[] = []
|
||||||
let hasNativeSigner = Boolean(getNip07())
|
let loading: string | undefined
|
||||||
|
|
||||||
|
$: hasSigner = getNip07() || signers.length > 0
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (Capacitor.isNativePlatform()) {
|
if (Capacitor.isNativePlatform()) {
|
||||||
signers = await getNip55()
|
signers = await getNip55()
|
||||||
|
|
||||||
if (signers.length > 0) {
|
|
||||||
hasNativeSigner = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -92,7 +91,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{#if getNip07()}
|
{#if getNip07()}
|
||||||
<Button disabled={loading} on:click={loginWithNip07} class="btn btn-primary">
|
<Button disabled={loading} on:click={loginWithNip07} class="btn btn-primary">
|
||||||
{#if loading}
|
{#if loading === "nip07"}
|
||||||
<span class="loading loading-spinner mr-3" />
|
<span class="loading loading-spinner mr-3" />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon="widget" />
|
<Icon icon="widget" />
|
||||||
@@ -101,8 +100,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#each signers as app}
|
{#each signers as app}
|
||||||
<Button disabled={loading} class="btn btn-primary" on:click={() => loginWithSigner(app)}>
|
<Button disabled={loading} class="btn btn-primary" on:click={() => loginWithNip55(app)}>
|
||||||
{#if loading}
|
{#if loading === "nip55"}
|
||||||
<span class="loading loading-spinner mr-3" />
|
<span class="loading loading-spinner mr-3" />
|
||||||
{:else}
|
{:else}
|
||||||
<img src={app.iconUrl} alt={app.name} width="20" height="20" />
|
<img src={app.iconUrl} alt={app.name} width="20" height="20" />
|
||||||
@@ -110,21 +109,43 @@
|
|||||||
Log in with {app.name}
|
Log in with {app.name}
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if BURROW_URL && !hasSigner}
|
||||||
|
<Button disabled={loading} on:click={loginWithPassword} class="btn btn-primary">
|
||||||
|
{#if loading === "password"}
|
||||||
|
<span class="loading loading-spinner mr-3" />
|
||||||
|
{:else}
|
||||||
|
<Icon icon="key" />
|
||||||
|
{/if}
|
||||||
|
Log in with Password
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<Button
|
<Button
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
on:click={loginWithBunker}
|
on:click={loginWithBunker}
|
||||||
class="btn {hasNativeSigner ? 'btn-neutral' : 'btn-primary'}">
|
class="btn {hasSigner || BURROW_URL ? 'btn-neutral' : 'btn-primary'}">
|
||||||
<Icon icon="cpu" />
|
<Icon icon="cpu" />
|
||||||
Log in with Remote Signer
|
Log in with Remote Signer
|
||||||
</Button>
|
</Button>
|
||||||
<Link
|
{#if BURROW_URL && hasSigner}
|
||||||
external
|
<Button disabled={loading} on:click={loginWithPassword} class="btn">
|
||||||
disabled={loading}
|
{#if loading === "password"}
|
||||||
href="https://nostrapps.com#signers"
|
<span class="loading loading-spinner mr-3" />
|
||||||
class="btn {hasNativeSigner ? '' : 'btn-neutral'}">
|
{:else}
|
||||||
<Icon icon="compass" />
|
<Icon icon="key" />
|
||||||
Browse Signer Apps
|
{/if}
|
||||||
</Link>
|
Log in with Password
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if !hasSigner || !BURROW_URL}
|
||||||
|
<Link
|
||||||
|
external
|
||||||
|
disabled={loading}
|
||||||
|
href="https://nostrapps.com#signers"
|
||||||
|
class="btn {hasSigner || BURROW_URL ? '' : 'btn-neutral'}">
|
||||||
|
<Icon icon="compass" />
|
||||||
|
Browse Signer Apps
|
||||||
|
</Link>
|
||||||
|
{/if}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
Need an account?
|
Need an account?
|
||||||
<Button class="link" on:click={signUp}>Register instead</Button>
|
<Button class="link" on:click={signUp}>Register instead</Button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onDestroy} from "svelte"
|
import {onMount, onDestroy} from "svelte"
|
||||||
import {Nip46Broker} from "@welshman/signer"
|
import {Nip46Broker, getPubkey, makeSecret} from "@welshman/signer"
|
||||||
import {nip46Perms, addSession} from "@welshman/app"
|
import {addSession} from "@welshman/app"
|
||||||
import {slideAndFade} from "@lib/transition"
|
import {slideAndFade} from "@lib/transition"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -15,29 +15,24 @@
|
|||||||
import {pushModal, clearModals} from "@app/modal"
|
import {pushModal, clearModals} from "@app/modal"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO, SIGNER_RELAYS} from "@app/state"
|
import {NIP46_PERMS, PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO, SIGNER_RELAYS} from "@app/state"
|
||||||
|
|
||||||
const back = () => history.back()
|
const clientSecret = makeSecret()
|
||||||
|
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
|
|
||||||
const init = Nip46Broker.initiate({
|
const broker = Nip46Broker.get({clientSecret, relays: SIGNER_RELAYS})
|
||||||
perms: nip46Perms,
|
|
||||||
url: PLATFORM_URL,
|
const back = () => history.back()
|
||||||
name: PLATFORM_NAME,
|
|
||||||
relays: SIGNER_RELAYS,
|
|
||||||
image: PLATFORM_LOGO,
|
|
||||||
abortController,
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
const {pubkey, token, relays} = Nip46Broker.parseBunkerLink(bunker)
|
const {signerPubkey, connectSecret, relays} = broker.parseBunkerUrl(input)
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pubkey || relays.length === 0) {
|
if (!signerPubkey || relays.length === 0) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Sorry, it looks like that's an invalid bunker link.",
|
message: "Sorry, it looks like that's an invalid bunker link.",
|
||||||
@@ -47,16 +42,16 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!(await loginWithNip46(token, {pubkey, relays}))) {
|
const success = await loginWithNip46({connectSecret, clientSecret, signerPubkey, relays})
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
abortController.abort()
|
||||||
|
} else {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Something went wrong, please try again!",
|
message: "Something went wrong, please try again!",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
abortController.abort()
|
|
||||||
|
|
||||||
await loadUserData(pubkey)
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
@@ -64,21 +59,57 @@
|
|||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
|
|
||||||
let bunker = ""
|
let url = ""
|
||||||
|
let input = ""
|
||||||
let loading = false
|
let loading = false
|
||||||
|
|
||||||
init.result.then(async pubkey => {
|
$: {
|
||||||
if (pubkey) {
|
// For testing and for play store reviewers
|
||||||
|
if (input === "reviewkey") {
|
||||||
|
const secret = makeSecret()
|
||||||
|
|
||||||
|
addSession({method: "nip01", secret, pubkey: getPubkey(secret)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
} 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
|
loading = true
|
||||||
|
|
||||||
addSession({
|
const userPubkey = await broker.getPublicKey()
|
||||||
pubkey,
|
|
||||||
method: "nip46",
|
|
||||||
secret: init.clientSecret,
|
|
||||||
handler: {pubkey, relays: SIGNER_RELAYS},
|
|
||||||
})
|
|
||||||
|
|
||||||
await loadUserData(pubkey)
|
await loadUserData(userPubkey)
|
||||||
|
|
||||||
|
addSession({
|
||||||
|
method: "nip46",
|
||||||
|
pubkey: userPubkey,
|
||||||
|
secret: clientSecret,
|
||||||
|
handler: {
|
||||||
|
pubkey: response.event.pubkey,
|
||||||
|
relays: SIGNER_RELAYS,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
setChecked("*")
|
setChecked("*")
|
||||||
clearModals()
|
clearModals()
|
||||||
@@ -97,16 +128,16 @@
|
|||||||
Connect your signer by scanning the QR code below or pasting a bunker link.
|
Connect your signer by scanning the QR code below or pasting a bunker link.
|
||||||
</div>
|
</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
{#if !loading}
|
{#if !loading && url}
|
||||||
<div class="w-xs m-auto" out:slideAndFade>
|
<div class="w-xs m-auto" out:slideAndFade>
|
||||||
<QRCode code={init.nostrconnect} />
|
<QRCode code={url} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Field>
|
<Field>
|
||||||
<p slot="label">Bunker Link*</p>
|
<p slot="label">Bunker Link*</p>
|
||||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
<Icon icon="cpu" />
|
<Icon icon="cpu" />
|
||||||
<input disabled={loading} bind:value={bunker} class="grow" placeholder="bunker://" />
|
<input disabled={loading} bind:value={input} class="grow" placeholder="bunker://" />
|
||||||
</label>
|
</label>
|
||||||
<p slot="info">
|
<p slot="info">
|
||||||
A login link provided by a nostr signing app.
|
A login link provided by a nostr signing app.
|
||||||
@@ -118,7 +149,7 @@
|
|||||||
<Icon icon="alt-arrow-left" />
|
<Icon icon="alt-arrow-left" />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading || !bunker}>
|
<Button type="submit" class="btn btn-primary" disabled={loading || !input}>
|
||||||
<Spinner {loading}>Next</Spinner>
|
<Spinner {loading}>Next</Spinner>
|
||||||
<Icon icon="alt-arrow-right" />
|
<Icon icon="alt-arrow-right" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<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} from "@welshman/app"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
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 {loadUserData} from "@app/commands"
|
||||||
|
import {clearModals, pushModal} from "@app/modal"
|
||||||
|
import {setChecked} from "@app/notifications"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {NIP46_PERMS, BURROW_URL, PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO} from "@app/state"
|
||||||
|
|
||||||
|
export let email = ""
|
||||||
|
|
||||||
|
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 = Nip46Broker.get({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 = ""
|
||||||
|
let loading = 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)
|
||||||
|
} 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 userPubkey = await broker.getPublicKey()
|
||||||
|
|
||||||
|
await loadUserData(userPubkey)
|
||||||
|
|
||||||
|
addSession({
|
||||||
|
email,
|
||||||
|
method: "nip46",
|
||||||
|
pubkey: userPubkey,
|
||||||
|
secret: clientSecret,
|
||||||
|
handler: {pubkey: response.event.pubkey, relays},
|
||||||
|
})
|
||||||
|
|
||||||
|
setChecked("*")
|
||||||
|
clearModals()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
abortController.abort()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" on:submit|preventDefault={onSubmit}>
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Log In</div>
|
||||||
|
<div slot="info">Log in using your email and password</div>
|
||||||
|
</ModalHeader>
|
||||||
|
<FieldInline>
|
||||||
|
<p slot="label">Email</p>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
|
<Icon icon="user-rounded" />
|
||||||
|
<input bind:value={email} />
|
||||||
|
</label>
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
<p slot="label">Password</p>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
|
<Icon icon="key" />
|
||||||
|
<input bind:value={password} type="password" />
|
||||||
|
</label>
|
||||||
|
</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" on:click={startReset}
|
||||||
|
>Forgot your password?</Button>
|
||||||
|
</p>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" on:click={back} disabled={loading}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading || !email || !password}>
|
||||||
|
<Spinner {loading}>Next</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -1,30 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {clearStorage} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {logout} from "@app/commands"
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const logout = async () => {
|
const doLogout = async () => {
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await clearStorage()
|
await logout()
|
||||||
localStorage.clear()
|
window.location.href = "/"
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.reload()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading = false
|
let loading = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" on:submit|preventDefault={logout}>
|
<form class="column gap-4" on:submit|preventDefault={doLogout}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">Are you sure you want<br />to log out?</div>
|
<div slot="title">Are you sure you want<br />to log out?</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<div slot="info">Learn about {PLATFORM_NAME} and support the developer</div>
|
<div slot="info">Learn about {PLATFORM_NAME} and support the developer</div>
|
||||||
</CardButton>
|
</CardButton>
|
||||||
</Link>
|
</Link>
|
||||||
<Button on:click={logout} class="btn btn-error">
|
<Button on:click={logout} class="btn btn-neutral">
|
||||||
<Icon icon="exit" /> Log Out
|
<Icon icon="exit" /> Log Out
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl, GROUP_META} from "@welshman/util"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -15,22 +15,22 @@
|
|||||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||||
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
|
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
|
||||||
import {
|
import {
|
||||||
getMembershipRoomsByUrl,
|
userRoomsByUrl,
|
||||||
getMembershipUrls,
|
|
||||||
hasMembershipUrl,
|
hasMembershipUrl,
|
||||||
userMembership,
|
|
||||||
memberships,
|
memberships,
|
||||||
roomsByUrl,
|
deriveUserRooms,
|
||||||
GENERAL,
|
deriveOtherRooms,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {deriveNotification, THREAD_FILTERS} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
|
import {pullConservatively} from "@app/requests"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
const threadsPath = makeSpacePath(url, "threads")
|
const threadsPath = makeSpacePath(url, "threads")
|
||||||
const threadsNotification = deriveNotification(threadsPath, THREAD_FILTERS, url)
|
const userRooms = deriveUserRooms(url)
|
||||||
|
const otherRooms = deriveOtherRooms(url)
|
||||||
|
|
||||||
const openMenu = () => {
|
const openMenu = () => {
|
||||||
showMenu = true
|
showMenu = true
|
||||||
@@ -59,20 +59,19 @@
|
|||||||
let replaceState = false
|
let replaceState = false
|
||||||
let element: Element
|
let element: Element
|
||||||
|
|
||||||
$: rooms = getMembershipRoomsByUrl(url, $userMembership)
|
|
||||||
$: otherRooms = ($roomsByUrl.get(url) || []).filter(room => !rooms.concat(GENERAL).includes(room))
|
|
||||||
$: members = $memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey)
|
$: members = $memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey)
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
replaceState = Boolean(element.closest(".drawer"))
|
replaceState = Boolean(element.closest(".drawer"))
|
||||||
|
pullConservatively({relays: [url], filters: [{kinds: [GROUP_META]}]})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={element}>
|
<div bind:this={element}>
|
||||||
<SecondaryNavSection>
|
<SecondaryNavSection class="max-h-screen">
|
||||||
<div>
|
<div>
|
||||||
<SecondaryNavItem class="w-full !justify-between" on:click={openMenu}>
|
<SecondaryNavItem class="w-full !justify-between" on:click={openMenu}>
|
||||||
<strong>{displayRelayUrl(url)}</strong>
|
<strong class="ellipsize">{displayRelayUrl(url)}</strong>
|
||||||
<Icon icon="alt-arrow-down" />
|
<Icon icon="alt-arrow-down" />
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{#if showMenu}
|
{#if showMenu}
|
||||||
@@ -93,13 +92,13 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{#if getMembershipUrls($userMembership).includes(url)}
|
{#if $userRoomsByUrl.has(url)}
|
||||||
<Button on:click={leaveSpace} class="text-error">
|
<Button on:click={leaveSpace} class="text-error">
|
||||||
<Icon icon="exit" />
|
<Icon icon="exit" />
|
||||||
Leave Space
|
Leave Space
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button on:click={joinSpace}>
|
<Button on:click={joinSpace} class="bg-primary text-primary-content">
|
||||||
<Icon icon="login-2" />
|
<Icon icon="login-2" />
|
||||||
Join Space
|
Join Space
|
||||||
</Button>
|
</Button>
|
||||||
@@ -109,37 +108,35 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<SecondaryNavItem href={makeSpacePath(url)}>
|
<div class="flex min-h-0 flex-col gap-1 overflow-auto">
|
||||||
<Icon icon="home-smile" /> Home
|
<SecondaryNavItem href={makeSpacePath(url)}>
|
||||||
</SecondaryNavItem>
|
<Icon icon="home-smile" /> Home
|
||||||
<SecondaryNavItem href={threadsPath} notification={$threadsNotification}>
|
|
||||||
<Icon icon="notes-minimalistic" /> Threads
|
|
||||||
</SecondaryNavItem>
|
|
||||||
<div class="h-2" />
|
|
||||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
|
||||||
<MenuSpaceRoomItem {url} room={GENERAL} />
|
|
||||||
{#each rooms as room, i (room)}
|
|
||||||
<MenuSpaceRoomItem {url} {room} />
|
|
||||||
{/each}
|
|
||||||
{#if otherRooms.length > 0}
|
|
||||||
<div class="h-2" />
|
|
||||||
<SecondaryNavHeader>
|
|
||||||
{#if rooms.length > 0}
|
|
||||||
Other Rooms
|
|
||||||
{:else}
|
|
||||||
Rooms
|
|
||||||
{/if}
|
|
||||||
</SecondaryNavHeader>
|
|
||||||
{/if}
|
|
||||||
{#each otherRooms as room, i (room)}
|
|
||||||
<SecondaryNavItem href={makeSpacePath(url, room)}>
|
|
||||||
<Icon icon="hashtag" />
|
|
||||||
{room}
|
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/each}
|
<SecondaryNavItem href={threadsPath} notification={$notifications.has(threadsPath)}>
|
||||||
<SecondaryNavItem on:click={addRoom}>
|
<Icon icon="notes-minimalistic" /> Threads
|
||||||
<Icon icon="add-circle" />
|
</SecondaryNavItem>
|
||||||
Create room
|
<div class="h-2" />
|
||||||
</SecondaryNavItem>
|
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||||
|
{#each $userRooms as room, i (room)}
|
||||||
|
<MenuSpaceRoomItem notify {url} {room} />
|
||||||
|
{/each}
|
||||||
|
{#if $otherRooms.length > 0}
|
||||||
|
<div class="h-2" />
|
||||||
|
<SecondaryNavHeader>
|
||||||
|
{#if $userRooms.length > 0}
|
||||||
|
Other Rooms
|
||||||
|
{:else}
|
||||||
|
Rooms
|
||||||
|
{/if}
|
||||||
|
</SecondaryNavHeader>
|
||||||
|
{/if}
|
||||||
|
{#each $otherRooms as room, i (room)}
|
||||||
|
<MenuSpaceRoomItem {url} {room} />
|
||||||
|
{/each}
|
||||||
|
<SecondaryNavItem on:click={addRoom}>
|
||||||
|
<Icon icon="add-circle" />
|
||||||
|
Create room
|
||||||
|
</SecondaryNavItem>
|
||||||
|
</div>
|
||||||
</SecondaryNavSection>
|
</SecondaryNavSection>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,54 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {page} from "$app/stores"
|
|
||||||
import {derived} from "svelte/store"
|
|
||||||
import {max} from "@welshman/lib"
|
|
||||||
import {matchFilter} from "@welshman/util"
|
|
||||||
import {pubkey} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import MenuSpace from "@app/components/MenuSpace.svelte"
|
import MenuSpace from "@app/components/MenuSpace.svelte"
|
||||||
import {checked, getNotification, deriveNotification, THREAD_FILTERS} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
import {
|
import {makeSpacePath} from "@app/routes"
|
||||||
userMembership,
|
|
||||||
getMembershipRoomsByUrl,
|
|
||||||
deriveEventsForUrl,
|
|
||||||
MESSAGE,
|
|
||||||
GENERAL,
|
|
||||||
} from "@app/state"
|
|
||||||
import {makeRoomPath, makeSpacePath} from "@app/routes"
|
|
||||||
import {pushDrawer} from "@app/modal"
|
import {pushDrawer} from "@app/modal"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
|
const path = makeSpacePath(url)
|
||||||
|
|
||||||
const openMenu = () => pushDrawer(MenuSpace, {url})
|
const openMenu = () => pushDrawer(MenuSpace, {url})
|
||||||
|
|
||||||
const events = deriveEventsForUrl(url, [{kinds: [MESSAGE]}])
|
|
||||||
|
|
||||||
const threadsPath = makeSpacePath(url, "threads")
|
|
||||||
|
|
||||||
const threadsNotification = deriveNotification(threadsPath, THREAD_FILTERS, url)
|
|
||||||
|
|
||||||
const notification = derived(
|
|
||||||
[page, events, checked, userMembership],
|
|
||||||
([$page, $events, $checked, $userMembership]) =>
|
|
||||||
getMembershipRoomsByUrl(url, $userMembership)
|
|
||||||
.concat(GENERAL)
|
|
||||||
.some(room => {
|
|
||||||
const path = makeRoomPath(url, room)
|
|
||||||
|
|
||||||
if ($page.url.pathname === path) return false
|
|
||||||
|
|
||||||
const lastChecked = max([$checked["*"], $checked[path]])
|
|
||||||
const roomEvents = $events.filter(e => matchFilter({"#~": [room]}, e))
|
|
||||||
|
|
||||||
return getNotification($pubkey, lastChecked, roomEvents)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button on:click={openMenu} class="btn btn-neutral btn-sm relative md:hidden">
|
<Button on:click={openMenu} class="btn btn-neutral btn-sm relative md:hidden">
|
||||||
<Icon icon="menu-dots" />
|
<Icon icon="menu-dots" />
|
||||||
{#if $threadsNotification || $notification}
|
{#if $notifications.has(path)}
|
||||||
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-primary" />
|
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-primary" />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
import {deriveNotification, getRoomFilters} from "@app/notifications"
|
import {makeRoomPath} from "@app/routes"
|
||||||
|
import {deriveChannel, channelIsLocked} from "@app/state"
|
||||||
|
import {notifications} from "@app/notifications"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
export let room
|
export let room
|
||||||
|
export let notify = false
|
||||||
|
|
||||||
const path = makeSpacePath(url, room)
|
const path = makeRoomPath(url, room)
|
||||||
const notification = deriveNotification(path, getRoomFilters(room), url)
|
const channel = deriveChannel(url, room)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SecondaryNavItem href={path} notification={$notification}>
|
<SecondaryNavItem href={path} notification={notify ? $notifications.has(path) : false}>
|
||||||
<Icon icon="hashtag" />
|
{#if channelIsLocked($channel)}
|
||||||
{room}
|
<Icon icon="lock" size={4} />
|
||||||
|
{:else}
|
||||||
|
<Icon icon="hashtag" />
|
||||||
|
{/if}
|
||||||
|
<div class="min-w-0 overflow-hidden text-ellipsis">
|
||||||
|
<ChannelName {url} {room} />
|
||||||
|
</div>
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import CardButton from "@lib/components/CardButton.svelte"
|
import CardButton from "@lib/components/CardButton.svelte"
|
||||||
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
|
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
|
||||||
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
||||||
import {userMembership, getMembershipUrls, PLATFORM_RELAY} from "@app/state"
|
import {userRoomsByUrl, PLATFORM_RELAY} from "@app/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
const addSpace = () => pushModal(SpaceAdd)
|
const addSpace = () => pushModal(SpaceAdd)
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
{#if PLATFORM_RELAY}
|
{#if PLATFORM_RELAY}
|
||||||
<MenuSpacesItem url={PLATFORM_RELAY} />
|
<MenuSpacesItem url={PLATFORM_RELAY} />
|
||||||
<Divider />
|
<Divider />
|
||||||
{:else if getMembershipUrls($userMembership).length > 0}
|
{:else if $userRoomsByUrl.size > 0}
|
||||||
{#each getMembershipUrls($userMembership) as url (url)}
|
{#each $userRoomsByUrl.keys() as url (url)}
|
||||||
<MenuSpacesItem {url} />
|
<MenuSpacesItem {url} />
|
||||||
{/each}
|
{/each}
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {spacesNotifications} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<div slot="icon"><SpaceAvatar {url} /></div>
|
<div slot="icon"><SpaceAvatar {url} /></div>
|
||||||
<div slot="title" class="flex gap-1">
|
<div slot="title" class="flex gap-1">
|
||||||
<RelayName {url} />
|
<RelayName {url} />
|
||||||
{#if $spacesNotifications.includes(url)}
|
{#if $notifications.has(path)}
|
||||||
<div class="relative top-1 h-2 w-2 rounded-full bg-primary" />
|
<div class="relative top-1 h-2 w-2 rounded-full bg-primary" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
import {nip19} from "nostr-tools"
|
import {nip19} from "nostr-tools"
|
||||||
import {ctx} from "@welshman/lib"
|
import {ctx} from "@welshman/lib"
|
||||||
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||||
@@ -7,9 +8,11 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import {entityLink} from "@app/state"
|
import {entityLink} from "@app/state"
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
|
export let minimal = false
|
||||||
export let hideProfile = false
|
export let hideProfile = false
|
||||||
|
|
||||||
const relays = ctx.app.router.Event(event).getUrls()
|
const relays = ctx.app.router.Event(event).getUrls()
|
||||||
@@ -34,9 +37,16 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
{#if !hideProfile}
|
{#if !hideProfile}
|
||||||
<Profile pubkey={event.pubkey} />
|
{#if minimal}
|
||||||
|
@<ProfileName pubkey={event.pubkey} />
|
||||||
|
{:else}
|
||||||
|
<Profile pubkey={event.pubkey} />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<Link external href={entityLink(nevent)} class="text-sm opacity-75">
|
<Link
|
||||||
|
external
|
||||||
|
href={entityLink(nevent)}
|
||||||
|
class={cx("text-sm opacity-75", {"text-xs": minimal})}>
|
||||||
{formatTimestamp(event.created_at)}
|
{formatTimestamp(event.created_at)}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,13 +23,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) =>
|
const onEmoji = (emoji: NativeEmoji) =>
|
||||||
publishReaction({event, relays: [url], content: emoji.unicode})
|
publishReaction({event, content: emoji.unicode, relays: [url]})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NoteCard {event} class="card2 bg-alt">
|
<NoteCard {event} class="card2 bg-alt">
|
||||||
<Content {event} expandMode="inline" />
|
<Content {event} expandMode="inline" />
|
||||||
<div class="flex w-full justify-between gap-2">
|
<div class="flex w-full justify-between gap-2">
|
||||||
<ReactionSummary relays={[url]} {event} {onReactionClick}>
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right">
|
||||||
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
||||||
<Icon icon="smile-circle" size={4} />
|
<Icon icon="smile-circle" size={4} />
|
||||||
</EmojiButton>
|
</EmojiButton>
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {postJson, sleep} from "@welshman/lib"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
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/modal"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {BURROW_URL} from "@app/state"
|
||||||
|
|
||||||
|
export let email
|
||||||
|
export let reset_token
|
||||||
|
|
||||||
|
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 = false
|
||||||
|
let password = ""
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" on:submit|preventDefault={onSubmit}>
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Reset your password</div>
|
||||||
|
</ModalHeader>
|
||||||
|
<FieldInline disabled={loading}>
|
||||||
|
<p slot="label">Email Address</p>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
|
<Icon icon="user-rounded" />
|
||||||
|
<input readonly value={email} class="grow" />
|
||||||
|
</label>
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline disabled={loading}>
|
||||||
|
<p slot="label">New Password</p>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
|
<Icon icon="key" />
|
||||||
|
<input bind:value={password} class="grow" type="password" />
|
||||||
|
</label>
|
||||||
|
</FieldInline>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Reset password</Spinner>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {postJson, sleep} from "@welshman/lib"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {BURROW_URL} from "@app/state"
|
||||||
|
|
||||||
|
export let email: string
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [res] = await Promise.all([
|
||||||
|
postJson(BURROW_URL + "/user/request-reset", {email}),
|
||||||
|
sleep(1000),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
pushToast({message: res.error, theme: "error"})
|
||||||
|
} else {
|
||||||
|
pushToast({message: `Password reset email has been sent!`})
|
||||||
|
pushModal(LogInPassword, {email}, {path: "/"})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" on:submit|preventDefault={onSubmit}>
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Reset your password</div>
|
||||||
|
</ModalHeader>
|
||||||
|
<FieldInline disabled={loading}>
|
||||||
|
<p slot="label">Email Address</p>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
|
<Icon icon="user-rounded" />
|
||||||
|
<input bind:value={email} class="grow" />
|
||||||
|
</label>
|
||||||
|
<p slot="info">You'll be sent an email with a password reset link.</p>
|
||||||
|
</FieldInline>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" on:click={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Request password reset link</Spinner>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -1,24 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {page} from "$app/stores"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import {userProfile} from "@welshman/app"
|
import {userProfile} from "@welshman/app"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
||||||
|
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||||
import MenuSpaces from "@app/components/MenuSpaces.svelte"
|
import MenuSpaces from "@app/components/MenuSpaces.svelte"
|
||||||
import MenuSettings from "@app/components/MenuSettings.svelte"
|
import MenuSettings from "@app/components/MenuSettings.svelte"
|
||||||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||||
import {userMembership, getMembershipUrls, PLATFORM_RELAY, PLATFORM_LOGO} from "@app/state"
|
import {userRoomsByUrl, canDecrypt, PLATFORM_RELAY, PLATFORM_LOGO} from "@app/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {deriveNotification, inactiveSpacesNotifications, CHAT_FILTERS} from "@app/notifications"
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
import {notifications} from "@app/notifications"
|
||||||
const chatNotification = deriveNotification("/chat", CHAT_FILTERS)
|
|
||||||
|
|
||||||
const addSpace = () => pushModal(SpaceAdd)
|
const addSpace = () => pushModal(SpaceAdd)
|
||||||
|
|
||||||
const showSpacesMenu = () =>
|
const showSpacesMenu = () => (spacePaths.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd))
|
||||||
getMembershipUrls($userMembership).length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd)
|
|
||||||
|
|
||||||
const showSettingsMenu = () => pushModal(MenuSettings)
|
const showSettingsMenu = () => pushModal(MenuSettings)
|
||||||
|
|
||||||
|
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
|
||||||
|
|
||||||
|
$: spaceUrls = Array.from($userRoomsByUrl.keys())
|
||||||
|
$: spacePaths = spaceUrls.map(url => makeSpacePath(url))
|
||||||
|
$: anySpaceNotifications = spacePaths.some(
|
||||||
|
path => !$page.url.pathname.startsWith(path) && $notifications.has(path),
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block">
|
<div class="relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block">
|
||||||
@@ -31,7 +40,7 @@
|
|||||||
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" />
|
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#each getMembershipUrls($userMembership) as url (url)}
|
{#each spaceUrls as url (url)}
|
||||||
<PrimaryNavItemSpace {url} />
|
<PrimaryNavItemSpace {url} />
|
||||||
{/each}
|
{/each}
|
||||||
<PrimaryNavItem title="Add Space" on:click={addSpace} class="tooltip-right">
|
<PrimaryNavItem title="Add Space" on:click={addSpace} class="tooltip-right">
|
||||||
@@ -47,14 +56,11 @@
|
|||||||
class="tooltip-right">
|
class="tooltip-right">
|
||||||
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
|
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem title="Notes" href="/notes" class="tooltip-right">
|
|
||||||
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
|
|
||||||
</PrimaryNavItem>
|
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
title="Messages"
|
title="Messages"
|
||||||
href="/chat"
|
on:click={openChat}
|
||||||
class="tooltip-right"
|
class="tooltip-right"
|
||||||
notification={$chatNotification}>
|
notification={$notifications.has("/chat")}>
|
||||||
<Avatar icon="letter" class="!h-10 !w-10" />
|
<Avatar icon="letter" class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
|
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
|
||||||
@@ -70,24 +76,21 @@
|
|||||||
class="border-top fixed bottom-0 left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
class="border-top fixed bottom-0 left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
||||||
<div class="content-padding-x content-sizing flex justify-between px-2">
|
<div class="content-padding-x content-sizing flex justify-between px-2">
|
||||||
<div class="flex gap-2 sm:gap-8">
|
<div class="flex gap-2 sm:gap-8">
|
||||||
<PrimaryNavItem title="Search" href="/people">
|
<PrimaryNavItem title="Home" href="/home">
|
||||||
<Avatar icon="magnifer" class="!h-10 !w-10" />
|
<Avatar icon="home-smile" class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
|
||||||
<PrimaryNavItem title="Notes" href="/notes">
|
|
||||||
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
|
|
||||||
</PrimaryNavItem>
|
|
||||||
<PrimaryNavItem title="Messages" href="/chat" notification={$chatNotification}>
|
|
||||||
<Avatar icon="letter" class="!h-10 !w-10" />
|
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
title="Spaces"
|
title="Messages"
|
||||||
on:click={showSpacesMenu}
|
on:click={openChat}
|
||||||
notification={$inactiveSpacesNotifications.length > 0}>
|
notification={$notifications.has("/chat")}>
|
||||||
|
<Avatar icon="letter" class="!h-10 !w-10" />
|
||||||
|
</PrimaryNavItem>
|
||||||
|
<PrimaryNavItem title="Spaces" on:click={showSpacesMenu} notification={anySpaceNotifications}>
|
||||||
<Avatar icon="settings-minimalistic" class="!h-10 !w-10" />
|
<Avatar icon="settings-minimalistic" class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
<PrimaryNavItem title="Settings" on:click={showSettingsMenu}>
|
<PrimaryNavItem title="Settings" on:click={showSettingsMenu}>
|
||||||
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
|
<Avatar icon="settings" src={$userProfile?.picture} class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,18 +3,17 @@
|
|||||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {deriveNotification, SPACE_FILTERS} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
const path = makeSpacePath(url)
|
const path = makeSpacePath(url)
|
||||||
const notification = deriveNotification(path, SPACE_FILTERS, url)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
title={displayRelayUrl(url)}
|
title={displayRelayUrl(url)}
|
||||||
href={path}
|
href={path}
|
||||||
class="tooltip-right"
|
class="tooltip-right"
|
||||||
notification={$notification}>
|
notification={$notifications.has(path)}>
|
||||||
<SpaceAvatar {url} />
|
<SpaceAvatar {url} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
|
|||||||
@@ -9,10 +9,11 @@
|
|||||||
displayHandle,
|
displayHandle,
|
||||||
deriveProfileDisplay,
|
deriveProfileDisplay,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import WotScore from "@lib/components/WotScore.svelte"
|
import WotScore from "@lib/components/WotScore.svelte"
|
||||||
import {pubkeyLink} from "@app/state"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let pubkey
|
export let pubkey
|
||||||
|
|
||||||
@@ -21,19 +22,21 @@
|
|||||||
const handle = deriveHandleForPubkey(pubkey)
|
const handle = deriveHandleForPubkey(pubkey)
|
||||||
const score = deriveUserWotScore(pubkey)
|
const score = deriveUserWotScore(pubkey)
|
||||||
|
|
||||||
|
const openProfile = () => pushModal(ProfileDetail, {pubkey})
|
||||||
|
|
||||||
$: following =
|
$: following =
|
||||||
pubkey === $session!.pubkey || getPubkeyTagValues(getListTags($userFollows)).includes(pubkey)
|
pubkey === $session!.pubkey || getPubkeyTagValues(getListTags($userFollows)).includes(pubkey)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex max-w-full gap-3">
|
<div class="flex max-w-full gap-3">
|
||||||
<Link external href={pubkeyLink(pubkey)} class="py-1">
|
<Button on:click={openProfile} class="py-1">
|
||||||
<Avatar src={$profile?.picture} size={10} />
|
<Avatar src={$profile?.picture} size={10} />
|
||||||
</Link>
|
</Button>
|
||||||
<div class="flex min-w-0 flex-col">
|
<div class="flex min-w-0 flex-col">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Link external href={pubkeyLink(pubkey)} class="text-bold overflow-hidden text-ellipsis">
|
<Button on:click={openProfile} class="text-bold overflow-hidden text-ellipsis">
|
||||||
{$profileDisplay}
|
{$profileDisplay}
|
||||||
</Link>
|
</Button>
|
||||||
<WotScore score={$score} active={following} />
|
<WotScore score={$score} active={following} />
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden text-ellipsis text-sm opacity-75">
|
<div class="overflow-hidden text-ellipsis text-sm opacity-75">
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
|
import {displayPubkey, getPubkeyTagValues, getListTags} from "@welshman/util"
|
||||||
|
import {
|
||||||
|
session,
|
||||||
|
userFollows,
|
||||||
|
deriveUserWotScore,
|
||||||
|
deriveProfile,
|
||||||
|
deriveHandleForPubkey,
|
||||||
|
displayHandle,
|
||||||
|
deriveProfileDisplay,
|
||||||
|
} from "@welshman/app"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
|
import WotScore from "@lib/components/WotScore.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
||||||
|
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||||
|
import {canDecrypt, pubkeyLink} from "@app/state"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
import {makeChatPath} from "@app/routes"
|
||||||
|
|
||||||
|
export let pubkey
|
||||||
|
|
||||||
|
const profile = deriveProfile(pubkey)
|
||||||
|
const profileDisplay = deriveProfileDisplay(pubkey)
|
||||||
|
const handle = deriveHandleForPubkey(pubkey)
|
||||||
|
const score = deriveUserWotScore(pubkey)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const chatPath = makeChatPath([pubkey])
|
||||||
|
|
||||||
|
const openChat = () => ($canDecrypt ? goto(chatPath) : pushModal(ChatEnable, {next: chatPath}))
|
||||||
|
|
||||||
|
$: following =
|
||||||
|
pubkey === $session!.pubkey || getPubkeyTagValues(getListTags($userFollows)).includes(pubkey)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<div class="flex max-w-full gap-3">
|
||||||
|
<span class="py-1">
|
||||||
|
<Avatar src={$profile?.picture} size={10} />
|
||||||
|
</span>
|
||||||
|
<div class="flex min-w-0 flex-col">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-bold overflow-hidden text-ellipsis">
|
||||||
|
{$profileDisplay}
|
||||||
|
</span>
|
||||||
|
<WotScore score={$score} active={following} />
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden text-ellipsis text-sm opacity-75">
|
||||||
|
{$handle ? displayHandle($handle) : displayPubkey(pubkey)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ProfileInfo {pubkey} />
|
||||||
|
<ModalFooter>
|
||||||
|
<Button on:click={back} class="btn btn-link">
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Link external href={pubkeyLink(pubkey)} class="btn btn-neutral">
|
||||||
|
<Icon icon="user-circle" />
|
||||||
|
See Complete Profile
|
||||||
|
</Link>
|
||||||
|
<Button on:click={openChat} class="btn btn-primary">
|
||||||
|
<Icon icon="letter" />
|
||||||
|
Open Chat
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<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 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/state"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {logout} from "@app/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 = ""
|
||||||
|
let success = false
|
||||||
|
let loading = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Export your keys</div>
|
||||||
|
</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>
|
||||||
|
<p slot="label">To confirm, please enter your password below:</p>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
|
<Icon icon="key" />
|
||||||
|
<input type="password" disabled={loading} bind:value={password} class="grow" />
|
||||||
|
</label>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" disabled={loading || success} on:click={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
{#if success}
|
||||||
|
<Button class="btn btn-primary" disabled={loading} on:click={reload}>
|
||||||
|
<Icon icon="check-circle" />
|
||||||
|
<Spinner {loading}>Refresh the page</Spinner>
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button class="btn btn-error" disabled={loading} on:click={confirm}>
|
||||||
|
<Icon icon="check-circle" />
|
||||||
|
<Spinner {loading}>I understand, send me my private key</Spinner>
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
@@ -1,20 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {sortBy, uniqBy} from "@welshman/lib"
|
import {sortBy, uniqBy} from "@welshman/lib"
|
||||||
import {feedFromFilter} from "@welshman/feeds"
|
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
||||||
import {NOTE, getAncestorTags} from "@welshman/util"
|
import {NOTE, getAncestorTags} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {createFeedController} from "@welshman/app"
|
import {createFeedController} from "@welshman/app"
|
||||||
import {createScroller} from "@lib/html"
|
import {createScroller} from "@lib/html"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import NoteItem from "@app/components/NoteItem.svelte"
|
import NoteItem from "@app/components/NoteItem.svelte"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
export let pubkey
|
export let pubkey
|
||||||
|
export let events: TrustedEvent[] = []
|
||||||
|
export let hideLoading = false
|
||||||
|
|
||||||
const ctrl = createFeedController({
|
const ctrl = createFeedController({
|
||||||
useWindowing: true,
|
useWindowing: true,
|
||||||
feed: feedFromFilter({kinds: [NOTE], authors: [pubkey]}),
|
feed: makeIntersectionFeed(
|
||||||
|
makeRelayFeed(url),
|
||||||
|
feedFromFilter({kinds: [NOTE], authors: [pubkey]}),
|
||||||
|
),
|
||||||
onEvent: (event: TrustedEvent) => {
|
onEvent: (event: TrustedEvent) => {
|
||||||
if (getAncestorTags(event.tags).replies.length === 0) {
|
if (getAncestorTags(event.tags).replies.length === 0) {
|
||||||
buffer.push(event)
|
buffer.push(event)
|
||||||
@@ -24,7 +30,6 @@
|
|||||||
|
|
||||||
let element: Element
|
let element: Element
|
||||||
let buffer: TrustedEvent[] = []
|
let buffer: TrustedEvent[] = []
|
||||||
let events: TrustedEvent[] = []
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const scroller = createScroller({
|
const scroller = createScroller({
|
||||||
@@ -51,10 +56,14 @@
|
|||||||
<div class="col-4" bind:this={element}>
|
<div class="col-4" bind:this={element}>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#each events as event (event.id)}
|
{#each events as event (event.id)}
|
||||||
<NoteItem {url} {event} />
|
<div in:fly>
|
||||||
|
<NoteItem {url} {event} />
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<p class="center my-12 flex">
|
{#if !hideLoading}
|
||||||
<Spinner loading />
|
<p class="center my-12 flex">
|
||||||
</p>
|
<Spinner loading />
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {SvelteComponent} from "svelte"
|
import type {SvelteComponent} from "svelte"
|
||||||
|
import {derived} from "svelte/store"
|
||||||
import {type Instance} from "tippy.js"
|
import {type Instance} from "tippy.js"
|
||||||
import {append, remove, uniq} from "@welshman/lib"
|
import {append, remove, uniq} from "@welshman/lib"
|
||||||
import {profileSearch} from "@welshman/app"
|
import {profileSearch} from "@welshman/app"
|
||||||
|
import {Suggestions} from "@welshman/editor"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Suggestions from "@lib/editor/Suggestions.svelte"
|
import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
|
||||||
import SuggestionProfile from "@lib/editor/SuggestionProfile.svelte"
|
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import {pubkeyLink} from "@app/state"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let value: string[]
|
export let value: string[]
|
||||||
export let autofocus = false
|
export let autofocus = false
|
||||||
@@ -20,6 +21,8 @@
|
|||||||
let popover: Instance
|
let popover: Instance
|
||||||
let instance: SvelteComponent
|
let instance: SvelteComponent
|
||||||
|
|
||||||
|
const search = derived(profileSearch, $profileSearch => $profileSearch.searchValues)
|
||||||
|
|
||||||
const selectPubkey = (pubkey: string) => {
|
const selectPubkey = (pubkey: string) => {
|
||||||
term = ""
|
term = ""
|
||||||
popover.hide()
|
popover.hide()
|
||||||
@@ -48,13 +51,14 @@
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div>
|
<div>
|
||||||
{#each value as pubkey (pubkey)}
|
{#each value as pubkey (pubkey)}
|
||||||
|
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
|
||||||
<div class="flex-inline badge badge-neutral mr-1 gap-1">
|
<div class="flex-inline badge badge-neutral mr-1 gap-1">
|
||||||
<Button class="flex items-center" on:click={() => removePubkey(pubkey)}>
|
<Button class="flex items-center" on:click={() => removePubkey(pubkey)}>
|
||||||
<Icon icon="close-circle" size={4} class="-ml-1 mt-px" />
|
<Icon icon="close-circle" size={4} class="-ml-1 mt-px" />
|
||||||
</Button>
|
</Button>
|
||||||
<Link external href={pubkeyLink(pubkey)}>
|
<Button on:click={onClick}>
|
||||||
<ProfileName {pubkey} />
|
<ProfileName {pubkey} />
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -75,9 +79,9 @@
|
|||||||
component={Suggestions}
|
component={Suggestions}
|
||||||
props={{
|
props={{
|
||||||
term,
|
term,
|
||||||
|
search,
|
||||||
select: selectPubkey,
|
select: selectPubkey,
|
||||||
search: profileSearch,
|
component: ProfileSuggestion,
|
||||||
component: SuggestionProfile,
|
|
||||||
class: "rounded-box",
|
class: "rounded-box",
|
||||||
style: `left: 4px; width: ${input?.clientWidth + 12}px`,
|
style: `left: 4px; width: ${input?.clientWidth + 12}px`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {groupBy, uniqBy} from "@welshman/lib"
|
import {groupBy, uniq, uniqBy, batch} from "@welshman/lib"
|
||||||
import {REACTION} from "@welshman/util"
|
import {REACTION, getTag, REPORT, DELETE} from "@welshman/util"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {deriveEvents} from "@welshman/store"
|
import {deriveEvents} from "@welshman/store"
|
||||||
import {pubkey, repository, load, displayProfileByPubkey} from "@welshman/app"
|
import {pubkey, repository, load, displayProfileByPubkey} from "@welshman/app"
|
||||||
import {displayList} from "@lib/util"
|
import {displayList} from "@lib/util"
|
||||||
|
import {isMobile} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import EventReportDetails from "@app/components/EventReportDetails.svelte"
|
||||||
import {displayReaction} from "@app/state"
|
import {displayReaction} from "@app/state"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
export let onReactionClick
|
export let onReactionClick
|
||||||
export let relays: string[] = []
|
export let url = ""
|
||||||
|
export let reactionClass = ""
|
||||||
|
export let noTooltip = false
|
||||||
|
|
||||||
const filters = [{kinds: [REACTION], "#e": [event.id]}]
|
const reports = deriveEvents(repository, {
|
||||||
const reactions = deriveEvents(repository, {filters})
|
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
||||||
|
})
|
||||||
|
|
||||||
|
const reactions = deriveEvents(repository, {
|
||||||
|
filters: [{kinds: [REACTION], "#e": [event.id]}],
|
||||||
|
})
|
||||||
|
|
||||||
|
const onReportClick = () => pushModal(EventReportDetails, {url, event})
|
||||||
|
|
||||||
|
$: reportReasons = uniq($reports.map(e => getTag("e", e.tags)?.[2]))
|
||||||
|
|
||||||
$: groupedReactions = groupBy(
|
$: groupedReactions = groupBy(
|
||||||
e => e.content,
|
e => e.content,
|
||||||
@@ -20,12 +36,32 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
load({relays, filters})
|
load({
|
||||||
|
relays: [url],
|
||||||
|
filters: [{kinds: [REACTION, REPORT, DELETE], "#e": [event.id]}],
|
||||||
|
onEvent: batch(300, (events: TrustedEvent[]) => {
|
||||||
|
load({
|
||||||
|
relays: [url],
|
||||||
|
filters: [{kinds: [DELETE], "#e": events.map(e => e.id)}],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $reactions.length > 0}
|
{#if $reactions.length > 0 || $reports.length > 0}
|
||||||
<div class="flex min-w-0 flex-wrap gap-2">
|
<div class="flex min-w-0 flex-wrap gap-2">
|
||||||
|
{#if url && $reports.length > 0}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-tip="{`This content has been reported as "${displayList(reportReasons)}".`}}"
|
||||||
|
class="btn btn-error btn-xs tooltip-right flex items-center gap-1 rounded-full"
|
||||||
|
class:tooltip={!noTooltip && !isMobile}
|
||||||
|
on:click|preventDefault|stopPropagation={onReportClick}>
|
||||||
|
<Icon icon="danger" />
|
||||||
|
<span>{$reports.length}</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{#each groupedReactions.entries() as [content, events]}
|
{#each groupedReactions.entries() as [content, events]}
|
||||||
{@const pubkeys = events.map(e => e.pubkey)}
|
{@const pubkeys = events.map(e => e.pubkey)}
|
||||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||||
@@ -35,7 +71,8 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-tip={tooltip}
|
data-tip={tooltip}
|
||||||
class="flex-inline btn btn-neutral btn-xs tooltip gap-1 rounded-full"
|
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full {reactionClass}"
|
||||||
|
class:tooltip={!noTooltip && !isMobile}
|
||||||
class:border={isOwn}
|
class:border={isOwn}
|
||||||
class:border-solid={isOwn}
|
class:border-solid={isOwn}
|
||||||
class:border-primary={isOwn}
|
class:border-primary={isOwn}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {deriveEvents} from "@welshman/store"
|
|
||||||
import {repository, load} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import {COMMENT} from "@app/state"
|
|
||||||
|
|
||||||
export let event
|
|
||||||
export let relays: string[] = []
|
|
||||||
|
|
||||||
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
|
|
||||||
const replies = deriveEvents(repository, {filters})
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
load({relays, filters})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $replies.length > 0}
|
|
||||||
<button type="button" on:click class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full">
|
|
||||||
<Icon icon="reply" />
|
|
||||||
{$replies.length}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
@@ -1,21 +1,48 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
|
import {randomId} from "@welshman/lib"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
import {deriveRelay} from "@welshman/app"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {addRoomMembership} from "@app/commands"
|
import {hasNip29} from "@app/state"
|
||||||
|
import {addRoomMembership, nip29, getThunkError} from "@app/commands"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
|
const room = randomId()
|
||||||
|
const relay = deriveRelay(url)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const tryCreate = async () => {
|
const tryCreate = async () => {
|
||||||
addRoomMembership(url, room)
|
if (hasNip29($relay)) {
|
||||||
|
const createMessage = await getThunkError(nip29.createRoom(url, room))
|
||||||
|
|
||||||
|
if (createMessage && !createMessage.match(/^duplicate:|already a member/)) {
|
||||||
|
return pushToast({theme: "error", message: createMessage})
|
||||||
|
}
|
||||||
|
|
||||||
|
const editMessage = await getThunkError(nip29.editMeta(url, room, {name}))
|
||||||
|
|
||||||
|
if (editMessage) {
|
||||||
|
return pushToast({theme: "error", message: editMessage})
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinMessage = await getThunkError(nip29.joinRoom(url, room))
|
||||||
|
|
||||||
|
if (joinMessage && !joinMessage.includes("already")) {
|
||||||
|
return pushToast({theme: "error", message: joinMessage})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addRoomMembership(url, room, name)
|
||||||
goto(makeSpacePath(url, room))
|
goto(makeSpacePath(url, room))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let room = ""
|
let name = ""
|
||||||
let loading = false
|
let loading = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -44,7 +71,7 @@
|
|||||||
<p slot="label">Room Name</p>
|
<p slot="label">Room Name</p>
|
||||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
<Icon icon="hashtag" />
|
<Icon icon="hashtag" />
|
||||||
<input bind:value={room} class="grow" type="text" />
|
<input bind:value={name} class="grow" type="text" />
|
||||||
</label>
|
</label>
|
||||||
</Field>
|
</Field>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
@@ -52,7 +79,7 @@
|
|||||||
<Icon icon="alt-arrow-left" />
|
<Icon icon="alt-arrow-left" />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={!room || loading}>
|
<Button type="submit" class="btn btn-primary" disabled={!name || loading}>
|
||||||
<Spinner {loading}>Create Room</Spinner>
|
<Spinner {loading}>Create Room</Spinner>
|
||||||
<Icon icon="alt-arrow-right" />
|
<Icon icon="alt-arrow-right" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,24 +1,55 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {postJson, assoc} from "@welshman/lib"
|
||||||
import {makeSecret, Nip46Broker} from "@welshman/signer"
|
import {makeSecret, Nip46Broker} from "@welshman/signer"
|
||||||
import {addSession, nip46Perms, loadHandle} from "@welshman/app"
|
import {pubkey, loadHandle, updateSession} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import LogIn from "@app/components/LogIn.svelte"
|
import LogIn from "@app/components/LogIn.svelte"
|
||||||
import InfoNostr from "@app/components/InfoNostr.svelte"
|
import InfoNostr from "@app/components/InfoNostr.svelte"
|
||||||
|
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
||||||
import {pushModal, clearModals} from "@app/modal"
|
import {pushModal, clearModals} from "@app/modal"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {BURROW_URL, PLATFORM_NAME, NIP46_PERMS} from "@app/state"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
import {loginWithNip46} from "@app/commands"
|
||||||
|
|
||||||
|
const relays = ["wss://relay.nsec.app"]
|
||||||
|
|
||||||
|
const signerDomain = "nsec.app"
|
||||||
|
|
||||||
|
const signerPubkey = "e24a86943d37a91ab485d6f9a7c66097c25ddd67e8bd1b75ed252a3c266cf9bb"
|
||||||
|
|
||||||
const login = () => pushModal(LogIn)
|
const login = () => pushModal(LogIn)
|
||||||
|
|
||||||
const trySignup = async () => {
|
const withLoading =
|
||||||
const secret = makeSecret()
|
(cb: (...args: any[]) => any) =>
|
||||||
const handle = await loadHandle(`${username}@${handler.domain}`)
|
async (...args: any[]) => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cb(...args)
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const signupPassword = withLoading(async () => {
|
||||||
|
const res = await postJson(BURROW_URL + "/user", {email, password})
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
return pushToast({message: res.error, theme: "error"})
|
||||||
|
}
|
||||||
|
|
||||||
|
pushModal(SignUpSuccess, {email}, {replaceState: true})
|
||||||
|
})
|
||||||
|
|
||||||
|
const signupNsecApp = withLoading(async () => {
|
||||||
|
const handle = await loadHandle(`${username}@${signerDomain}`)
|
||||||
|
|
||||||
if (handle?.pubkey) {
|
if (handle?.pubkey) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -27,48 +58,49 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const signupBroker = Nip46Broker.get({secret, handler})
|
const clientSecret = makeSecret()
|
||||||
const pubkey = await signupBroker.createAccount(username, nip46Perms)
|
const broker = Nip46Broker.get({
|
||||||
|
relays,
|
||||||
|
clientSecret,
|
||||||
|
signerPubkey,
|
||||||
|
algorithm: "nip04",
|
||||||
|
})
|
||||||
|
|
||||||
if (!pubkey) {
|
const userPubkey = await broker.createAccount(username, signerDomain, NIP46_PERMS)
|
||||||
|
|
||||||
|
if (!userPubkey) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Sorry, it looks like something went wrong. Please try again.",
|
message: "Sorry, it looks like something went wrong. Please try again.",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gotta use user pubkey as the handler pubkey for historical reasons
|
// Now we can log in. Use the user's pubkey for the handler (legacy stuff)
|
||||||
const loginBroker = Nip46Broker.get({secret, handler: {...handler, pubkey}})
|
const success = await loginWithNip46({relays, signerPubkey: userPubkey, clientSecret})
|
||||||
|
|
||||||
if (await loginBroker.connect("", nip46Perms)) {
|
if (!success) {
|
||||||
addSession({method: "nip46", pubkey, secret, handler: {...handler, pubkey}})
|
return pushToast({
|
||||||
pushToast({message: "Successfully logged in!"})
|
|
||||||
setChecked("*")
|
|
||||||
clearModals()
|
|
||||||
} else {
|
|
||||||
pushToast({
|
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Something went wrong! Please try again.",
|
message: "Sorry, it looks like something went wrong. Please try again.",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const signup = async () => {
|
updateSession($pubkey!, assoc("email", email))
|
||||||
loading = true
|
pushToast({message: "Successfully logged in!"})
|
||||||
|
setChecked("*")
|
||||||
|
clearModals()
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
const signup = () => {
|
||||||
await trySignup()
|
if (BURROW_URL) {
|
||||||
} finally {
|
signupPassword()
|
||||||
loading = false
|
} else {
|
||||||
|
signupNsecApp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handler = {
|
let email = ""
|
||||||
domain: "nsec.app",
|
let password = ""
|
||||||
relays: ["wss://relay.nsec.app"],
|
|
||||||
pubkey: "e24a86943d37a91ab485d6f9a7c66097c25ddd67e8bd1b75ed252a3c266cf9bb",
|
|
||||||
}
|
|
||||||
|
|
||||||
let username = ""
|
let username = ""
|
||||||
let loading = false
|
let loading = false
|
||||||
</script>
|
</script>
|
||||||
@@ -80,21 +112,50 @@
|
|||||||
<Button class="link" on:click={() => pushModal(InfoNostr)}>nostr protocol</Button>, which allows
|
<Button class="link" on:click={() => pushModal(InfoNostr)}>nostr protocol</Button>, which allows
|
||||||
you to own your social identity.
|
you to own your social identity.
|
||||||
</p>
|
</p>
|
||||||
<Field>
|
{#if BURROW_URL}
|
||||||
<div class="flex items-center gap-2" slot="input">
|
<FieldInline>
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
<p slot="label">Email</p>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
<Icon icon="user-rounded" />
|
<Icon icon="user-rounded" />
|
||||||
<input bind:value={username} class="grow" type="text" placeholder="username" />
|
<input bind:value={email} />
|
||||||
</label>
|
</label>
|
||||||
@{handler.domain}
|
</FieldInline>
|
||||||
</div>
|
<FieldInline>
|
||||||
</Field>
|
<p slot="label">Password</p>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={!username || loading}>
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
<Spinner {loading}>Sign Up</Spinner>
|
<Icon icon="key" />
|
||||||
<Icon icon="alt-arrow-right" />
|
<input bind:value={password} type="password" />
|
||||||
</Button>
|
</label>
|
||||||
|
</FieldInline>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading || !email || !password}>
|
||||||
|
<Spinner {loading}>Sign Up</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</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>
|
||||||
|
{:else}
|
||||||
|
<Field>
|
||||||
|
<div class="flex items-center gap-2" slot="input">
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="user-rounded" />
|
||||||
|
<input bind:value={username} class="grow" type="text" placeholder="username" />
|
||||||
|
</label>
|
||||||
|
@{signerDomain}
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading || !username}>
|
||||||
|
<Spinner {loading}>Sign Up</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<Divider>Or</Divider>
|
<Divider>Or</Divider>
|
||||||
<Link external href="https://nosta.me" class="btn {username ? 'btn-neutral' : 'btn-primary'}">
|
<Link
|
||||||
|
external
|
||||||
|
href="https://nosta.me"
|
||||||
|
class="btn {username || email || password ? 'btn-neutral' : 'btn-primary'}">
|
||||||
<Icon icon="square-share-line" />
|
<Icon icon="square-share-line" />
|
||||||
Get started on Nosta.me
|
Get started on Nosta.me
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
export let email
|
||||||
|
|
||||||
|
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" on:click={login}>Back to Login</Button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
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 {pushToast} from "@app/toast"
|
||||||
|
import {clearModals} from "@app/modal"
|
||||||
|
import {attemptRelayAccess} from "@app/commands"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let error
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const joinRelay = async (claim: string) => {
|
||||||
|
const error = await attemptRelayAccess(url, claim)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return pushToast({theme: "error", message: error})
|
||||||
|
}
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
message: "You have successfully joined the space!",
|
||||||
|
})
|
||||||
|
|
||||||
|
clearModals()
|
||||||
|
}
|
||||||
|
|
||||||
|
const join = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await joinRelay(claim)
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let claim = ""
|
||||||
|
let loading = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" on:submit|preventDefault={join}>
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Access Error</div>
|
||||||
|
<div slot="info">We couldn't connect you to this space.</div>
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
We received an error from the relay indicating you don't have access to {displayRelayUrl(url)}.
|
||||||
|
</p>
|
||||||
|
<p class="border-l border-solid border-error pl-4 text-error">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
<p>If you have one, you can try entering an invite code below to request access.</p>
|
||||||
|
<Field>
|
||||||
|
<p slot="label">Invite code</p>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||||
|
<Icon icon="link-round" />
|
||||||
|
<input bind:value={claim} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
<p slot="info">Enter an invite code provided to you by the admin of the relay.</p>
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" on:click={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={!claim || loading}>
|
||||||
|
<Spinner {loading}>Request Access</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
let url = ""
|
let url = ""
|
||||||
let loading = false
|
let loading = false
|
||||||
|
|
||||||
$: linkIsValid = Boolean(tryCatch(() => isRelayUrl(normalizeRelayUrl(url))))
|
$: linkIsValid = Boolean(tryCatch(() => isRelayUrl(normalizeRelayUrl(url.split("|")[0]))))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" on:submit|preventDefault={join}>
|
<form class="column gap-4" on:submit|preventDefault={join}>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import {max} from "@welshman/lib"
|
import {max} from "@welshman/lib"
|
||||||
import {deriveEvents, deriveIsDeleted} from "@welshman/store"
|
import {deriveEvents, deriveIsDeleted} from "@welshman/store"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {COMMENT} from "@welshman/util"
|
||||||
import {thunks, load, pubkey, repository, formatTimestampRelative} from "@welshman/app"
|
import {thunks, load, pubkey, repository, formatTimestampRelative} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
@@ -14,9 +15,8 @@
|
|||||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
||||||
import ThreadMenu from "@app/components/ThreadMenu.svelte"
|
import ThreadMenu from "@app/components/ThreadMenu.svelte"
|
||||||
import {publishDelete, publishReaction} from "@app/commands"
|
import {publishDelete, publishReaction} from "@app/commands"
|
||||||
import {deriveNotification} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {COMMENT} from "@app/state"
|
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
export let event
|
export let event
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
const path = makeSpacePath(url, "threads", event.id)
|
const path = makeSpacePath(url, "threads", event.id)
|
||||||
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
|
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
|
||||||
const replies = deriveEvents(repository, {filters})
|
const replies = deriveEvents(repository, {filters})
|
||||||
const notification = deriveNotification(path, filters, url)
|
|
||||||
|
|
||||||
const showPopover = () => popover.show()
|
const showPopover = () => popover.show()
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) =>
|
const onEmoji = (emoji: NativeEmoji) =>
|
||||||
publishReaction({event, relays: [url], content: emoji.unicode})
|
publishReaction({event, content: emoji.unicode, relays: [url]})
|
||||||
|
|
||||||
let popover: Instance
|
let popover: Instance
|
||||||
|
|
||||||
@@ -56,8 +55,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<ReactionSummary relays={[url]} {event} {onReactionClick} />
|
|
||||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-left" />
|
||||||
{#if $deleted}
|
{#if $deleted}
|
||||||
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
|
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
|
||||||
{:else if thunk}
|
{:else if thunk}
|
||||||
@@ -69,7 +68,7 @@
|
|||||||
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
|
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn btn-neutral btn-xs relative hidden rounded-full sm:flex">
|
<div class="btn btn-neutral btn-xs relative hidden rounded-full sm:flex">
|
||||||
{#if $notification}
|
{#if $notifications.has(path)}
|
||||||
<div class="h-2 w-2 rounded-full bg-primary" />
|
<div class="h-2 w-2 rounded-full bg-primary" />
|
||||||
{/if}
|
{/if}
|
||||||
Active {formatTimestampRelative(lastActive)}
|
Active {formatTimestampRelative(lastActive)}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import type {Readable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
import {createEvent, THREAD} from "@welshman/util"
|
||||||
import {createEvent} from "@welshman/util"
|
|
||||||
import {publishThunk} from "@welshman/app"
|
import {publishThunk} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -11,16 +10,17 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {THREAD, GENERAL, tagRoom} from "@app/state"
|
import {GENERAL, tagRoom, PROTECTED} from "@app/state"
|
||||||
import {getPubkeyHints} from "@app/commands"
|
import {getEditor} from "@app/editor"
|
||||||
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
|
const uploading = writable(false)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if ($loading) return
|
if ($uploading) return
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = $editor.getText({blockSeparator: "\n"})
|
const content = $editor.getText({blockSeparator: "\n"}).trim()
|
||||||
|
|
||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -38,7 +38,12 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = [["title", title], tagRoom(GENERAL, url), ...getEditorTags($editor)]
|
const tags = [
|
||||||
|
...$editor.storage.nostr.getEditorTags(),
|
||||||
|
tagRoom(GENERAL, url),
|
||||||
|
["title", title],
|
||||||
|
PROTECTED,
|
||||||
|
]
|
||||||
|
|
||||||
publishThunk({
|
publishThunk({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
@@ -49,14 +54,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let title: string
|
let title: string
|
||||||
let editor: Readable<Editor>
|
let element: HTMLElement
|
||||||
|
let editor: ReturnType<typeof getEditor>
|
||||||
$: loading = $editor?.storage.fileUpload.loading
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor = createEditor(
|
editor = getEditor({submit, element, uploading, placeholder: "What's on your mind?"})
|
||||||
getEditorOptions({submit, getPubkeyHints, placeholder: "What's on your mind?"}),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -81,14 +83,14 @@
|
|||||||
<Field>
|
<Field>
|
||||||
<p slot="label">Message*</p>
|
<p slot="label">Message*</p>
|
||||||
<div slot="input" class="note-editor flex-grow overflow-hidden">
|
<div slot="input" class="note-editor flex-grow overflow-hidden">
|
||||||
<EditorContent editor={$editor} />
|
<div bind:this={element} />
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||||
on:click={$editor.commands.selectFiles}>
|
on:click={$editor.commands.selectFiles}>
|
||||||
{#if $loading}
|
{#if $uploading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon="paperclip" size={3} />
|
<Icon icon="paperclip" size={3} />
|
||||||
|
|||||||
@@ -4,31 +4,40 @@
|
|||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ThreadActions from "@app/components/ThreadActions.svelte"
|
import ThreadActions from "@app/components/ThreadActions.svelte"
|
||||||
import {makeThreadPath} from "@app/routes"
|
import {makeThreadPath} from "@app/routes"
|
||||||
import {pubkeyLink} from "@app/state"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
export let event
|
export let event
|
||||||
export let hideActions = false
|
export let hideActions = false
|
||||||
|
|
||||||
const title = event.tags.find(nthEq(0, "title"))?.[1]
|
const title = event.tags.find(nthEq(0, "title"))?.[1]
|
||||||
|
|
||||||
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeThreadPath(url, event.id)}>
|
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeThreadPath(url, event.id)}>
|
||||||
<div class="flex w-full items-center justify-between gap-2">
|
{#if title}
|
||||||
<p class="text-xl">{title}</p>
|
<div class="flex w-full items-center justify-between gap-2">
|
||||||
<p class="text-sm opacity-75">
|
<p class="text-xl">{title}</p>
|
||||||
|
<p class="text-sm opacity-75">
|
||||||
|
{formatTimestamp(event.created_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="mb-3 h-0 text-xs opacity-75">
|
||||||
{formatTimestamp(event.created_at)}
|
{formatTimestamp(event.created_at)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
{/if}
|
||||||
<Content {event} expandMode="inline" />
|
<Content {event} expandMode="inline" quoteProps={{relays: [url]}} />
|
||||||
<div class="flex w-full items-end justify-between gap-2">
|
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||||
Posted by
|
Posted by
|
||||||
<Link external href={pubkeyLink(event.pubkey)} class="link-content">
|
<button type="button" on:click|preventDefault={openProfile} class="link-content">
|
||||||
@<ProfileName pubkey={event.pubkey} />
|
@<ProfileName pubkey={event.pubkey} />
|
||||||
</Link>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
{#if !hideActions}
|
{#if !hideActions}
|
||||||
<ThreadActions showActivity {url} {event} />
|
<ThreadActions showActivity {url} {event} />
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {COMMENT} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
|
import EventReport from "@app/components/EventReport.svelte"
|
||||||
import ThreadShare from "@app/components/ThreadShare.svelte"
|
import ThreadShare from "@app/components/ThreadShare.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {COMMENT} from "@app/state"
|
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
export let event
|
export let event
|
||||||
@@ -14,6 +15,11 @@
|
|||||||
|
|
||||||
const isRoot = event.kind !== COMMENT
|
const isRoot = event.kind !== COMMENT
|
||||||
|
|
||||||
|
const report = () => {
|
||||||
|
onClick()
|
||||||
|
pushModal(EventReport, {url, event})
|
||||||
|
}
|
||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(EventInfo, {event})
|
pushModal(EventInfo, {event})
|
||||||
@@ -52,5 +58,12 @@
|
|||||||
Delete Message
|
Delete Message
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" on:click={report}>
|
||||||
|
<Icon size={4} icon="danger" />
|
||||||
|
Report Content
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import type {Readable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
|
||||||
import {append} from "@welshman/lib"
|
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import {fly, slideAndFade} from "@lib/transition"
|
import {fly, slideAndFade} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
import {publishComment} from "@app/commands"
|
||||||
import {getPubkeyHints, publishComment} from "@app/commands"
|
import {tagRoom, GENERAL, PROTECTED} from "@app/state"
|
||||||
import {tagRoom, GENERAL} from "@app/state"
|
import {getEditor} from "@app/editor"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
@@ -18,13 +16,15 @@
|
|||||||
export let onClose
|
export let onClose
|
||||||
export let onSubmit
|
export let onSubmit
|
||||||
|
|
||||||
|
const uploading = writable(false)
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if ($loading) return
|
if ($uploading) return
|
||||||
|
|
||||||
const content = $editor.getText({blockSeparator: "\n"})
|
const content = $editor.getText({blockSeparator: "\n"}).trim()
|
||||||
const tags = append(tagRoom(GENERAL, url), getEditorTags($editor))
|
const tags = [...$editor.storage.nostr.getEditorTags(), tagRoom(GENERAL, url), PROTECTED]
|
||||||
|
|
||||||
if (!content.trim()) {
|
if (!content) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Please provide a message for your reply.",
|
message: "Please provide a message for your reply.",
|
||||||
@@ -34,12 +34,11 @@
|
|||||||
onSubmit(publishComment({event, content, tags, relays: [url]}))
|
onSubmit(publishComment({event, content, tags, relays: [url]}))
|
||||||
}
|
}
|
||||||
|
|
||||||
let editor: Readable<Editor>
|
let editor: ReturnType<typeof getEditor>
|
||||||
|
let element: HTMLElement
|
||||||
$: loading = $editor?.storage.fileUpload.loading
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor = createEditor(getEditorOptions({submit, getPubkeyHints, autofocus: !isMobile}))
|
editor = getEditor({element, submit, uploading, autofocus: !isMobile})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -47,16 +46,16 @@
|
|||||||
in:fly
|
in:fly
|
||||||
out:slideAndFade
|
out:slideAndFade
|
||||||
on:submit|preventDefault={submit}
|
on:submit|preventDefault={submit}
|
||||||
class="card2 sticky bottom-2 z-feature mx-2 mt-2 bg-neutral">
|
class="card2 sticky bottom-2 z-feature mx-2 mt-4 bg-neutral">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="note-editor flex-grow overflow-hidden">
|
<div class="note-editor flex-grow overflow-hidden">
|
||||||
<EditorContent editor={$editor} />
|
<div bind:this={element} />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||||
on:click={$editor.commands.selectFiles}>
|
on:click={$editor.commands.selectFiles}>
|
||||||
{#if $loading}
|
{#if $uploading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon="paperclip" size={3} />
|
<Icon icon="paperclip" size={3} />
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {roomsByUrl} from "@app/state"
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
|
import {channelsByUrl} from "@app/state"
|
||||||
import {makeRoomPath} from "@app/routes"
|
import {makeRoomPath} from "@app/routes"
|
||||||
import {setKey} from "@app/implicit"
|
import {setKey} from "@app/implicit"
|
||||||
|
|
||||||
@@ -37,14 +38,14 @@
|
|||||||
<div slot="info">Which room would you like to share this thread to?</div>
|
<div slot="info">Which room would you like to share this thread to?</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
{#each $roomsByUrl.get(url) || [] as room (room)}
|
{#each $channelsByUrl.get(url) || [] as channel (channel.room)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn"
|
class="btn"
|
||||||
class:btn-neutral={selection !== room}
|
class:btn-neutral={selection !== channel.room}
|
||||||
class:btn-primary={selection === room}
|
class:btn-primary={selection === channel.room}
|
||||||
on:click={() => toggleRoom(room)}>
|
on:click={() => toggleRoom(channel.room)}>
|
||||||
#{room}
|
#<ChannelName {...channel} />
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,9 +30,11 @@
|
|||||||
$: isFailure = !canCancel && ps.every(s => [Failure, Timeout].includes(s.status))
|
$: isFailure = !canCancel && ps.every(s => [Failure, Timeout].includes(s.status))
|
||||||
$: failure = Object.entries($status).find(([url, s]) => [Failure, Timeout].includes(s.status))
|
$: failure = Object.entries($status).find(([url, s]) => [Failure, Timeout].includes(s.status))
|
||||||
|
|
||||||
|
// Delay updating isPending so users can see that the message is sent
|
||||||
$: {
|
$: {
|
||||||
// Delay updating isPending so users can see that the message is sent
|
isPending = isPending || ps.some(s => s.status === Pending)
|
||||||
if (!ps.some(s => s.status == Pending)) {
|
|
||||||
|
if (!ps.some(s => s.status === Pending)) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isPending = false
|
isPending = false
|
||||||
}, 2000)
|
}, 2000)
|
||||||
@@ -40,20 +42,22 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-end px-1 text-xs {$$props.class}">
|
{#if isFailure && failure}
|
||||||
{#if isFailure && failure}
|
{@const [url, {message, status}] = failure}
|
||||||
{@const [url, {message, status}] = failure}
|
<div class="flex justify-end px-1 text-xs {$$props.class}">
|
||||||
<Tippy
|
<Tippy
|
||||||
class={$$props.class}
|
class="flex items-center {$$props.class}"
|
||||||
component={ThunkStatusDetail}
|
component={ThunkStatusDetail}
|
||||||
props={{url, message, status, retry}}
|
props={{url, message, status, retry}}
|
||||||
params={{interactive: true}}>
|
params={{interactive: true}}>
|
||||||
<span class="tooltip flex cursor-pointer items-center gap-1">
|
<span class="flex cursor-pointer items-center gap-1 text-error">
|
||||||
<Icon icon="danger" size={3} />
|
<Icon icon="danger" size={3} />
|
||||||
<span class="opacity-50">Failed to send!</span>
|
<span>Failed to send!</span>
|
||||||
</span>
|
</span>
|
||||||
</Tippy>
|
</Tippy>
|
||||||
{:else if canCancel || isPending}
|
</div>
|
||||||
|
{:else if canCancel || isPending}
|
||||||
|
<div class="flex justify-end px-1 text-xs {$$props.class}">
|
||||||
<span class="flex items-center gap-1 {$$props.class}">
|
<span class="flex items-center gap-1 {$$props.class}">
|
||||||
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
|
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
|
||||||
<span class="opacity-50">Sending...</span>
|
<span class="opacity-50">Sending...</span>
|
||||||
@@ -61,5 +65,5 @@
|
|||||||
<Button class="link" on:click={abort}>Cancel</Button>
|
<Button class="link" on:click={abort}>Cancel</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card2 bg-alt col-2">
|
<div class="card2 bg-alt col-2 shadow-2xl">
|
||||||
<p>
|
<p>
|
||||||
Failed to publish to {displayRelayUrl(url)}: {message}.
|
Failed to publish to {displayRelayUrl(url)}: {message}.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{#key $toast.id}
|
{#key $toast.id}
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
class="alert flex justify-center"
|
class="alert flex justify-center whitespace-normal text-left"
|
||||||
class:bg-base-100={theme === "info"}
|
class:bg-base-100={theme === "info"}
|
||||||
class:text-base-content={theme === "info"}
|
class:text-base-content={theme === "info"}
|
||||||
class:alert-error={theme === "error"}>
|
class:alert-error={theme === "error"}>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {NodeViewProps} from "@tiptap/core"
|
||||||
|
import {NodeViewWrapper} from "svelte-tiptap"
|
||||||
|
import {deriveProfileDisplay} from "@welshman/app"
|
||||||
|
|
||||||
|
export let node: NodeViewProps["node"]
|
||||||
|
export let selected: NodeViewProps["selected"]
|
||||||
|
|
||||||
|
const display = deriveProfileDisplay(node.attrs.pubkey)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeViewWrapper as="span">
|
||||||
|
<button class="tiptap-object {selected ? 'tiptap-active' : ''}">
|
||||||
|
@{$display}
|
||||||
|
</button>
|
||||||
|
</NodeViewWrapper>
|
||||||
@@ -3,18 +3,16 @@
|
|||||||
import {
|
import {
|
||||||
userFollows,
|
userFollows,
|
||||||
deriveUserWotScore,
|
deriveUserWotScore,
|
||||||
deriveProfile,
|
|
||||||
deriveHandleForPubkey,
|
deriveHandleForPubkey,
|
||||||
displayHandle,
|
displayHandle,
|
||||||
deriveProfileDisplay,
|
deriveProfileDisplay,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
|
||||||
import WotScore from "@lib/components/WotScore.svelte"
|
import WotScore from "@lib/components/WotScore.svelte"
|
||||||
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
const pubkey = value
|
const pubkey = value
|
||||||
const profile = deriveProfile(pubkey)
|
|
||||||
const profileDisplay = deriveProfileDisplay(pubkey)
|
const profileDisplay = deriveProfileDisplay(pubkey)
|
||||||
const handle = deriveHandleForPubkey(pubkey)
|
const handle = deriveHandleForPubkey(pubkey)
|
||||||
const score = deriveUserWotScore(pubkey)
|
const score = deriveUserWotScore(pubkey)
|
||||||
@@ -24,11 +22,11 @@
|
|||||||
|
|
||||||
<div class="flex max-w-full gap-3">
|
<div class="flex max-w-full gap-3">
|
||||||
<div class="py-1">
|
<div class="py-1">
|
||||||
<Avatar src={$profile?.picture} size={10} />
|
<ProfileCircle {pubkey} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex min-w-0 flex-col">
|
<div class="flex min-w-0 flex-col">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-bold overflow-hidden text-ellipsis">
|
<div class="text-bold overflow-hidden text-ellipsis text-base">
|
||||||
{$profileDisplay}
|
{$profileDisplay}
|
||||||
</div>
|
</div>
|
||||||
<WotScore score={$score} active={following} />
|
<WotScore score={$score} active={following} />
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import type {Writable} from "svelte/store"
|
||||||
|
import {derived} from "svelte/store"
|
||||||
|
import {createEditor, SvelteNodeViewRenderer} from "svelte-tiptap"
|
||||||
|
import {ctx} from "@welshman/lib"
|
||||||
|
import type {StampedEvent} from "@welshman/util"
|
||||||
|
import {signer, profileSearch} from "@welshman/app"
|
||||||
|
import {MentionSuggestion, WelshmanExtension} from "@welshman/editor"
|
||||||
|
import {getSetting, userSettingValues} from "@app/state"
|
||||||
|
import ProfileSuggestion from "./ProfileSuggestion.svelte"
|
||||||
|
import EditMention from "./EditMention.svelte"
|
||||||
|
|
||||||
|
export const getUploadType = () => getSetting<"nip96" | "blossom">("upload_type")
|
||||||
|
|
||||||
|
export const getUploadUrl = () => {
|
||||||
|
const {upload_type, nip96_urls, blossom_urls} = userSettingValues.get()
|
||||||
|
|
||||||
|
return upload_type === "nip96"
|
||||||
|
? nip96_urls[0] || "https://nostr.build"
|
||||||
|
: blossom_urls[0] || "https://cdn.satellite.earth"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const signWithAssert = async (template: StampedEvent) => {
|
||||||
|
const event = await signer.get().sign(template)
|
||||||
|
|
||||||
|
return event!
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEditor = ({
|
||||||
|
autofocus = false,
|
||||||
|
charCount,
|
||||||
|
content = "",
|
||||||
|
element,
|
||||||
|
placeholder = "",
|
||||||
|
submit,
|
||||||
|
uploading,
|
||||||
|
wordCount,
|
||||||
|
}: {
|
||||||
|
autofocus?: boolean
|
||||||
|
charCount?: Writable<number>
|
||||||
|
content?: string
|
||||||
|
element: HTMLElement
|
||||||
|
placeholder?: string
|
||||||
|
submit: () => void
|
||||||
|
uploading?: Writable<boolean>
|
||||||
|
wordCount?: Writable<number>
|
||||||
|
}) =>
|
||||||
|
createEditor({
|
||||||
|
element,
|
||||||
|
content,
|
||||||
|
autofocus,
|
||||||
|
extensions: [
|
||||||
|
WelshmanExtension.configure({
|
||||||
|
submit,
|
||||||
|
sign: signWithAssert,
|
||||||
|
defaultUploadType: getUploadType(),
|
||||||
|
defaultUploadUrl: getUploadUrl(),
|
||||||
|
extensions: {
|
||||||
|
placeholder: {
|
||||||
|
config: {
|
||||||
|
placeholder,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fileUpload: {
|
||||||
|
config: {
|
||||||
|
onDrop() {
|
||||||
|
uploading?.set(true)
|
||||||
|
},
|
||||||
|
onComplete() {
|
||||||
|
uploading?.set(false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nprofile: {
|
||||||
|
extend: {
|
||||||
|
addNodeView: () => SvelteNodeViewRenderer(EditMention),
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
MentionSuggestion({
|
||||||
|
editor: (this as any).editor,
|
||||||
|
search: derived(profileSearch, s => s.searchValues),
|
||||||
|
getRelays: (pubkey: string) => ctx.app.router.FromPubkeys([pubkey]).getUrls(),
|
||||||
|
component: ProfileSuggestion,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
onUpdate({editor}) {
|
||||||
|
wordCount?.set(editor.storage.wordCount.words)
|
||||||
|
charCount?.set(editor.storage.wordCount.chars)
|
||||||
|
},
|
||||||
|
})
|
||||||
+3
-1
@@ -7,6 +7,7 @@ export type ModalOptions = {
|
|||||||
drawer?: boolean
|
drawer?: boolean
|
||||||
fullscreen?: boolean
|
fullscreen?: boolean
|
||||||
replaceState?: boolean
|
replaceState?: boolean
|
||||||
|
path?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Modal = {
|
export type Modal = {
|
||||||
@@ -26,10 +27,11 @@ export const pushModal = (
|
|||||||
options: ModalOptions = {},
|
options: ModalOptions = {},
|
||||||
) => {
|
) => {
|
||||||
const id = randomId()
|
const id = randomId()
|
||||||
|
const path = options.path || ""
|
||||||
|
|
||||||
modals.update(assoc(id, {id, component, props, options}))
|
modals.update(assoc(id, {id, component, props, options}))
|
||||||
|
|
||||||
goto("#" + id, {replaceState: options.replaceState})
|
goto(path + "#" + id, {replaceState: options.replaceState})
|
||||||
|
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-61
@@ -1,85 +1,105 @@
|
|||||||
import {writable, derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {page} from "$app/stores"
|
import {synced, throttled} from "@welshman/store"
|
||||||
import {deriveEvents} from "@welshman/store"
|
import {pubkey} from "@welshman/app"
|
||||||
import {repository, pubkey} from "@welshman/app"
|
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
|
||||||
import {prop, max, sortBy, assoc, lt, now} from "@welshman/lib"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import type {Filter, TrustedEvent} from "@welshman/util"
|
import {MESSAGE, COMMENT, getTagValue} from "@welshman/util"
|
||||||
import {DIRECT_MESSAGE} from "@welshman/util"
|
import {makeSpacePath, makeChatPath, makeThreadPath, makeRoomPath} from "@app/routes"
|
||||||
import {makeSpacePath} from "@app/routes"
|
|
||||||
import {
|
import {
|
||||||
MESSAGE,
|
THREAD_FILTER,
|
||||||
THREAD,
|
COMMENT_FILTER,
|
||||||
COMMENT,
|
chats,
|
||||||
deriveEventsForUrl,
|
getUrlsForEvent,
|
||||||
getMembershipUrls,
|
userRoomsByUrl,
|
||||||
userMembership,
|
repositoryStore,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
|
|
||||||
// Checked state
|
// Checked state
|
||||||
|
|
||||||
export const checked = writable<Record<string, number>>({})
|
export const checked = synced<Record<string, number>>("checked", {})
|
||||||
|
|
||||||
export const deriveChecked = (key: string) => derived(checked, prop(key))
|
export const deriveChecked = (key: string) => derived(checked, prop(key))
|
||||||
|
|
||||||
export const setChecked = (key: string, ts = now()) =>
|
export const setChecked = (key: string) => checked.update(state => ({...state, [key]: now()}))
|
||||||
checked.update(state => ({...state, [key]: ts}))
|
|
||||||
|
|
||||||
// Filters for various routes
|
// Derived notifications state
|
||||||
|
|
||||||
export const CHAT_FILTERS: Filter[] = [{kinds: [DIRECT_MESSAGE]}]
|
export const notifications = derived(
|
||||||
|
throttled(
|
||||||
|
1000,
|
||||||
|
derived([pubkey, checked, chats, userRoomsByUrl, repositoryStore, getUrlsForEvent], identity),
|
||||||
|
),
|
||||||
|
([$pubkey, $checked, $chats, $userRoomsByUrl, $repository, $getUrlsForEvent]) => {
|
||||||
|
const hasNotification = (path: string, latestEvent: TrustedEvent | undefined) => {
|
||||||
|
if (!latestEvent || latestEvent.pubkey === $pubkey) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export const SPACE_FILTERS: Filter[] = [{kinds: [THREAD, MESSAGE, COMMENT]}]
|
for (const [entryPath, ts] of Object.entries($checked)) {
|
||||||
|
const isMatch =
|
||||||
|
entryPath === "*" ||
|
||||||
|
entryPath.startsWith(path) ||
|
||||||
|
(entryPath === "/chat/*" && path.startsWith("/chat/"))
|
||||||
|
|
||||||
export const ROOM_FILTERS: Filter[] = [{kinds: [MESSAGE]}]
|
if (isMatch && ts > latestEvent.created_at) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const THREAD_FILTERS: Filter[] = [
|
return true
|
||||||
{kinds: [THREAD]},
|
}
|
||||||
{kinds: [COMMENT], "#K": [String(THREAD)]},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const getNotificationFilters = (since: number): Filter[] =>
|
const paths = new Set<string>()
|
||||||
[...CHAT_FILTERS, ...SPACE_FILTERS, ...THREAD_FILTERS].map(assoc("since", since))
|
|
||||||
|
|
||||||
export const getRoomFilters = (room: string): Filter[] => ROOM_FILTERS.map(assoc("#~", [room]))
|
for (const {pubkeys, messages} of $chats) {
|
||||||
|
const chatPath = makeChatPath(pubkeys)
|
||||||
|
|
||||||
// Notification derivation
|
if (hasNotification(chatPath, messages[0])) {
|
||||||
|
paths.add("/chat")
|
||||||
|
paths.add(chatPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getNotification = (
|
const allThreadEvents = $repository.query([THREAD_FILTER, COMMENT_FILTER])
|
||||||
pubkey: string | null,
|
const allMessageEvents = $repository.query([{kinds: [MESSAGE]}])
|
||||||
lastChecked: number,
|
|
||||||
events: TrustedEvent[],
|
|
||||||
) => {
|
|
||||||
const [latestEvent] = sortBy($e => -$e.created_at, events)
|
|
||||||
|
|
||||||
return latestEvent?.pubkey !== pubkey && lt(lastChecked, latestEvent?.created_at)
|
for (const [url, rooms] of $userRoomsByUrl.entries()) {
|
||||||
}
|
const spacePath = makeSpacePath(url)
|
||||||
|
const threadPath = makeThreadPath(url)
|
||||||
|
const threadEvents = allThreadEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||||
|
|
||||||
export const deriveNotification = (path: string, filters: Filter[], url?: string) => {
|
if (hasNotification(threadPath, threadEvents[0])) {
|
||||||
const events = url ? deriveEventsForUrl(url, filters) : deriveEvents(repository, {filters})
|
paths.add(spacePath)
|
||||||
|
paths.add(threadPath)
|
||||||
|
}
|
||||||
|
|
||||||
return derived(
|
const commentsByThreadId = groupBy(
|
||||||
[pubkey, deriveChecked("*"), deriveChecked(path), events],
|
e => getTagValue("E", e.tags),
|
||||||
([$pubkey, $allChecked, $checked, $events]) => {
|
threadEvents.filter(spec({kind: COMMENT})),
|
||||||
return getNotification($pubkey, max([$allChecked, $checked]), $events)
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const spacesNotifications = derived(
|
for (const [threadId, [comment]] of commentsByThreadId.entries()) {
|
||||||
[pubkey, checked, userMembership, deriveEvents(repository, {filters: SPACE_FILTERS})],
|
const threadItemPath = makeThreadPath(url, threadId)
|
||||||
([$pubkey, $checked, $userMembership, $events]) => {
|
|
||||||
return getMembershipUrls($userMembership).filter(url => {
|
|
||||||
const path = makeSpacePath(url)
|
|
||||||
const lastChecked = max([$checked["*"], $checked[path]])
|
|
||||||
const [latestEvent] = sortBy($e => -$e.created_at, $events)
|
|
||||||
|
|
||||||
return latestEvent?.pubkey !== $pubkey && lt(lastChecked, latestEvent?.created_at)
|
if (hasNotification(threadItemPath, comment)) {
|
||||||
})
|
paths.add(threadItemPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const room of rooms) {
|
||||||
|
const roomPath = makeRoomPath(url, room)
|
||||||
|
const latestEvent = allMessageEvents.find(
|
||||||
|
e =>
|
||||||
|
$getUrlsForEvent(e.id).includes(url) && e.tags.find(t => t[0] === "h" && t[1] === room),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasNotification(roomPath, latestEvent)) {
|
||||||
|
paths.add(spacePath)
|
||||||
|
paths.add(roomPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export const inactiveSpacesNotifications = derived(
|
|
||||||
[page, spacesNotifications],
|
|
||||||
([$page, $spacesNotifications]) =>
|
|
||||||
$spacesNotifications.filter(url => !$page.url.pathname.startsWith(makeSpacePath(url))),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import {get} from "svelte/store"
|
||||||
|
import {partition, assoc, now} from "@welshman/lib"
|
||||||
|
import {MESSAGE, THREAD, COMMENT} from "@welshman/util"
|
||||||
|
import type {Subscription} from "@welshman/net"
|
||||||
|
import type {AppSyncOpts} from "@welshman/app"
|
||||||
|
import {subscribe, load, repository, pull, hasNegentropy} from "@welshman/app"
|
||||||
|
import {userRoomsByUrl, getUrlsForEvent} from "@app/state"
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
|
||||||
|
export const pullConservatively = ({relays, filters}: AppSyncOpts) => {
|
||||||
|
const $getUrlsForEvent = get(getUrlsForEvent)
|
||||||
|
const [smart, dumb] = partition(hasNegentropy, relays)
|
||||||
|
const promises = [pull({relays: smart, filters})]
|
||||||
|
const allEvents = repository.query(filters, {shouldSort: false})
|
||||||
|
|
||||||
|
// Since pulling from relays without negentropy is expensive, limit how many
|
||||||
|
// duplicates we repeatedly download
|
||||||
|
for (const url of dumb) {
|
||||||
|
const events = allEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||||
|
|
||||||
|
if (events.length > 100) {
|
||||||
|
filters = filters.map(assoc("since", events[10]!.created_at))
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.push(pull({relays: [url], filters}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application requests
|
||||||
|
|
||||||
|
export const listenForNotifications = () => {
|
||||||
|
const subs: Subscription[] = []
|
||||||
|
|
||||||
|
for (const [url, rooms] of userRoomsByUrl.get()) {
|
||||||
|
load({
|
||||||
|
relays: [url],
|
||||||
|
filters: [
|
||||||
|
{kinds: [THREAD], limit: 1},
|
||||||
|
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
|
||||||
|
...Array.from(rooms).map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
subs.push(
|
||||||
|
subscribe({
|
||||||
|
relays: [url],
|
||||||
|
filters: [
|
||||||
|
{kinds: [THREAD], since: now()},
|
||||||
|
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
|
||||||
|
...Array.from(rooms).map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const sub of subs) {
|
||||||
|
sub.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-4
@@ -1,5 +1,5 @@
|
|||||||
import type {Page} from "@sveltejs/kit"
|
import type {Page} from "@sveltejs/kit"
|
||||||
import {userMembership, makeChatId, decodeRelay, encodeRelay, getMembershipUrls} from "@app/state"
|
import {makeChatId, decodeRelay, encodeRelay, userRoomsByUrl} from "@app/state"
|
||||||
|
|
||||||
export const makeSpacePath = (url: string, ...extra: string[]) => {
|
export const makeSpacePath = (url: string, ...extra: string[]) => {
|
||||||
let path = `/spaces/${encodeRelay(url)}`
|
let path = `/spaces/${encodeRelay(url)}`
|
||||||
@@ -15,13 +15,20 @@ export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}
|
|||||||
|
|
||||||
export const makeRoomPath = (url: string, room: string) => `/spaces/${encodeRelay(url)}/${room}`
|
export const makeRoomPath = (url: string, room: string) => `/spaces/${encodeRelay(url)}/${room}`
|
||||||
|
|
||||||
export const makeThreadPath = (url: string, eventId: string) =>
|
export const makeThreadPath = (url: string, eventId?: string) => {
|
||||||
`/spaces/${encodeRelay(url)}/threads/${eventId}`
|
let path = `/spaces/${encodeRelay(url)}/threads`
|
||||||
|
|
||||||
|
if (eventId) {
|
||||||
|
path += "/" + eventId
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
export const getPrimaryNavItem = ($page: Page) => $page.route?.id?.split("/")[1]
|
export const getPrimaryNavItem = ($page: Page) => $page.route?.id?.split("/")[1]
|
||||||
|
|
||||||
export const getPrimaryNavItemIndex = ($page: Page) => {
|
export const getPrimaryNavItemIndex = ($page: Page) => {
|
||||||
const urls = getMembershipUrls(userMembership.get())
|
const urls = Array.from(userRoomsByUrl.get().keys())
|
||||||
|
|
||||||
switch (getPrimaryNavItem($page)) {
|
switch (getPrimaryNavItem($page)) {
|
||||||
case "discover":
|
case "discover":
|
||||||
|
|||||||
+275
-166
@@ -1,28 +1,36 @@
|
|||||||
import twColors from "tailwindcss/colors"
|
import twColors from "tailwindcss/colors"
|
||||||
import {get, derived} from "svelte/store"
|
import {get, derived} from "svelte/store"
|
||||||
import {nip19} from "nostr-tools"
|
import {nip19} from "nostr-tools"
|
||||||
import type {Maybe} from "@welshman/lib"
|
|
||||||
import {
|
import {
|
||||||
ctx,
|
ctx,
|
||||||
setContext,
|
setContext,
|
||||||
remove,
|
remove,
|
||||||
assoc,
|
|
||||||
sortBy,
|
sortBy,
|
||||||
sort,
|
sort,
|
||||||
uniq,
|
uniq,
|
||||||
partition,
|
|
||||||
nth,
|
nth,
|
||||||
pushToMapKey,
|
pushToMapKey,
|
||||||
nthEq,
|
nthEq,
|
||||||
shuffle,
|
shuffle,
|
||||||
parseJson,
|
parseJson,
|
||||||
|
fromPairs,
|
||||||
|
memoize,
|
||||||
|
addToMapKey,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
getIdFilters,
|
getIdFilters,
|
||||||
WRAP,
|
WRAP,
|
||||||
|
CLIENT_AUTH,
|
||||||
|
AUTH_JOIN,
|
||||||
REACTION,
|
REACTION,
|
||||||
ZAP_RESPONSE,
|
ZAP_RESPONSE,
|
||||||
DIRECT_MESSAGE,
|
DIRECT_MESSAGE,
|
||||||
|
GROUP_META,
|
||||||
|
MESSAGE,
|
||||||
|
GROUPS,
|
||||||
|
THREAD,
|
||||||
|
COMMENT,
|
||||||
|
getGroupTags,
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
getPubkeyTagValues,
|
getPubkeyTagValues,
|
||||||
isHashedEvent,
|
isHashedEvent,
|
||||||
@@ -30,11 +38,16 @@ import {
|
|||||||
readList,
|
readList,
|
||||||
getListTags,
|
getListTags,
|
||||||
asDecryptedEvent,
|
asDecryptedEvent,
|
||||||
isSignedEvent,
|
|
||||||
hasValidSignature,
|
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
|
import type {
|
||||||
|
TrustedEvent,
|
||||||
|
Repository,
|
||||||
|
SignedEvent,
|
||||||
|
PublishedList,
|
||||||
|
List,
|
||||||
|
Filter,
|
||||||
|
} from "@welshman/util"
|
||||||
import {Nip59} from "@welshman/signer"
|
import {Nip59} from "@welshman/signer"
|
||||||
import {
|
import {
|
||||||
pubkey,
|
pubkey,
|
||||||
@@ -46,30 +59,30 @@ import {
|
|||||||
getDefaultNetContext,
|
getDefaultNetContext,
|
||||||
makeRouter,
|
makeRouter,
|
||||||
tracker,
|
tracker,
|
||||||
|
makeTrackerStore,
|
||||||
|
makeRepositoryStore,
|
||||||
relay,
|
relay,
|
||||||
getSession,
|
getSession,
|
||||||
getSigner,
|
getSigner,
|
||||||
hasNegentropy,
|
|
||||||
pull,
|
|
||||||
createSearch,
|
createSearch,
|
||||||
userFollows,
|
userFollows,
|
||||||
ensurePlaintext,
|
ensurePlaintext,
|
||||||
|
thunks,
|
||||||
|
walkThunks,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {AppSyncOpts} from "@welshman/app"
|
import type {Thunk, Relay} from "@welshman/app"
|
||||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||||
import {deriveEvents, deriveEventsMapped, withGetter} from "@welshman/store"
|
import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store"
|
||||||
|
|
||||||
export const ROOM = "~"
|
export const ROOM = "h"
|
||||||
|
|
||||||
export const GENERAL = "general"
|
export const GENERAL = "_"
|
||||||
|
|
||||||
export const MESSAGE = 209
|
export const PROTECTED = ["-"]
|
||||||
|
|
||||||
export const THREAD = 309
|
export const LEGACY_MESSAGE = 209
|
||||||
|
|
||||||
export const COMMENT = 1111
|
export const LEGACY_THREAD = 309
|
||||||
|
|
||||||
export const MEMBERSHIPS = 10209
|
|
||||||
|
|
||||||
export const INDEXER_RELAYS = [
|
export const INDEXER_RELAYS = [
|
||||||
"wss://purplepag.es/",
|
"wss://purplepag.es/",
|
||||||
@@ -81,6 +94,10 @@ export const SIGNER_RELAYS = ["wss://relay.nsec.app/", "wss://bucket.coracle.soc
|
|||||||
|
|
||||||
export const PLATFORM_URL = window.location.origin
|
export const PLATFORM_URL = window.location.origin
|
||||||
|
|
||||||
|
export const PLATFORM_TERMS = import.meta.env.VITE_PLATFORM_TERMS
|
||||||
|
|
||||||
|
export const PLATFORM_PRIVACY = import.meta.env.VITE_PLATFORM_PRIVACY
|
||||||
|
|
||||||
export const PLATFORM_LOGO = PLATFORM_URL + "/pwa-192x192.png"
|
export const PLATFORM_LOGO = PLATFORM_URL + "/pwa-192x192.png"
|
||||||
|
|
||||||
export const PLATFORM_NAME = import.meta.env.VITE_PLATFORM_NAME
|
export const PLATFORM_NAME = import.meta.env.VITE_PLATFORM_NAME
|
||||||
@@ -91,6 +108,8 @@ export const PLATFORM_ACCENT = import.meta.env.VITE_PLATFORM_ACCENT
|
|||||||
|
|
||||||
export const PLATFORM_DESCRIPTION = import.meta.env.VITE_PLATFORM_DESCRIPTION
|
export const PLATFORM_DESCRIPTION = import.meta.env.VITE_PLATFORM_DESCRIPTION
|
||||||
|
|
||||||
|
export const BURROW_URL = import.meta.env.VITE_BURROW_URL
|
||||||
|
|
||||||
export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
|
export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
|
||||||
|
|
||||||
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
|
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
|
||||||
@@ -99,6 +118,19 @@ export const IMGPROXY_URL = "https://imgproxy.coracle.social"
|
|||||||
|
|
||||||
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
|
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
|
||||||
|
|
||||||
|
export const THREAD_FILTER: Filter = {kinds: [THREAD, LEGACY_THREAD]}
|
||||||
|
|
||||||
|
export const COMMENT_FILTER: Filter = {
|
||||||
|
kinds: [COMMENT],
|
||||||
|
"#K": [String(THREAD), String(LEGACY_THREAD)],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NIP46_PERMS =
|
||||||
|
"nip04_encrypt,nip04_decrypt,nip44_encrypt,nip44_decrypt," +
|
||||||
|
[CLIENT_AUTH, AUTH_JOIN, MESSAGE, THREAD, COMMENT, GROUPS, WRAP, REACTION]
|
||||||
|
.map(k => `sign_event:${k}`)
|
||||||
|
.join(",")
|
||||||
|
|
||||||
export const colors = [
|
export const colors = [
|
||||||
["amber", twColors.amber[600]],
|
["amber", twColors.amber[600]],
|
||||||
["blue", twColors.blue[600]],
|
["blue", twColors.blue[600]],
|
||||||
@@ -144,7 +176,7 @@ export const pubkeyLink = (
|
|||||||
relays = ctx.app.router.FromPubkeys([pubkey]).getUrls(),
|
relays = ctx.app.router.FromPubkeys([pubkey]).getUrls(),
|
||||||
) => entityLink(nip19.nprofileEncode({pubkey, relays}))
|
) => entityLink(nip19.nprofileEncode({pubkey, relays}))
|
||||||
|
|
||||||
export const tagRoom = (room: string, url: string) => [ROOM, room, url]
|
export const tagRoom = (room: string, url: string) => [ROOM, room]
|
||||||
|
|
||||||
export const getDefaultPubkeys = () => {
|
export const getDefaultPubkeys = () => {
|
||||||
const appPubkeys = DEFAULT_PUBKEYS.split(",")
|
const appPubkeys = DEFAULT_PUBKEYS.split(",")
|
||||||
@@ -153,6 +185,8 @@ export const getDefaultPubkeys = () => {
|
|||||||
return userPubkeys.length > 5 ? userPubkeys : [...userPubkeys, ...appPubkeys]
|
return userPubkeys.length > 5 ? userPubkeys : [...userPubkeys, ...appPubkeys]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const failedUnwraps = new Set()
|
||||||
|
|
||||||
export const ensureUnwrapped = async (event: TrustedEvent) => {
|
export const ensureUnwrapped = async (event: TrustedEvent) => {
|
||||||
if (event.kind !== WRAP) {
|
if (event.kind !== WRAP) {
|
||||||
return event
|
return event
|
||||||
@@ -160,7 +194,7 @@ export const ensureUnwrapped = async (event: TrustedEvent) => {
|
|||||||
|
|
||||||
let rumor = repository.eventsByWrap.get(event.id)
|
let rumor = repository.eventsByWrap.get(event.id)
|
||||||
|
|
||||||
if (rumor) {
|
if (rumor || failedUnwraps.has(event.id)) {
|
||||||
return rumor
|
return rumor
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,53 +218,16 @@ export const ensureUnwrapped = async (event: TrustedEvent) => {
|
|||||||
|
|
||||||
// Send the rumor via our relay so listeners get updated
|
// Send the rumor via our relay so listeners get updated
|
||||||
relay.send("EVENT", rumor)
|
relay.send("EVENT", rumor)
|
||||||
|
} else {
|
||||||
|
failedUnwraps.add(event.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rumor
|
return rumor
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pullConservatively = ({relays, filters}: AppSyncOpts) => {
|
export const trackerStore = makeTrackerStore()
|
||||||
const [smart, dumb] = partition(hasNegentropy, relays)
|
|
||||||
const promises = [pull({relays: smart, filters})]
|
|
||||||
|
|
||||||
// Since pulling from relays without negentropy is expensive, limit how many
|
export const repositoryStore = makeRepositoryStore()
|
||||||
// duplicates we repeatedly download
|
|
||||||
if (dumb.length > 0) {
|
|
||||||
const events = sortBy(e => -e.created_at, repository.query(filters))
|
|
||||||
|
|
||||||
if (events.length > 100) {
|
|
||||||
filters = filters.map(assoc("since", events[100]!.created_at))
|
|
||||||
}
|
|
||||||
|
|
||||||
promises.push(pull({relays: dumb, filters}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(promises)
|
|
||||||
}
|
|
||||||
|
|
||||||
setContext({
|
|
||||||
net: getDefaultNetContext({
|
|
||||||
isValid: (url: string, event: TrustedEvent) => {
|
|
||||||
if (!isSignedEvent(event) || !hasValidSignature(event)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomTags = event.tags.filter(nthEq(0, "~"))
|
|
||||||
|
|
||||||
if (roomTags.length > 0 && !roomTags.some(nthEq(2, url))) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
app: getDefaultAppContext({
|
|
||||||
dufflepudUrl: DUFFLEPUD_URL,
|
|
||||||
indexerRelays: INDEXER_RELAYS,
|
|
||||||
requestTimeout: 5000,
|
|
||||||
router: makeRouter(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
||||||
let attempted = false
|
let attempted = false
|
||||||
@@ -251,25 +248,64 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const eventIsForUrl = (url: string, event: TrustedEvent) =>
|
export const getUrlsForEvent = derived([trackerStore, thunks], ([$tracker, $thunks]) => {
|
||||||
event.tags.find(nthEq(0, "~"))?.[2] === url
|
const getThunksByEventId = memoize(() => {
|
||||||
|
const thunksByEventId = new Map<string, Thunk[]>()
|
||||||
|
|
||||||
export const getEventsForUrl = (url: string, filters: Filter[]) =>
|
for (const thunk of walkThunks(Object.values($thunks))) {
|
||||||
sortBy(
|
pushToMapKey(thunksByEventId, thunk.event.id, thunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
return thunksByEventId
|
||||||
|
})
|
||||||
|
|
||||||
|
return (id: string) => {
|
||||||
|
const urls = Array.from($tracker.getRelays(id))
|
||||||
|
|
||||||
|
for (const thunk of getThunksByEventId().get(id) || []) {
|
||||||
|
for (const url of thunk.request.relays) {
|
||||||
|
urls.push(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniq(urls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getEventsForUrl = (repository: Repository, url: string, filters: Filter[]) => {
|
||||||
|
const $getUrlsForEvent = get(getUrlsForEvent)
|
||||||
|
const $events = repository.query(filters)
|
||||||
|
|
||||||
|
return sortBy(
|
||||||
e => -e.created_at,
|
e => -e.created_at,
|
||||||
repository.query(filters).filter(e => eventIsForUrl(url, e)),
|
$events.filter(e => $getUrlsForEvent(e.id).includes(url)),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
|
export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
|
||||||
derived(deriveEvents(repository, {filters}), $events =>
|
derived([deriveEvents(repository, {filters}), getUrlsForEvent], ([$events, $getUrlsForEvent]) =>
|
||||||
sortBy(
|
sortBy(
|
||||||
e => -e.created_at,
|
e => -e.created_at,
|
||||||
$events.filter(e => eventIsForUrl(url, e)),
|
$events.filter(e => $getUrlsForEvent(e.id).includes(url)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Context
|
||||||
|
|
||||||
|
setContext({
|
||||||
|
net: getDefaultNetContext(),
|
||||||
|
app: getDefaultAppContext({
|
||||||
|
dufflepudUrl: DUFFLEPUD_URL,
|
||||||
|
indexerRelays: INDEXER_RELAYS,
|
||||||
|
requestTimeout: 5000,
|
||||||
|
router: makeRouter(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
|
|
||||||
|
export const canDecrypt = synced("canDecrypt", false)
|
||||||
|
|
||||||
export const SETTINGS = 38489
|
export const SETTINGS = 38489
|
||||||
|
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
@@ -319,27 +355,29 @@ export const {
|
|||||||
export const hasMembershipUrl = (list: List | undefined, url: string) =>
|
export const hasMembershipUrl = (list: List | undefined, url: string) =>
|
||||||
getListTags(list).some(t => {
|
getListTags(list).some(t => {
|
||||||
if (t[0] === "r") return t[1] === url
|
if (t[0] === "r") return t[1] === url
|
||||||
if (t[0] === "~") return t[2] === url
|
if (t[0] === "group") return t[2] === url
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getMembershipUrls = (list?: List) => sort(getRelayTagValues(getListTags(list)))
|
export const getMembershipUrls = (list?: List) => {
|
||||||
|
const tags = getListTags(list)
|
||||||
|
|
||||||
|
return sort(uniq([...getRelayTagValues(tags), ...getGroupTags(tags).map(nth(2))]))
|
||||||
|
}
|
||||||
|
|
||||||
export const getMembershipRooms = (list?: List) =>
|
export const getMembershipRooms = (list?: List) =>
|
||||||
getListTags(list)
|
getGroupTags(getListTags(list)).map(([_, room, url, name = ""]) => ({url, room, name}))
|
||||||
.filter(t => t[0] === "~")
|
|
||||||
.map(t => ({url: t[2], room: t[1]}))
|
|
||||||
|
|
||||||
export const getMembershipRoomsByUrl = (url: string, list?: List) =>
|
export const getMembershipRoomsByUrl = (url: string, list?: List) =>
|
||||||
sort(
|
sort(
|
||||||
getListTags(list)
|
getGroupTags(getListTags(list))
|
||||||
.filter(t => t[0] === "~" && t[2] === url)
|
.filter(t => t[2] === url)
|
||||||
.map(nth(1)),
|
.map(nth(1)),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const memberships = deriveEventsMapped<PublishedList>(repository, {
|
export const memberships = deriveEventsMapped<PublishedList>(repository, {
|
||||||
filters: [{kinds: [MEMBERSHIPS]}],
|
filters: [{kinds: [GROUPS]}],
|
||||||
itemToEvent: item => item.event,
|
itemToEvent: item => item.event,
|
||||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||||
})
|
})
|
||||||
@@ -353,89 +391,7 @@ export const {
|
|||||||
store: memberships,
|
store: memberships,
|
||||||
getKey: list => list.event.pubkey,
|
getKey: list => list.event.pubkey,
|
||||||
load: (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) =>
|
load: (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) =>
|
||||||
load({
|
load({...request, filters: [{kinds: [GROUPS], authors: [pubkey]}]}),
|
||||||
...request,
|
|
||||||
filters: [{kinds: [MEMBERSHIPS], authors: [pubkey]}],
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Messages
|
|
||||||
|
|
||||||
export type ChannelMessage = {
|
|
||||||
url: string
|
|
||||||
room: string
|
|
||||||
event: TrustedEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
export const readMessage = (event: TrustedEvent): Maybe<ChannelMessage> => {
|
|
||||||
const roomTags = event.tags.filter(nthEq(0, ROOM))
|
|
||||||
|
|
||||||
if (roomTags.length !== 1) return undefined
|
|
||||||
|
|
||||||
const [_, room, url] = roomTags[0]
|
|
||||||
|
|
||||||
if (!url || !room) return undefined
|
|
||||||
|
|
||||||
return {url: normalizeRelayUrl(url), room, event}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const channelMessages = deriveEventsMapped<ChannelMessage>(repository, {
|
|
||||||
filters: [{kinds: [MESSAGE, COMMENT]}],
|
|
||||||
eventToItem: readMessage,
|
|
||||||
itemToEvent: item => item.event,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Channels
|
|
||||||
|
|
||||||
export type Channel = {
|
|
||||||
id: string
|
|
||||||
url: string
|
|
||||||
room: string
|
|
||||||
messages: ChannelMessage[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeChannelId = (url: string, room: string) => `${url}|${room}`
|
|
||||||
|
|
||||||
export const splitChannelId = (id: string) => id.split("|")
|
|
||||||
|
|
||||||
export const channels = derived(
|
|
||||||
[memberships, channelMessages],
|
|
||||||
([$memberships, $channelMessages]) => {
|
|
||||||
const messagesByChannelId = new Map<string, ChannelMessage[]>()
|
|
||||||
|
|
||||||
// Add known rooms by membership so we don't have to scan messages to load all rooms
|
|
||||||
for (const membership of $memberships) {
|
|
||||||
for (const {url, room} of getMembershipRooms(membership)) {
|
|
||||||
messagesByChannelId.set(makeChannelId(url, room), [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add messages/rooms without memberships
|
|
||||||
for (const message of $channelMessages) {
|
|
||||||
pushToMapKey(messagesByChannelId, makeChannelId(message.url, message.room), message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(messagesByChannelId.entries()).map(([id, messages]) => {
|
|
||||||
const [url, room] = splitChannelId(id)
|
|
||||||
|
|
||||||
return {id, url, room, messages}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const {
|
|
||||||
indexStore: channelsById,
|
|
||||||
deriveItem: deriveChannel,
|
|
||||||
loadItem: loadChannel,
|
|
||||||
} = collection({
|
|
||||||
name: "channels",
|
|
||||||
store: channels,
|
|
||||||
getKey: channel => channel.id,
|
|
||||||
load: (id: string, request: Partial<SubscribeRequestWithHandlers> = {}) => {
|
|
||||||
const [url, room] = splitChannelId(id)
|
|
||||||
|
|
||||||
return load({...request, relays: [url], filters: [{"#~": [room]}]})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Chats
|
// Chats
|
||||||
@@ -502,20 +458,143 @@ export const chatSearch = derived(chats, $chats =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rooms
|
// Messages
|
||||||
|
|
||||||
export const roomsByUrl = derived(channels, $channels => {
|
// TODO: remove support for legacy messages
|
||||||
const $roomsByUrl = new Map<string, string[]>()
|
export const adaptLegacyMessage = (event: TrustedEvent) => {
|
||||||
|
if (event.kind === LEGACY_MESSAGE) {
|
||||||
|
let room = event.tags.find(nthEq(0, "~"))?.[1] || GENERAL
|
||||||
|
|
||||||
for (const channel of $channels) {
|
if (room === "general") {
|
||||||
if (channel.room) {
|
room = GENERAL
|
||||||
pushToMapKey($roomsByUrl, channel.url, channel.room)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {...event, kind: MESSAGE, tags: [...event.tags, tagRoom(room, "")]}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $roomsByUrl
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
export const messages = derived(
|
||||||
|
deriveEvents(repository, {filters: [{kinds: [MESSAGE, LEGACY_MESSAGE]}]}),
|
||||||
|
$events => $events.map(adaptLegacyMessage),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Nip29
|
||||||
|
|
||||||
|
export const groupMeta = deriveEvents(repository, {filters: [{kinds: [GROUP_META]}]})
|
||||||
|
|
||||||
|
export const hasNip29 = (relay?: Relay) =>
|
||||||
|
relay?.profile?.supported_nips?.map(String)?.includes("29")
|
||||||
|
|
||||||
|
// Channels
|
||||||
|
|
||||||
|
export type ChannelMeta = {
|
||||||
|
access: "public" | "private"
|
||||||
|
membership: "open" | "closed"
|
||||||
|
picture?: string
|
||||||
|
about?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Channel = {
|
||||||
|
url: string
|
||||||
|
room: string
|
||||||
|
name: string
|
||||||
|
meta?: ChannelMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeChannelId = (url: string, room: string) => `${url}|${room}`
|
||||||
|
|
||||||
|
export const splitChannelId = (id: string) => id.split("|")
|
||||||
|
|
||||||
|
export const channelsById = withGetter(
|
||||||
|
derived(
|
||||||
|
[groupMeta, memberships, messages, getUrlsForEvent],
|
||||||
|
([$groupMeta, $memberships, $messages, $getUrlsForEvent]) => {
|
||||||
|
const channelsById = new Map<string, Channel>()
|
||||||
|
|
||||||
|
// Add meta using group meta events
|
||||||
|
for (const event of $groupMeta) {
|
||||||
|
const meta = fromPairs(event.tags)
|
||||||
|
const room = meta.d
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
for (const url of $getUrlsForEvent(event.id)) {
|
||||||
|
const id = makeChannelId(url, room)
|
||||||
|
|
||||||
|
channelsById.set(id, {
|
||||||
|
url,
|
||||||
|
room,
|
||||||
|
name: meta.name || room,
|
||||||
|
meta: {
|
||||||
|
access: meta.private ? "private" : "public",
|
||||||
|
membership: meta.closed ? "closed" : "open",
|
||||||
|
picture: meta.picture,
|
||||||
|
about: meta.about,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add known rooms based on membership events
|
||||||
|
for (const membership of $memberships) {
|
||||||
|
for (const {url, room, name} of getMembershipRooms(membership)) {
|
||||||
|
const id = makeChannelId(url, room)
|
||||||
|
|
||||||
|
if (!channelsById.has(id)) {
|
||||||
|
channelsById.set(id, {url, room, name})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add rooms based on known messages
|
||||||
|
for (const event of $messages) {
|
||||||
|
const [_, room] = event.tags.find(nthEq(0, ROOM)) || []
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
for (const url of $getUrlsForEvent(event.id)) {
|
||||||
|
const id = makeChannelId(url, room)
|
||||||
|
|
||||||
|
if (!channelsById.has(id)) {
|
||||||
|
channelsById.set(id, {url, room, name: room})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelsById
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const deriveChannel = (url: string, room: string) =>
|
||||||
|
derived(channelsById, $channelsById => $channelsById.get(makeChannelId(url, room)))
|
||||||
|
|
||||||
|
export const channelsByUrl = derived(channelsById, $channelsById => {
|
||||||
|
const $channelsByUrl = new Map<string, Channel[]>()
|
||||||
|
|
||||||
|
for (const channel of $channelsById.values()) {
|
||||||
|
pushToMapKey($channelsByUrl, channel.url, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $channelsByUrl
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const displayChannel = (url: string, room: string) => {
|
||||||
|
if (room === GENERAL) {
|
||||||
|
return "general"
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelsById.get().get(makeChannelId(url, room))?.name || room
|
||||||
|
}
|
||||||
|
|
||||||
|
export const roomComparator = (url: string) => (room: string) =>
|
||||||
|
displayChannel(url, room).toLowerCase()
|
||||||
|
|
||||||
|
export const channelIsLocked = (channel?: Channel) =>
|
||||||
|
channel?.meta?.access === "private" && channel?.meta?.membership === "closed"
|
||||||
|
|
||||||
// User stuff
|
// User stuff
|
||||||
|
|
||||||
export const userSettings = withGetter(
|
export const userSettings = withGetter(
|
||||||
@@ -532,7 +611,7 @@ export const userSettingValues = withGetter(
|
|||||||
derived(userSettings, $s => $s?.values || defaultSettings),
|
derived(userSettings, $s => $s?.values || defaultSettings),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const getSetting = (key: keyof Settings["values"]) => userSettingValues.get()[key]
|
export const getSetting = <T>(key: keyof Settings["values"]) => userSettingValues.get()[key] as T
|
||||||
|
|
||||||
export const userMembership = withGetter(
|
export const userMembership = withGetter(
|
||||||
derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => {
|
derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => {
|
||||||
@@ -544,6 +623,36 @@ export const userMembership = withGetter(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const userRoomsByUrl = withGetter(
|
||||||
|
derived(userMembership, $userMembership => {
|
||||||
|
const tags = getListTags($userMembership)
|
||||||
|
const $userRoomsByUrl = new Map<string, Set<string>>()
|
||||||
|
|
||||||
|
for (const [_, room, url] of getGroupTags(tags)) {
|
||||||
|
addToMapKey($userRoomsByUrl, url, room)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const url of getRelayTagValues(tags)) {
|
||||||
|
addToMapKey($userRoomsByUrl, url, GENERAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $userRoomsByUrl
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const deriveUserRooms = (url: string) =>
|
||||||
|
derived(userRoomsByUrl, $userRoomsByUrl =>
|
||||||
|
sortBy(roomComparator(url), uniq(Array.from($userRoomsByUrl.get(url) || [GENERAL]))),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const deriveOtherRooms = (url: string) =>
|
||||||
|
derived([deriveUserRooms(url), channelsByUrl], ([$userRooms, $channelsByUrl]) =>
|
||||||
|
sortBy(
|
||||||
|
roomComparator(url),
|
||||||
|
($channelsByUrl.get(url) || []).filter(c => !$userRooms.includes(c.room)).map(c => c.room),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// Other utils
|
// Other utils
|
||||||
|
|
||||||
export const encodeRelay = (url: string) => encodeURIComponent(normalizeRelayUrl(url))
|
export const encodeRelay = (url: string) => encodeURIComponent(normalizeRelayUrl(url))
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 16C2 13.1716 2 11.7574 2.87868 10.8787C3.75736 10 5.17157 10 8 10H16C18.8284 10 20.2426 10 21.1213 10.8787C22 11.7574 22 13.1716 22 16C22 18.8284 22 20.2426 21.1213 21.1213C20.2426 22 18.8284 22 16 22H8C5.17157 22 3.75736 22 2.87868 21.1213C2 20.2426 2 18.8284 2 16Z" stroke="#1C274C" stroke-width="1.5"/>
|
||||||
|
<path d="M6 10V8C6 4.68629 8.68629 2 12 2C15.3137 2 18 4.68629 18 8V10" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 554 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16 18L16 6M16 6L20 10.125M16 6L12 10.125" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M8 6L8 18M8 18L12 13.875M8 18L4 13.875" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 374 B |
@@ -1,20 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import {onMount} from "svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
|
||||||
export let src = ""
|
export let src = ""
|
||||||
export let size = 7
|
export let size = 7
|
||||||
export let icon = "user-rounded"
|
export let icon = "user-rounded"
|
||||||
|
|
||||||
|
let element: HTMLElement
|
||||||
|
|
||||||
|
$: rem = size * 4
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (src) {
|
||||||
|
const image = new Image()
|
||||||
|
|
||||||
|
image.addEventListener("error", () => {
|
||||||
|
element.querySelector(".hidden")?.classList.remove("hidden")
|
||||||
|
})
|
||||||
|
|
||||||
|
image.src = src
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if src}
|
<div
|
||||||
<div
|
bind:this={element}
|
||||||
class={cx($$props.class, "shrink-0 overflow-hidden rounded-full bg-cover bg-center")}
|
class="{$$props.class} relative !flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-cover bg-center"
|
||||||
style={`width: ${size * 4}px; height: ${size * 4}px; min-width: ${size * 4}px; background-image: url(${src}); ${$$props.style || ""}`} />
|
style="width: {rem}px; height: {rem}px; min-width: {rem}px; background-image: url({src}); {$$props.style ||
|
||||||
{:else}
|
''}">
|
||||||
<div
|
<Icon {icon} class={src ? "hidden" : ""} size={Math.round(size * 0.8)} />
|
||||||
class={cx($$props.class, "center !flex rounded-full")}
|
</div>
|
||||||
style={`width: ${size * 4}px; height: ${size * 4}px; min-width: ${size * 4}px; ${$$props.style || ""}`}>
|
|
||||||
<Icon {icon} size={Math.round(size * 0.8)} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {throttle} from "throttle-debounce"
|
|
||||||
import {type Instance} from "tippy.js"
|
import {type Instance} from "tippy.js"
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
import {between} from "@welshman/lib"
|
import {between, throttle} from "@welshman/lib"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-2 {$$props.class}">
|
<div class="grid grid-cols-1 gap-2 md:grid-cols-3 {$$props.class}">
|
||||||
<label class="flex items-center gap-2 font-bold">
|
<label class="flex items-center gap-2 font-bold">
|
||||||
<slot name="label" />
|
<slot name="label" />
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center gap-2">
|
<div class="col-span-2 flex items-center gap-2">
|
||||||
<slot name="input" />
|
<slot name="input" />
|
||||||
</div>
|
</div>
|
||||||
{#if $$slots.info}
|
<p class="flex-end text-sm md:col-span-3">
|
||||||
<p class="flex-end text-sm sm:col-span-2">
|
{#if $$slots.info}
|
||||||
<slot name="info" />
|
<slot name="info" />
|
||||||
</p>
|
{/if}
|
||||||
{/if}
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
import KeyMinimalisticSquare3 from "@assets/icons/Key Minimalistic Square 3.svg?dataurl"
|
import KeyMinimalisticSquare3 from "@assets/icons/Key Minimalistic Square 3.svg?dataurl"
|
||||||
import Letter from "@assets/icons/Letter.svg?dataurl"
|
import Letter from "@assets/icons/Letter.svg?dataurl"
|
||||||
import LinkRound from "@assets/icons/Link Round.svg?dataurl"
|
import LinkRound from "@assets/icons/Link Round.svg?dataurl"
|
||||||
|
import Lock from "@assets/icons/Lock.svg?dataurl"
|
||||||
import Login from "@assets/icons/Login.svg?dataurl"
|
import Login from "@assets/icons/Login.svg?dataurl"
|
||||||
import Login2 from "@assets/icons/Login 2.svg?dataurl"
|
import Login2 from "@assets/icons/Login 2.svg?dataurl"
|
||||||
import Magnifer from "@assets/icons/Magnifer.svg?dataurl"
|
import Magnifer from "@assets/icons/Magnifer.svg?dataurl"
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
import ShopMinimalistic from "@assets/icons/Shop Minimalistic.svg?dataurl"
|
import ShopMinimalistic from "@assets/icons/Shop Minimalistic.svg?dataurl"
|
||||||
import SmileCircle from "@assets/icons/Smile Circle.svg?dataurl"
|
import SmileCircle from "@assets/icons/Smile Circle.svg?dataurl"
|
||||||
import SquareShareLine from "@assets/icons/Square Share Line.svg?dataurl"
|
import SquareShareLine from "@assets/icons/Square Share Line.svg?dataurl"
|
||||||
|
import SortVertical from "@assets/icons/Sort Vertical.svg?dataurl"
|
||||||
import TrashBin2 from "@assets/icons/Trash Bin 2.svg?dataurl"
|
import TrashBin2 from "@assets/icons/Trash Bin 2.svg?dataurl"
|
||||||
import UFO3 from "@assets/icons/UFO 3.svg?dataurl"
|
import UFO3 from "@assets/icons/UFO 3.svg?dataurl"
|
||||||
import UserHeart from "@assets/icons/User Heart.svg?dataurl"
|
import UserHeart from "@assets/icons/User Heart.svg?dataurl"
|
||||||
@@ -132,6 +134,7 @@
|
|||||||
letter: Letter,
|
letter: Letter,
|
||||||
"link-round": LinkRound,
|
"link-round": LinkRound,
|
||||||
login: Login,
|
login: Login,
|
||||||
|
lock: Lock,
|
||||||
"login-2": Login2,
|
"login-2": Login2,
|
||||||
magnifer: Magnifer,
|
magnifer: Magnifer,
|
||||||
mailbox: Mailbox,
|
mailbox: Mailbox,
|
||||||
@@ -155,6 +158,7 @@
|
|||||||
"trash-bin-2": TrashBin2,
|
"trash-bin-2": TrashBin2,
|
||||||
"ufo-3": UFO3,
|
"ufo-3": UFO3,
|
||||||
"square-share-line": SquareShareLine,
|
"square-share-line": SquareShareLine,
|
||||||
|
"sort-vertical": SortVertical,
|
||||||
"user-heart": UserHeart,
|
"user-heart": UserHeart,
|
||||||
"user-circle": UserCircle,
|
"user-circle": UserCircle,
|
||||||
"user-rounded": UserRounded,
|
"user-rounded": UserRounded,
|
||||||
|
|||||||
@@ -4,10 +4,9 @@
|
|||||||
import {type Instance} from "tippy.js"
|
import {type Instance} from "tippy.js"
|
||||||
import {identity} from "@welshman/lib"
|
import {identity} from "@welshman/lib"
|
||||||
import {createSearch} from "@welshman/app"
|
import {createSearch} from "@welshman/app"
|
||||||
|
import {Suggestions, SuggestionString} from "@welshman/editor"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
import Suggestions from "@lib/editor/Suggestions.svelte"
|
|
||||||
import SuggestionString from "@lib/editor/SuggestionString.svelte"
|
|
||||||
|
|
||||||
export let value: string
|
export let value: string
|
||||||
export let options: string[]
|
export let options: string[]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<div class="flex flex-col gap-1 px-2 py-4">
|
<div class="flex flex-col gap-1 px-2 py-4 {$$props.class}">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {slide, fade} from "svelte/transition"
|
import {slide, fade} from "svelte/transition"
|
||||||
|
|
||||||
export let loading
|
export let loading = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="flex items-center">
|
<span class="flex min-h-10 items-center">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<span class="pr-3" transition:slide|local={{axis: "x"}}>
|
<span class="pr-3" transition:slide|local={{axis: "x"}}>
|
||||||
<span class="loading loading-spinner" transition:fade|local={{duration: 100}} />
|
<span class="loading loading-spinner" transition:fade|local={{duration: 100}} />
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import cx from "classnames"
|
|
||||||
import type {NodeViewProps} from "@tiptap/core"
|
|
||||||
import {NodeViewWrapper} from "svelte-tiptap"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import {clip} from "@app/toast"
|
|
||||||
|
|
||||||
export let node: NodeViewProps["node"]
|
|
||||||
export let selected: NodeViewProps["selected"]
|
|
||||||
|
|
||||||
const copy = () => clip(node.attrs.lnbc)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NodeViewWrapper class="inline">
|
|
||||||
<Button on:click={copy} class={cx("link-content", {"link-content-selected": selected})}>
|
|
||||||
<Icon icon="bolt" size={3} class="inline-block translate-y-px" />
|
|
||||||
{node.attrs.lnbc.slice(0, 16)}...
|
|
||||||
</Button>
|
|
||||||
</NodeViewWrapper>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import cx from "classnames"
|
|
||||||
import type {NodeViewProps} from "@tiptap/core"
|
|
||||||
import {NodeViewWrapper} from "svelte-tiptap"
|
|
||||||
import {ellipsize, nthEq} from "@welshman/lib"
|
|
||||||
import {type TrustedEvent, fromNostrURI, Address} from "@welshman/util"
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import {deriveEvent, entityLink} from "@app/state"
|
|
||||||
|
|
||||||
export let node: NodeViewProps["node"]
|
|
||||||
export let selected: NodeViewProps["selected"]
|
|
||||||
|
|
||||||
const displayEvent = (e: TrustedEvent) => {
|
|
||||||
const content = e?.tags.find(nthEq(0, "alt"))?.[1] || e?.content
|
|
||||||
|
|
||||||
return content.length > 1
|
|
||||||
? ellipsize(content, 30)
|
|
||||||
: fromNostrURI(nevent || naddr).slice(0, 16) + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
$: ({identifier, pubkey, kind, id, relays = [], nevent, naddr} = node.attrs)
|
|
||||||
$: event = deriveEvent(id || new Address(kind, pubkey, identifier).toString(), relays)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NodeViewWrapper class="inline">
|
|
||||||
<Link
|
|
||||||
external
|
|
||||||
href={entityLink(node.attrs.nevent)}
|
|
||||||
class={cx("link-content", {"link-content-selected": selected})}>
|
|
||||||
{displayEvent($event)}
|
|
||||||
</Link>
|
|
||||||
</NodeViewWrapper>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import cx from "classnames"
|
|
||||||
import type {NodeViewProps} from "@tiptap/core"
|
|
||||||
import {NodeViewWrapper} from "svelte-tiptap"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
|
|
||||||
export let node: NodeViewProps["node"]
|
|
||||||
export let selected: NodeViewProps["selected"]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NodeViewWrapper class={cx("link-content inline", {"link-content-selected": selected})}>
|
|
||||||
{#if node.attrs.uploading}
|
|
||||||
<span class="loading loading-spinner loading-xs translate-y-[2px] scale-75" />
|
|
||||||
{:else}
|
|
||||||
<Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
|
|
||||||
{/if}
|
|
||||||
{node.attrs.file.name}
|
|
||||||
</NodeViewWrapper>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import cx from "classnames"
|
|
||||||
import type {NodeViewProps} from "@tiptap/core"
|
|
||||||
import {NodeViewWrapper} from "svelte-tiptap"
|
|
||||||
import {displayUrl} from "@welshman/lib"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
|
|
||||||
export let node: NodeViewProps["node"]
|
|
||||||
export let selected: NodeViewProps["selected"]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NodeViewWrapper class="inline-block">
|
|
||||||
<Link
|
|
||||||
external
|
|
||||||
href={node.attrs.url}
|
|
||||||
class={cx("link-content", {"link-content-selected": selected})}>
|
|
||||||
<Icon icon="link-round" size={3} class="inline-block" />
|
|
||||||
{displayUrl(node.attrs.url)}
|
|
||||||
</Link>
|
|
||||||
</NodeViewWrapper>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import cx from "classnames"
|
|
||||||
import type {NodeViewProps} from "@tiptap/core"
|
|
||||||
import {NodeViewWrapper} from "svelte-tiptap"
|
|
||||||
import {displayProfile} from "@welshman/util"
|
|
||||||
import {deriveProfile} from "@welshman/app"
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import {pubkeyLink} from "@app/state"
|
|
||||||
|
|
||||||
export let node: NodeViewProps["node"]
|
|
||||||
export let selected: NodeViewProps["selected"]
|
|
||||||
|
|
||||||
$: profile = deriveProfile(node.attrs.pubkey, node.attrs.relays)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NodeViewWrapper class="inline">
|
|
||||||
<Link
|
|
||||||
external
|
|
||||||
href={pubkeyLink(node.attrs.pubkey, node.attrs.relays)}
|
|
||||||
class={cx("link-content", {"link-content-selected": selected})}>
|
|
||||||
@{displayProfile($profile)}
|
|
||||||
</Link>
|
|
||||||
</NodeViewWrapper>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type {NodeViewProps} from "@tiptap/core"
|
|
||||||
import {NodeViewWrapper} from "svelte-tiptap"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
|
|
||||||
export let node: NodeViewProps["node"]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NodeViewWrapper class="link-content inline">
|
|
||||||
{#if node.attrs.uploading}
|
|
||||||
<span class="loading loading-spinner loading-xs translate-y-[2px] scale-75" />
|
|
||||||
{:else}
|
|
||||||
<Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
|
|
||||||
{/if}
|
|
||||||
{node.attrs.file.name}
|
|
||||||
</NodeViewWrapper>
|
|
||||||
@@ -1,395 +0,0 @@
|
|||||||
import type {CommandProps, Editor} from "@tiptap/core"
|
|
||||||
import {Extension} from "@tiptap/core"
|
|
||||||
import {now} from "@welshman/lib"
|
|
||||||
import type {StampedEvent, SignedEvent} from "@welshman/util"
|
|
||||||
import type {ImageAttributes, VideoAttributes} from "nostr-editor"
|
|
||||||
import {readServerConfig, uploadFile} from "nostr-tools/nip96"
|
|
||||||
import {getToken} from "nostr-tools/nip98"
|
|
||||||
import type {Node} from "prosemirror-model"
|
|
||||||
import {Plugin, PluginKey} from "prosemirror-state"
|
|
||||||
import {writable} from "svelte/store"
|
|
||||||
|
|
||||||
declare module "@tiptap/core" {
|
|
||||||
interface Commands<ReturnType> {
|
|
||||||
uploadFile: {
|
|
||||||
selectFiles: () => ReturnType
|
|
||||||
uploadFiles: () => ReturnType
|
|
||||||
getMetaTags: () => string[][]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileUploadOptions {
|
|
||||||
allowedMimeTypes: string[]
|
|
||||||
expiration: number
|
|
||||||
immediateUpload: boolean
|
|
||||||
hash: (file: File) => Promise<string>
|
|
||||||
sign?: (event: StampedEvent) => Promise<SignedEvent | undefined>
|
|
||||||
onDrop: (currentEditor: Editor, file: File, pos: number) => void
|
|
||||||
onComplete: (currentEditor: Editor) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UploadTask {
|
|
||||||
url?: string
|
|
||||||
sha256?: string
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function bufferToHex(buffer: ArrayBuffer) {
|
|
||||||
return Array.from(new Uint8Array(buffer))
|
|
||||||
.map(b => b.toString(16).padStart(2, "0"))
|
|
||||||
.join("")
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FileUploadExtension = Extension.create<FileUploadOptions>({
|
|
||||||
name: "fileUpload",
|
|
||||||
|
|
||||||
addStorage() {
|
|
||||||
return {
|
|
||||||
loading: writable(false),
|
|
||||||
tags: [] as string[][],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addOptions() {
|
|
||||||
return {
|
|
||||||
allowedMimeTypes: [
|
|
||||||
"image/jpeg",
|
|
||||||
"image/png",
|
|
||||||
"image/gif",
|
|
||||||
"video/mp4",
|
|
||||||
"video/mpeg",
|
|
||||||
"video/webm",
|
|
||||||
],
|
|
||||||
immediateUpload: true,
|
|
||||||
expiration: 60000,
|
|
||||||
async hash(file: File) {
|
|
||||||
return bufferToHex(await crypto.subtle.digest("SHA-256", await file.arrayBuffer()))
|
|
||||||
},
|
|
||||||
onDrop() {},
|
|
||||||
onComplete() {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
selectFiles: () => props => {
|
|
||||||
props.tr.setMeta("selectFiles", true)
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
uploadFiles: () => (props: CommandProps) => {
|
|
||||||
props.tr.setMeta("uploadFiles", true)
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
getMetaTags: () =>
|
|
||||||
((props: CommandProps) => {
|
|
||||||
const tags: string[][] = []
|
|
||||||
// make sure the file uploaded is still in the editor content
|
|
||||||
props.editor.state.doc.descendants(node => {
|
|
||||||
if (!(node.type.name === "image" || node.type.name === "video")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const tag = props.editor.storage.fileUpload.tags.find((t: string[]) =>
|
|
||||||
t[1].includes(node.attrs.src),
|
|
||||||
)
|
|
||||||
if (tag) {
|
|
||||||
tags.push(tag)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return tags
|
|
||||||
}) as any,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
|
||||||
const uploader = new Uploader(this.editor, this.options)
|
|
||||||
return [
|
|
||||||
new Plugin({
|
|
||||||
key: new PluginKey("fileUploadPlugin"),
|
|
||||||
state: {
|
|
||||||
init() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
apply(tr) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (tr.getMeta("selectFiles")) {
|
|
||||||
uploader.selectFiles()
|
|
||||||
tr.setMeta("selectFiles", null)
|
|
||||||
} else if (tr.getMeta("uploadFiles")) {
|
|
||||||
uploader.uploadFiles()
|
|
||||||
tr.setMeta("uploadFiles", null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
handleDrop: (_, event) => {
|
|
||||||
return uploader.handleDrop(event)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
class Uploader {
|
|
||||||
constructor(
|
|
||||||
public editor: Editor,
|
|
||||||
private options: FileUploadOptions,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
get view() {
|
|
||||||
return this.editor.view
|
|
||||||
}
|
|
||||||
|
|
||||||
addFile(file: File, pos: number) {
|
|
||||||
if (
|
|
||||||
!this.options.allowedMimeTypes.some(amt => amt.split("*").every(s => file.type.includes(s)))
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const {tr} = this.view.state
|
|
||||||
const [mimetype] = file.type.split("/")
|
|
||||||
const node = this.view.state.schema.nodes[mimetype].create({
|
|
||||||
file,
|
|
||||||
src: URL.createObjectURL(file),
|
|
||||||
alt: "",
|
|
||||||
uploading: false,
|
|
||||||
uploadError: null,
|
|
||||||
})
|
|
||||||
tr.insert(pos, node)
|
|
||||||
this.view.dispatch(tr)
|
|
||||||
|
|
||||||
if (this.options.immediateUpload) {
|
|
||||||
this.editor.storage.fileUpload.loading.set(true)
|
|
||||||
this.upload(node).then(() => this.editor.storage.fileUpload.loading.set(false))
|
|
||||||
}
|
|
||||||
this.options.onDrop(this.editor, file, pos)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
findNodePosition(node: Node) {
|
|
||||||
let pos = -1
|
|
||||||
this.view.state.doc.descendants((n, p) => {
|
|
||||||
if (n === node) {
|
|
||||||
pos = p
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return pos
|
|
||||||
}
|
|
||||||
|
|
||||||
findNodes(uploading: boolean) {
|
|
||||||
const nodes = [] as [Node, number][]
|
|
||||||
this.view.state.doc.descendants((node, pos) => {
|
|
||||||
if (!(node.type.name === "image" || node.type.name === "video")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (node.attrs.sha256) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if ((node.attrs.uploading || false) !== uploading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nodes.push([node, pos])
|
|
||||||
})
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
updateNodeAttributes(nodeRef: Node, attrs: Record<string, unknown>) {
|
|
||||||
const {tr} = this.editor.view.state
|
|
||||||
|
|
||||||
const pos = this.findNodePosition(nodeRef)
|
|
||||||
if (pos === -1) return
|
|
||||||
|
|
||||||
Object.entries(attrs).forEach(
|
|
||||||
([key, value]) => value !== undefined && tr.setNodeAttribute(pos, key, value),
|
|
||||||
)
|
|
||||||
this.view.dispatch(tr)
|
|
||||||
}
|
|
||||||
|
|
||||||
onUploadDone(nodeRef: Node, response: UploadTask) {
|
|
||||||
this.findNodes(true).forEach(([node, pos]) => {
|
|
||||||
if (node.attrs.src === nodeRef.attrs.src) {
|
|
||||||
this.updateNodeAttributes(node, {
|
|
||||||
uploading: false,
|
|
||||||
src: response.url,
|
|
||||||
sha256: response.sha256,
|
|
||||||
uploadError: response.error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async upload(node: Node) {
|
|
||||||
const {sign, hash, expiration} = this.options
|
|
||||||
|
|
||||||
const {
|
|
||||||
file,
|
|
||||||
alt,
|
|
||||||
uploadType,
|
|
||||||
uploadUrl: serverUrl,
|
|
||||||
} = node.attrs as ImageAttributes | VideoAttributes
|
|
||||||
|
|
||||||
this.updateNodeAttributes(node, {uploading: true, uploadError: null})
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (uploadType === "nip96") {
|
|
||||||
const res = (await uploadNIP96({file, alt, sign: sign!, serverUrl}))!
|
|
||||||
|
|
||||||
// add the tags as received from nip-96 to the storage
|
|
||||||
this.editor.storage.fileUpload.tags.push(["imeta", ...res.tags!])
|
|
||||||
this.onUploadDone(node, res)
|
|
||||||
} else {
|
|
||||||
const res = await uploadBlossom({file, serverUrl, hash, sign, expiration})
|
|
||||||
this.editor.storage.fileUpload.tags.push([
|
|
||||||
"imeta",
|
|
||||||
`url ${res.url}`,
|
|
||||||
`size ${res.size}`,
|
|
||||||
`m ${res.type}`,
|
|
||||||
`x ${res.sha256}`,
|
|
||||||
])
|
|
||||||
this.onUploadDone(node, res)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const msg = error as string
|
|
||||||
this.onUploadDone(node, {error: msg})
|
|
||||||
throw new Error(msg as string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async uploadFiles() {
|
|
||||||
const tasks = this.findNodes(false).map(([node]) => {
|
|
||||||
return this.upload(node)
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
this.editor.storage.fileUpload.loading.set(true)
|
|
||||||
await Promise.all(tasks)
|
|
||||||
this.options.onComplete(this.editor)
|
|
||||||
} finally {
|
|
||||||
this.editor.storage.fileUpload.loading.set(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectFiles() {
|
|
||||||
const input = document.createElement("input")
|
|
||||||
input.type = "file"
|
|
||||||
input.multiple = true
|
|
||||||
input.accept = this.options.allowedMimeTypes.join(",")
|
|
||||||
input.onchange = event => {
|
|
||||||
const files = (event.target as HTMLInputElement).files
|
|
||||||
if (files) {
|
|
||||||
Array.from(files).forEach(file => {
|
|
||||||
if (file) {
|
|
||||||
const pos = this.view.state.selection.from + 1
|
|
||||||
this.addFile(file, pos)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
input.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDrop(event: DragEvent) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
const pos = this.view.posAtCoords({left: event.clientX, top: event.clientY})?.pos
|
|
||||||
|
|
||||||
if (pos === undefined) return false
|
|
||||||
|
|
||||||
const file = event.dataTransfer?.files?.[0]
|
|
||||||
if (file) {
|
|
||||||
this.addFile(file, pos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NIP96Options {
|
|
||||||
file: File
|
|
||||||
alt?: string
|
|
||||||
serverUrl: string
|
|
||||||
expiration?: number
|
|
||||||
sign: (event: StampedEvent) => Promise<SignedEvent | undefined>
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadNIP96(options: NIP96Options) {
|
|
||||||
try {
|
|
||||||
const server = await readServerConfig(options.serverUrl)
|
|
||||||
const authorization = await getToken(server.api_url, "POST", options.sign as any, true)
|
|
||||||
const res = await uploadFile(options.file, server.api_url, authorization, {
|
|
||||||
alt: options.alt || "",
|
|
||||||
expiration: options.expiration?.toString() || "",
|
|
||||||
content_type: options.file.type,
|
|
||||||
})
|
|
||||||
if (res.status === "error") {
|
|
||||||
throw new Error(res.message)
|
|
||||||
}
|
|
||||||
const url = res.nip94_event?.tags.find(x => x[0] === "url")?.[1] || ""
|
|
||||||
const sha256 = res.nip94_event?.tags.find(x => x[0] === "x")?.[1] || ""
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
sha256,
|
|
||||||
tags: res.nip94_event?.tags.flatMap(item => item.join(" ")),
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlossomOptions {
|
|
||||||
file: File
|
|
||||||
serverUrl: string
|
|
||||||
expiration?: number
|
|
||||||
hash?: (file: File) => Promise<string>
|
|
||||||
sign?: (event: StampedEvent) => Promise<SignedEvent | undefined>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlossomResponse {
|
|
||||||
sha256: string
|
|
||||||
size: number
|
|
||||||
type: string
|
|
||||||
uploaded: number
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlossomResponseError {
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadBlossom(options: BlossomOptions) {
|
|
||||||
if (!options.hash) {
|
|
||||||
throw new Error("No hash function provided")
|
|
||||||
}
|
|
||||||
if (!options.sign) {
|
|
||||||
throw new Error("No signer provided")
|
|
||||||
}
|
|
||||||
const created_at = now()
|
|
||||||
const hash = await options.hash(options.file)
|
|
||||||
const event = await options.sign({
|
|
||||||
kind: 24242,
|
|
||||||
content: `Upload ${options.file.name}`,
|
|
||||||
created_at,
|
|
||||||
tags: [
|
|
||||||
["t", "upload"],
|
|
||||||
["x", hash],
|
|
||||||
["size", options.file.size.toString()],
|
|
||||||
["expiration", (created_at + (options.expiration || 60000)).toString()],
|
|
||||||
],
|
|
||||||
})
|
|
||||||
const data = JSON.stringify(event)
|
|
||||||
const base64 = btoa(data)
|
|
||||||
const authorization = `Nostr ${base64}`
|
|
||||||
const res = await fetch(options.serverUrl + "/upload", {
|
|
||||||
method: "PUT",
|
|
||||||
body: options.file,
|
|
||||||
headers: {
|
|
||||||
authorization,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const json = await res.json()
|
|
||||||
if (res.status === 200) {
|
|
||||||
return json as BlossomResponse
|
|
||||||
}
|
|
||||||
throw new Error((json as BlossomResponseError).message)
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import {last} from "@welshman/lib"
|
|
||||||
import {Node, InputRule, nodePasteRule} from "@tiptap/core"
|
|
||||||
import type {Node as ProsemirrorNode} from "@tiptap/pm/model"
|
|
||||||
import type {MarkdownSerializerState} from "prosemirror-markdown"
|
|
||||||
import {createPasteRuleMatch} from "./util"
|
|
||||||
|
|
||||||
export const LINK_REGEX = /([a-z\+:]{2,30}:\/\/)?[^<>\(\)\s]+\.[a-z]{2,6}[^\s<>"'\.!?,:\)\(]*/gi
|
|
||||||
|
|
||||||
export interface LinkAttributes {
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@tiptap/core" {
|
|
||||||
interface Commands<ReturnType> {
|
|
||||||
inlineLink: {
|
|
||||||
insertLink: (options: {url: string}) => ReturnType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LinkExtension = Node.create({
|
|
||||||
atom: true,
|
|
||||||
name: "inlineLink",
|
|
||||||
group: "inline",
|
|
||||||
inline: true,
|
|
||||||
selectable: true,
|
|
||||||
draggable: true,
|
|
||||||
priority: 1000,
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
url: {default: null},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderHTML(props) {
|
|
||||||
return ["div", {"data-url": props.node.attrs.url}]
|
|
||||||
},
|
|
||||||
renderText(props) {
|
|
||||||
return props.node.attrs.url
|
|
||||||
},
|
|
||||||
addStorage() {
|
|
||||||
return {
|
|
||||||
markdown: {
|
|
||||||
serialize(state: MarkdownSerializerState, node: ProsemirrorNode) {
|
|
||||||
state.write(node.attrs.url)
|
|
||||||
},
|
|
||||||
parse: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
insertLink:
|
|
||||||
({url}) =>
|
|
||||||
({commands}) => {
|
|
||||||
return commands.insertContent(
|
|
||||||
{type: this.name, attrs: {url}},
|
|
||||||
{
|
|
||||||
updateSelection: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addInputRules() {
|
|
||||||
return [
|
|
||||||
new InputRule({
|
|
||||||
find: text => {
|
|
||||||
const match = last(Array.from(text.matchAll(LINK_REGEX)))
|
|
||||||
|
|
||||||
if (match && text.length === match.index + match[0].length + 1) {
|
|
||||||
return {
|
|
||||||
index: match.index!,
|
|
||||||
text: match[0],
|
|
||||||
data: {
|
|
||||||
url: match[0],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
handler: ({state, range, match}) => {
|
|
||||||
const {tr} = state
|
|
||||||
|
|
||||||
if (match[0]) {
|
|
||||||
try {
|
|
||||||
tr.insert(range.from - 1, this.type.create(match.data))
|
|
||||||
.delete(tr.mapping.map(range.from - 1), tr.mapping.map(range.to))
|
|
||||||
.insert(
|
|
||||||
tr.mapping.map(range.to),
|
|
||||||
this.editor.schema.text(last(Array.from(match.input!))),
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
// If the node was already linkified, the above code breaks for whatever reason
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.scrollIntoView()
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
addPasteRules() {
|
|
||||||
return [
|
|
||||||
nodePasteRule({
|
|
||||||
type: this.type,
|
|
||||||
getAttributes: match => match.data,
|
|
||||||
find: text => {
|
|
||||||
const matches = []
|
|
||||||
|
|
||||||
for (const match of text.matchAll(LINK_REGEX)) {
|
|
||||||
try {
|
|
||||||
matches.push(createPasteRuleMatch(match, {url: match[0]}))
|
|
||||||
} catch (e) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let value
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{value}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user