Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -17,6 +17,9 @@ Thumbs.db
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Android
|
||||
.idea
|
||||
|
||||
# Generated assets
|
||||
static/favicon.ico
|
||||
static/pwa-64x64.png
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
# 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"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 2
|
||||
versionName "0.2.3"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
Generated
+478
-155
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flotilla",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flotilla",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.3",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^6.1.2",
|
||||
"@capacitor/cli": "^6.1.2",
|
||||
@@ -30,15 +30,16 @@
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"@vite-pwa/sveltekit": "^0.6.6",
|
||||
"@welshman/app": "~0.0.34",
|
||||
"@welshman/content": "~0.0.14",
|
||||
"@welshman/dvm": "~0.0.12",
|
||||
"@welshman/feeds": "~0.0.27",
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/net": "~0.0.43",
|
||||
"@welshman/signer": "~0.0.17",
|
||||
"@welshman/store": "~0.0.14",
|
||||
"@welshman/util": "~0.0.52",
|
||||
"@welshman/app": "~0.0.37",
|
||||
"@welshman/content": "~0.0.15",
|
||||
"@welshman/dvm": "~0.0.13",
|
||||
"@welshman/editor": "~0.0.6",
|
||||
"@welshman/feeds": "~0.0.30",
|
||||
"@welshman/lib": "~0.0.37",
|
||||
"@welshman/net": "~0.0.45",
|
||||
"@welshman/signer": "~0.0.19",
|
||||
"@welshman/store": "~0.0.15",
|
||||
"@welshman/util": "~0.0.57",
|
||||
"daisyui": "^4.12.10",
|
||||
"date-picker-svelte": "^2.13.0",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -55,6 +56,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@sentry/cli": "^2.40.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/eslint": "^9.6.0",
|
||||
@@ -3118,22 +3120,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.5.0.tgz",
|
||||
"integrity": "sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz",
|
||||
"integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.4.0"
|
||||
"@noble/hashes": "1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
|
||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz",
|
||||
"integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
@@ -3648,6 +3655,158 @@
|
||||
"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": {
|
||||
"version": "8.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.35.0.tgz",
|
||||
@@ -3782,9 +3941,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.7.2.tgz",
|
||||
"integrity": "sha512-rGAH90LPMR5OIG7vuTDRw8WxDYxPXSxuGtu++mxPF+Bv7V2ijPOy3P1oyV1G3KGoS0pPiNugLh+tVLsElcx/9Q==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.0.tgz",
|
||||
"integrity": "sha512-0S3AWx6E2QqwdQqb6z0/q6zq2u9lA9oL3BLyAaITGSC9zt8OwjloS2k1zN6wLa9hp2rO0c0vDnWsTPeFaEaMdw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -3812,53 +3972,57 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.6.6.tgz",
|
||||
"integrity": "sha512-JrEFKsZiLvfvOFhOnnrpA0TzCuJjDeysfbMeuKUZNV4+DhYOL28d39H1++rEtJAX0LcbBU60oC5/PrlU9SpvRQ==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.11.0.tgz",
|
||||
"integrity": "sha512-2roNZxcny1bGjyZ8x6VmGTuKbwfJyTZ1hiqPc/CRTQ1u42yOhbjF4ziA5kfyUoQlzygZrWH9LR5IMYGzPQ1N3w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.6.6"
|
||||
"@tiptap/core": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code-block": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.6.6.tgz",
|
||||
"integrity": "sha512-1YLp/zHMHSkE2xzht8nPR6T4sQJJ3ket798czxWuQEbetFv/l0U/mpiPpYSLObj6oTAoqYZ0kWXZj5eQSpPB8Q==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.11.0.tgz",
|
||||
"integrity": "sha512-8of3qTOLjpveHBrrk8KVliSUVd6R2i2TNrBj0f/21HcFVAy0fP++02p6vI6UPOhwM3+p3CprGdSM48DFCu1rqw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.6.6",
|
||||
"@tiptap/pm": "^2.6.6"
|
||||
"@tiptap/core": "^2.7.0",
|
||||
"@tiptap/pm": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-document": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.6.6.tgz",
|
||||
"integrity": "sha512-6qlH5VWzLHHRVeeciRC6C4ZHpMsAGPNG16EF53z0GeMSaaFD/zU3B239QlmqXmLsAl8bpf8Bn93N0t2ABUvScw==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.11.0.tgz",
|
||||
"integrity": "sha512-9YI0AT3mxyUZD7NHECHyV1uAjQ8KwxOS5ACwvrK1MU8TqY084LmodYNTXPKwpqbr51yvt3qZq1R7UIVu4/22Cg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.6.6"
|
||||
"@tiptap/core": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-dropcursor": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.6.6.tgz",
|
||||
"integrity": "sha512-O6CeKriA9uyHsg7Ui4z5ZjEWXQxrIL+1zDekffW0wenGC3G4LUsCzAiFS4LSrR9a3u7tnwqGApW10rdkmCGF4w==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.11.0.tgz",
|
||||
"integrity": "sha512-p7tUtlz7KzBa+06+7W2LJ8AEiHG5chdnUIapojZ7SqQCrFRVw70R+orpkzkoictxNNHsun0A9FCUy4rz8L0+nQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.6.6",
|
||||
"@tiptap/pm": "^2.6.6"
|
||||
"@tiptap/core": "^2.7.0",
|
||||
"@tiptap/pm": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-floating-menu": {
|
||||
@@ -3879,41 +4043,44 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-gapcursor": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.6.6.tgz",
|
||||
"integrity": "sha512-O2lQ2t0X0Vsbn3yLWxFFHrXY6C2N9Y6ZF/M7LWzpcDTUZeWuhoNkFE/1yOM0h6ZX1DO2A9hNIrKpi5Ny8yx+QA==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.0.tgz",
|
||||
"integrity": "sha512-1TVOthPkUYwTQnQwP0BzuIHVz09epOiXJQ3GqgNZsmTehwcMzz2vGCpx1JXhZ5DoMaREHNLCdraXb1n2FdhDNA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.6.6",
|
||||
"@tiptap/pm": "^2.6.6"
|
||||
"@tiptap/core": "^2.7.0",
|
||||
"@tiptap/pm": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-hard-break": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.6.6.tgz",
|
||||
"integrity": "sha512-bsUuyYBrMDEiudx1dOQSr9MzKv13m0xHWrOK+DYxuIDYJb5g+c9un5cK7Js+et/HEYYSPOoH/iTW6h+4I5YeUg==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.11.0.tgz",
|
||||
"integrity": "sha512-7pMgPNk2FnPT0LcWaWNNxOLK3LQnRSYFgrdBGMXec3sy+y3Lit3hM+EZhbZcHpTIQTbWWs+eskh1waRMIt0ZaQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.6.6"
|
||||
"@tiptap/core": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-history": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.6.6.tgz",
|
||||
"integrity": "sha512-tPTzAmPGqMX5Bd5H8lzRpmsaMvB9DvI5Dy2za/VQuFtxgXmDiFVgHRkRXIuluSkPTuANu84XBOQ0cBijqY8x4w==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.11.0.tgz",
|
||||
"integrity": "sha512-eEUEDoOtS17AHVEPbGfZ+x2L5A87SiIsppWYTkpfIH/8EnVQmzu+3i1tcT9cWvHC31d9JTG7TDptVuuHr30TJw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.6.6",
|
||||
"@tiptap/pm": "^2.6.6"
|
||||
"@tiptap/core": "^2.7.0",
|
||||
"@tiptap/pm": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-image": {
|
||||
@@ -3947,15 +4114,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-paragraph": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.6.6.tgz",
|
||||
"integrity": "sha512-fD/onCr16UQWx+/xEmuFC2MccZZ7J5u4YaENh8LMnAnBXf78iwU7CAcmuc9rfAEO3qiLoYGXgLKiHlh2ZfD4wA==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.11.0.tgz",
|
||||
"integrity": "sha512-xLNC05An3SQq0bVHJtOTLa8As5r6NxDZFpK0NZqO2hTq/fAIRL/9VPeZ8E0tziXULwIvIPp+L0Taw3TvaUkRUg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.6.6"
|
||||
"@tiptap/core": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-placeholder": {
|
||||
@@ -3973,41 +4141,43 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.6.6.tgz",
|
||||
"integrity": "sha512-e84uILnRzNzcwK1DVQNpXVmBG1Cq3BJipTOIDl1LHifOok7MBjhI/X+/NR0bd3N2t6gmDTWi63+4GuJ5EeDmsg==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.11.0.tgz",
|
||||
"integrity": "sha512-LcyrP+7ZEVx3YaKzjMAeujq+4xRt4mZ3ITGph2CQ4vOKFaMI8bzSR909q18t7Qyyvek0a9VydEU1NHSaq4G5jw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.6.6"
|
||||
"@tiptap/core": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.7.2.tgz",
|
||||
"integrity": "sha512-RiRPlwpuE6IHDJytE0tglbFlWELOaqeyGRGv25wBTjzV1plnqC5B3U65XY/8kKuuLjdd3NpRfR68DXBafusSBg==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.0.tgz",
|
||||
"integrity": "sha512-4RU6bpODkMY+ZshzdRFcuUc5jWlMW82LWXR6UOsHK/X/Mav41ZFS0Cyf+hQM6gxxTB09YFIICmGpEpULb+/CuA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.2.1",
|
||||
"prosemirror-collab": "^1.3.1",
|
||||
"prosemirror-commands": "^1.6.0",
|
||||
"prosemirror-commands": "^1.6.2",
|
||||
"prosemirror-dropcursor": "^1.8.1",
|
||||
"prosemirror-gapcursor": "^1.3.2",
|
||||
"prosemirror-history": "^1.4.1",
|
||||
"prosemirror-inputrules": "^1.4.0",
|
||||
"prosemirror-keymap": "^1.2.2",
|
||||
"prosemirror-markdown": "^1.13.0",
|
||||
"prosemirror-markdown": "^1.13.1",
|
||||
"prosemirror-menu": "^1.2.4",
|
||||
"prosemirror-model": "^1.22.3",
|
||||
"prosemirror-model": "^1.23.0",
|
||||
"prosemirror-schema-basic": "^1.2.3",
|
||||
"prosemirror-schema-list": "^1.4.1",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-tables": "^1.4.0",
|
||||
"prosemirror-tables": "^1.6.1",
|
||||
"prosemirror-trailing-node": "^3.0.0",
|
||||
"prosemirror-transform": "^1.10.0",
|
||||
"prosemirror-view": "^1.33.10"
|
||||
"prosemirror-transform": "^1.10.2",
|
||||
"prosemirror-view": "^1.37.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -4015,16 +4185,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/suggestion": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.6.4.tgz",
|
||||
"integrity": "sha512-t4GOEcsVSCwTlugHjZdK5Swe6or/tBej5E3ZWYOFHxkNLDod76Q7hvAeBPYrLeDo6m3sPnxrazfdqSeVclk72g==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.11.0.tgz",
|
||||
"integrity": "sha512-f+KcczhzEEy2f7/0N/RSID+Z6NjxCX6ab26NLfWZxdaEm/J+vQ2Pqh/e5Z59vMfKiC0DJXVcO0rdv2LBh23qDw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.6.4",
|
||||
"@tiptap/pm": "^2.6.4"
|
||||
"@tiptap/core": "^2.7.0",
|
||||
"@tiptap/pm": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@trapezedev/gradle-parse": {
|
||||
@@ -4292,7 +4463,8 @@
|
||||
"node_modules/@types/events": {
|
||||
"version": "3.0.3",
|
||||
"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": {
|
||||
"version": "8.1.5",
|
||||
@@ -4394,6 +4566,15 @@
|
||||
"license": "MIT",
|
||||
"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": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0.tgz",
|
||||
@@ -4662,19 +4843,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/app": {
|
||||
"version": "0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/app/-/app-0.0.34.tgz",
|
||||
"integrity": "sha512-wxCWvoal/ctRvImK8dIgg7IajA4eXPheUAXPwPmO6ZuYPV+ytIYzmKGalsYMDMDweQrWVKOf0gLEMOap8LS0iQ==",
|
||||
"version": "0.0.37",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/app/-/app-0.0.37.tgz",
|
||||
"integrity": "sha512-EhhLx10PE6r/soiuaR0GF+NSH9H3ilTaXwmfx2cHHR1PE2LXXvf1oWMJl0ZPFmYe0VWfNiu98SLbTLYwe1Y4dQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@welshman/dvm": "~0.0.11",
|
||||
"@welshman/feeds": "~0.0.26",
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/net": "~0.0.41",
|
||||
"@welshman/signer": "~0.0.16",
|
||||
"@welshman/store": "~0.0.13",
|
||||
"@welshman/util": "~0.0.50",
|
||||
"@welshman/dvm": "~0.0.13",
|
||||
"@welshman/feeds": "~0.0.30",
|
||||
"@welshman/lib": "~0.0.37",
|
||||
"@welshman/net": "~0.0.45",
|
||||
"@welshman/signer": "~0.0.19",
|
||||
"@welshman/store": "~0.0.15",
|
||||
"@welshman/util": "~0.0.54",
|
||||
"fuse.js": "^7.0.0",
|
||||
"idb": "^8.0.0",
|
||||
"svelte": "^4.2.18",
|
||||
@@ -4682,42 +4863,84 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/content": {
|
||||
"version": "0.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.14.tgz",
|
||||
"integrity": "sha512-LwdBJOF5n2EIdmLgn4tJliTKEmTEDZz68zvcmjVhpn34vkc/7lQvHz5pfsQK/CRjPGFsMEdjrSepGXD8v2JAwA==",
|
||||
"version": "0.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.15.tgz",
|
||||
"integrity": "sha512-y0f0iLIaHUqEJJ0ziRWbGw13mg0tOLTKpHQNgIXJ03PD3xGHBaQ5xPWiOI8XeUt35KgrayvQZHsaqfAsOWkwag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.2",
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/lib": "~0.0.34",
|
||||
"nostr-tools": "^2.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/dvm": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/dvm/-/dvm-0.0.12.tgz",
|
||||
"integrity": "sha512-6VqDJzzsfw2UxxIVbJB3+xw6o3qg29+Kkrp1Ei537oXnqmH4W4fGVyoGSdJQQfhEbzdDzT4u/OCUX+z85wfNNA==",
|
||||
"version": "0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/dvm/-/dvm-0.0.13.tgz",
|
||||
"integrity": "sha512-C8y4s7wDJTJ6DVuzQoRLAhMpFD+kBoRHlc7kCTmjzh62VOmDSY+46xKttK/WaEnypPiPbIbBg3hd3+tO2A9KoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/net": "~0.0.41",
|
||||
"@welshman/util": "~0.0.50",
|
||||
"@noble/hashes": "^1.6.1",
|
||||
"@welshman/lib": "~0.0.34",
|
||||
"@welshman/net": "~0.0.43",
|
||||
"@welshman/util": "~0.0.52",
|
||||
"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": {
|
||||
"version": "0.0.27",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/feeds/-/feeds-0.0.27.tgz",
|
||||
"integrity": "sha512-AMm3v3mJlCYMI8C86/LhF/hNx2dkBqdHVWebTX69NNIK24Srbd67YduOdIeuIAIYlBZj93BMi8lvOL5YY2RWPw==",
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/feeds/-/feeds-0.0.30.tgz",
|
||||
"integrity": "sha512-Zcex2uJVeYM55zDI1Dhb5I41lYGD4BURWl95nbFaWbbMYDwoAFIS2cPXBsaGNrITzsz8qByvRs2RnplrmZwSzA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/util": "~0.0.50"
|
||||
"@welshman/lib": "~0.0.37",
|
||||
"@welshman/util": "~0.0.54"
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/lib": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.33.tgz",
|
||||
"integrity": "sha512-otaTKItm0DDR+/IHI5puYo1hU3ssd0R9LTxS+DcIKL6H+0fxtn6OLUmhcHROQukqZ6Jf7l7sfj9MX50KqPicjQ==",
|
||||
"version": "0.0.37",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.37.tgz",
|
||||
"integrity": "sha512-qnEjdGIb/QVIYML0EQgGAhucds00hiX8/4rJ9OcqoRUXPS2cDD47BcgYap+kG/OKSfgSL7EH64voAdoprZtuvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@scure/base": "^1.1.6",
|
||||
@@ -4726,51 +4949,72 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/net": {
|
||||
"version": "0.0.43",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.43.tgz",
|
||||
"integrity": "sha512-qcQPl944ArM9+GvwXb0nsWaMKQgsBCKtGvLOWJZs24S+n+8LiW1hmUP71evp5hFoLFTbws/l3OXAML9jO+W7Lw==",
|
||||
"version": "0.0.45",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.45.tgz",
|
||||
"integrity": "sha512-sXYmfGdqvrj1ssr5xaSUxmJAFo+ScJtodBpzgya0CTLYorKRGoeQRJsyPWdh5VBVtoldPclzGhfvZ11d8d8Lyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/util": "~0.0.50",
|
||||
"@welshman/lib": "~0.0.37",
|
||||
"@welshman/util": "~0.0.54",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/signer": {
|
||||
"version": "0.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/signer/-/signer-0.0.17.tgz",
|
||||
"integrity": "sha512-mZhOKTmtEgAFI2D0KJrZIX5A6WDnHk1+YwIlzL3FyZwdxYnqg4Hx/MPHtAyZoMVt19iodKeZ+Fis/sLblXsXgg==",
|
||||
"version": "0.0.19",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/signer/-/signer-0.0.19.tgz",
|
||||
"integrity": "sha512-+pKkm5HeaSJB6ET456w0zVnbVAjRzYuDagYUns1BQ6Co22nUSp2CntWFFckwbH2eQ9bjdlC65LjRfIP9qhNSrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/net": "~0.0.41",
|
||||
"@welshman/util": "~0.0.50",
|
||||
"@noble/curves": "^1.7.0",
|
||||
"@noble/hashes": "^1.6.1",
|
||||
"@welshman/lib": "~0.0.37",
|
||||
"@welshman/net": "~0.0.45",
|
||||
"@welshman/util": "~0.0.54",
|
||||
"nostr-tools": "^2.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"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": {
|
||||
"version": "0.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/store/-/store-0.0.14.tgz",
|
||||
"integrity": "sha512-6y5c+/5yLcRGbVr7doOys0KJH/dnxCjjrUm0iQIvp/JEdSbGKcNhPjfWqs2mnh51PZIF9UlR9eDSjlE0nUxfgg==",
|
||||
"version": "0.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/store/-/store-0.0.15.tgz",
|
||||
"integrity": "sha512-xapI9cqmpf6ot90T0Z+gFH2HSAby/N8oyLl7u+JASYbNDS3pkK26SviNlTciMC+VBuJChEr1zX5l8RHuLPtw5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/util": "~0.0.50",
|
||||
"@welshman/lib": "~0.0.34",
|
||||
"@welshman/util": "~0.0.52",
|
||||
"svelte": "^4.2.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@welshman/util": {
|
||||
"version": "0.0.52",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.52.tgz",
|
||||
"integrity": "sha512-w17nJ9T8mhwy010WnSjGzRn9kPerZvtG6Ay5fGHw13ZC0hnOD8fkWi85r4/sI+FbCaMLAeKM57P9XD8rIkOfpw==",
|
||||
"version": "0.0.57",
|
||||
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.57.tgz",
|
||||
"integrity": "sha512-YflD6sfqdhIfHioJVlLydvyKOgACFL0dAcWHymlDz/FszIAl2k0XQXKgAjf0lT2uoXfrCdPsfSZwMTW7qUAY6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@welshman/lib": "~0.0.37",
|
||||
"nostr-tools": "^2.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xml-tools/parser": {
|
||||
@@ -4832,6 +5076,19 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||
@@ -7099,6 +7356,7 @@
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
@@ -8071,6 +8329,20 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "9.1.6",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz",
|
||||
@@ -8623,6 +8895,7 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
|
||||
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ws": "*"
|
||||
}
|
||||
@@ -9741,8 +10014,7 @@
|
||||
},
|
||||
"node_modules/nostr-editor": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nostr-editor/-/nostr-editor-0.0.3.tgz",
|
||||
"integrity": "sha512-ODfwzebBRweaYt8l0pz8EbV4OqbEKZpDAVdoU+j7ubmfjhqIyk1PcQoikEZ8UasqkBcZjEQMAPl776F8nb55fQ==",
|
||||
"resolved": "git+ssh://git@github.com/cesardeazevedo/nostr-editor.git#a211491c7cfeb792ae58ba91d295fe747c151ded",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"light-bolt11-decoder": "^3.1.1"
|
||||
@@ -9773,9 +10045,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-tools": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.2.tgz",
|
||||
"integrity": "sha512-Bq3Ug0SZFtgtL1+0wCnAe8AJtI7yx/00/a2nUug9SkhfOwlKS92Tef12iCK9FdwXw+oFZWMtRnSwcLayQso+xA==",
|
||||
"version": "2.10.4",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
|
||||
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^0.5.1",
|
||||
"@noble/curves": "1.2.0",
|
||||
@@ -9785,7 +10058,7 @@
|
||||
"@scure/bip39": "1.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"nostr-wasm": "v0.1.0"
|
||||
"nostr-wasm": "0.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.0"
|
||||
@@ -10546,6 +10819,16 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
@@ -10587,14 +10870,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-commands": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz",
|
||||
"integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==",
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.2.tgz",
|
||||
"integrity": "sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
"prosemirror-transform": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-dropcursor": {
|
||||
@@ -10653,15 +10937,42 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.0.tgz",
|
||||
"integrity": "sha512-UziddX3ZYSYibgx8042hfGKmukq5Aljp2qoBiJRejD/8MH70siQNz5RB1TrdTPheqLMy4aCe4GYNF10/3lQS5g==",
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz",
|
||||
"integrity": "sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/markdown-it": "^14.0.0",
|
||||
"markdown-it": "^14.0.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": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz",
|
||||
@@ -10675,9 +10986,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-model": {
|
||||
"version": "1.22.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.22.3.tgz",
|
||||
"integrity": "sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==",
|
||||
"version": "1.24.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.24.1.tgz",
|
||||
"integrity": "sha512-YM053N+vTThzlWJ/AtPtF1j0ebO36nvbmDy4U7qA2XQB8JVaQp1FmB9Jhrps8s+z+uxhhVTny4m20ptUvhk0Mg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
@@ -10715,16 +11027,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-tables": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.4.0.tgz",
|
||||
"integrity": "sha512-fxryZZkQG12fSCNuZDrYx6Xvo2rLYZTbKLRd8rglOPgNJGMKIS8uvTt6gGC38m7UCu/ENnXIP9pEz5uDaPc+cA==",
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.2.tgz",
|
||||
"integrity": "sha512-97dKocVLrEVTQjZ4GBLdrrMw7Gv3no8H8yMwf5IRM9OoHrzbWpcH5jJxYgNQIRCtdIqwDctT1HdMHrGTiwp1dQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.1.2",
|
||||
"prosemirror-model": "^1.8.1",
|
||||
"prosemirror-state": "^1.3.1",
|
||||
"prosemirror-transform": "^1.2.1",
|
||||
"prosemirror-view": "^1.13.3"
|
||||
"prosemirror-keymap": "^1.2.2",
|
||||
"prosemirror-model": "^1.24.1",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-transform": "^1.10.2",
|
||||
"prosemirror-view": "^1.37.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-trailing-node": {
|
||||
@@ -10743,18 +11056,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-transform": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz",
|
||||
"integrity": "sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg==",
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz",
|
||||
"integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-view": {
|
||||
"version": "1.33.11",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.33.11.tgz",
|
||||
"integrity": "sha512-K0z9oMf6EI2ZifS9yW8PUPjEw2o1ZoFAaNzvcuyfcjIzsU6pJMo3tk9r26MyzEsuGHXZwmKPEmrjgFd78biTGA==",
|
||||
"version": "1.37.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.37.1.tgz",
|
||||
"integrity": "sha512-MEAnjOdXU1InxEmhjgmEzQAikaS6lF3hD64MveTPpjOGNTl87iRLA1HupC/DEV6YuK7m4Q9DHFNTjwIVtqz5NA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
@@ -10762,6 +11077,13 @@
|
||||
"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": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
@@ -14068,6 +14390,7 @@
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
||||
+12
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flotilla",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -15,6 +15,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@sentry/cli": "^2.40.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/eslint": "^9.6.0",
|
||||
@@ -58,15 +59,16 @@
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"@vite-pwa/sveltekit": "^0.6.6",
|
||||
"@welshman/app": "~0.0.34",
|
||||
"@welshman/content": "~0.0.14",
|
||||
"@welshman/dvm": "~0.0.12",
|
||||
"@welshman/feeds": "~0.0.27",
|
||||
"@welshman/lib": "~0.0.33",
|
||||
"@welshman/net": "~0.0.43",
|
||||
"@welshman/signer": "~0.0.17",
|
||||
"@welshman/store": "~0.0.14",
|
||||
"@welshman/util": "~0.0.52",
|
||||
"@welshman/app": "~0.0.37",
|
||||
"@welshman/content": "~0.0.15",
|
||||
"@welshman/dvm": "~0.0.13",
|
||||
"@welshman/editor": "~0.0.6",
|
||||
"@welshman/feeds": "~0.0.30",
|
||||
"@welshman/lib": "~0.0.37",
|
||||
"@welshman/net": "~0.0.45",
|
||||
"@welshman/signer": "~0.0.19",
|
||||
"@welshman/store": "~0.0.15",
|
||||
"@welshman/util": "~0.0.57",
|
||||
"daisyui": "^4.12.10",
|
||||
"date-picker-svelte": "^2.13.0",
|
||||
"dotenv": "^16.4.5",
|
||||
|
||||
+28
-3
@@ -40,15 +40,14 @@
|
||||
|
||||
:root {
|
||||
font-family: Lato;
|
||||
}
|
||||
|
||||
[data-theme] {
|
||||
--base-100: oklch(var(--b1));
|
||||
--base-200: oklch(var(--b2));
|
||||
--base-300: oklch(var(--b3));
|
||||
--base-content: oklch(var(--bc));
|
||||
--primary: oklch(var(--p));
|
||||
--primary-content: oklch(var(--pc));
|
||||
--secondary: oklch(var(--s));
|
||||
--secondary-content: oklch(var(--sc));
|
||||
}
|
||||
|
||||
.bg-alt,
|
||||
@@ -120,6 +119,16 @@
|
||||
@apply overflow-hidden text-ellipsis;
|
||||
}
|
||||
|
||||
[data-tip]::before {
|
||||
@apply ellipsize;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
[data-tip]::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content-padding-x {
|
||||
@apply px-4 sm:px-8 md:px-12;
|
||||
}
|
||||
@@ -239,3 +248,19 @@ emoji-picker {
|
||||
--input-font-color: var(--base-content);
|
||||
--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);
|
||||
}
|
||||
|
||||
+27
-17
@@ -1,7 +1,8 @@
|
||||
import {get} from "svelte/store"
|
||||
import {ctx, sample, uniq, sleep, chunk, equals, choice} from "@welshman/lib"
|
||||
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
|
||||
import {
|
||||
DELETE,
|
||||
REPORT,
|
||||
PROFILE,
|
||||
INBOX_RELAYS,
|
||||
RELAYS,
|
||||
@@ -85,19 +86,6 @@ export const getPubkeyPetname = (pubkey: string) => {
|
||||
return display
|
||||
}
|
||||
|
||||
export const makeMention = (pubkey: string, hints?: string[]) => [
|
||||
"p",
|
||||
pubkey,
|
||||
choice(hints || getPubkeyHints(pubkey)),
|
||||
getPubkeyPetname(pubkey),
|
||||
]
|
||||
|
||||
export const makeIMeta = (url: string, data: Record<string, string>) => [
|
||||
"imeta",
|
||||
`url ${url}`,
|
||||
...Object.entries(data).map(([k, v]) => [k, v].join(" ")),
|
||||
]
|
||||
|
||||
export const getThunkError = async (thunk: Thunk) => {
|
||||
const result = await thunk.result
|
||||
const [{status, message}] = Object.values(result) as any
|
||||
@@ -121,7 +109,7 @@ export const loginWithNip46 = async ({
|
||||
connectSecret?: string
|
||||
}) => {
|
||||
const broker = Nip46Broker.get({relays, clientSecret, signerPubkey})
|
||||
const result = await broker.connect("", connectSecret, NIP46_PERMS)
|
||||
const result = await broker.connect(connectSecret, NIP46_PERMS)
|
||||
|
||||
// TODO: remove ack result
|
||||
if (!["ack", connectSecret].includes(result)) return false
|
||||
@@ -342,7 +330,7 @@ export const checkRelayAccess = async (url: string, claim = "") => {
|
||||
|
||||
const result = await thunk.result
|
||||
|
||||
if (result[url].status !== PublishStatus.Success) {
|
||||
if (result[url].status === PublishStatus.Failure) {
|
||||
const message =
|
||||
connection.auth.message?.replace(/^.*: /, "") ||
|
||||
result[url].message?.replace(/^.*: /, "") ||
|
||||
@@ -389,7 +377,6 @@ export const checkRelayAuth = async (url: string, timeout = 3000) => {
|
||||
|
||||
export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||
const checks = [
|
||||
() => checkRelayProfile(url),
|
||||
() => checkRelayConnection(url),
|
||||
() => checkRelayAccess(url, claim),
|
||||
() => checkRelayAuth(url),
|
||||
@@ -443,6 +430,29 @@ export const makeDelete = ({event}: {event: TrustedEvent}) => {
|
||||
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 = {
|
||||
event: TrustedEvent
|
||||
content: string
|
||||
|
||||
@@ -1,56 +1,63 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {createEditor, EditorContent} from "svelte-tiptap"
|
||||
import {writable} from "svelte/store"
|
||||
import {isMobile} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
||||
import {getPubkeyHints} from "@app/commands"
|
||||
import {getEditor} from "@app/editor"
|
||||
|
||||
export let onSubmit: any
|
||||
export let content = ""
|
||||
export let editor = createEditor(
|
||||
getEditorOptions({
|
||||
submit,
|
||||
getPubkeyHints,
|
||||
submitOnEnter: true,
|
||||
autofocus: !isMobile,
|
||||
}),
|
||||
)
|
||||
export let editor: ReturnType<typeof getEditor> | undefined = undefined
|
||||
|
||||
function submit() {
|
||||
if ($loading) return
|
||||
const uploading = writable(false)
|
||||
|
||||
onSubmit({
|
||||
content: $editor.getText({blockSeparator: "\n"}),
|
||||
tags: getEditorTags($editor),
|
||||
})
|
||||
let element: HTMLElement
|
||||
|
||||
$editor.chain().clearContent().run()
|
||||
const uploadFiles = () => $editor!.chain().selectFiles().run()
|
||||
|
||||
const submit = () => {
|
||||
if ($uploading) return
|
||||
|
||||
const content = $editor!.getText({blockSeparator: "\n"}).trim()
|
||||
const tags = $editor!.storage.nostr.getEditorTags()
|
||||
|
||||
if (!content) return
|
||||
|
||||
onSubmit({content, tags})
|
||||
|
||||
$editor!.chain().clearContent().run()
|
||||
}
|
||||
|
||||
$: loading = $editor?.storage.fileUpload.loading
|
||||
|
||||
onMount(() => {
|
||||
$editor.commands.setContent(content)
|
||||
editor = getEditor({autofocus: !isMobile, element, submit, uploading})
|
||||
|
||||
$editor!.chain().setContent(content).run()
|
||||
})
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="relative z-feature flex gap-2 p-2"
|
||||
on:submit|preventDefault={$loading ? undefined : submit}>
|
||||
on:submit|preventDefault={$uploading ? undefined : submit}>
|
||||
<Button
|
||||
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"
|
||||
disabled={$loading}
|
||||
on:click={$editor.commands.selectFiles}>
|
||||
{#if $loading}
|
||||
disabled={$uploading}
|
||||
on:click={uploadFiles}>
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon="gallery-send" />
|
||||
{/if}
|
||||
</Button>
|
||||
<div class="chat-editor flex-grow overflow-hidden">
|
||||
<EditorContent editor={$editor} />
|
||||
<div bind:this={element} />
|
||||
</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>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-2 ml-10 mt-1">
|
||||
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-right" />
|
||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
|
||||
</div>
|
||||
<button
|
||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import EventReport from "@app/components/EventReport.svelte"
|
||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
@@ -10,6 +11,11 @@
|
||||
export let event
|
||||
export let onClick
|
||||
|
||||
const report = () => {
|
||||
onClick()
|
||||
pushModal(EventReport, {url, event})
|
||||
}
|
||||
|
||||
const showInfo = () => {
|
||||
onClick()
|
||||
pushModal(EventInfo, {event})
|
||||
@@ -35,5 +41,12 @@
|
||||
Delete Message
|
||||
</Button>
|
||||
</li>
|
||||
{:else}
|
||||
<li>
|
||||
<Button class="text-error" on:click={report}>
|
||||
<Icon size={4} icon="danger" />
|
||||
Report Content
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
<form class="column gap-4" on:submit|preventDefault={submit}>
|
||||
<ModalHeader>
|
||||
<div slot="title">Enable Messages</div>
|
||||
<div slot="info">Do you want to enable notes and direct messages?</div>
|
||||
<div slot="info">Do you want to enable direct messages?</div>
|
||||
</ModalHeader>
|
||||
<p>
|
||||
By default, notes and direct messages are disabled, since loading them requires
|
||||
By default, direct messages are disabled, since loading them requires
|
||||
{PLATFORM_NAME} to download and decrypt a lot of data.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -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>
|
||||
@@ -22,7 +22,7 @@
|
||||
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
||||
</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]">
|
||||
{#if url.match(/\.(mov|webm|mp4)$/)}
|
||||
<video controls src={url} class="max-h-96 object-contain object-center">
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
||||
import {writable} from "svelte/store"
|
||||
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 Icon from "@lib/components/Icon.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
@@ -12,16 +11,17 @@
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
||||
import {PROTECTED} from "@app/state"
|
||||
import {getPubkeyHints} from "@app/commands"
|
||||
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
||||
import {getEditor} from "@app/editor"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
export let url
|
||||
|
||||
const uploading = writable(false)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const submit = () => {
|
||||
if ($loading) return
|
||||
if ($uploading) return
|
||||
|
||||
if (!title) {
|
||||
return pushToast({
|
||||
@@ -37,16 +37,15 @@
|
||||
})
|
||||
}
|
||||
|
||||
const kind = isAllDay ? EVENT_DATE : EVENT_TIME
|
||||
const event = createEvent(kind, {
|
||||
content: $editor.getText({blockSeparator: "\n"}),
|
||||
const event = createEvent(EVENT_TIME, {
|
||||
content: $editor.getText({blockSeparator: "\n"}).trim(),
|
||||
tags: [
|
||||
["d", randomId()],
|
||||
["title", title],
|
||||
["location", location],
|
||||
["start", dateToSeconds(start).toString()],
|
||||
["end", dateToSeconds(end).toString()],
|
||||
...getEditorTags($editor),
|
||||
...$editor.storage.nostr.getEditorTags(),
|
||||
PROTECTED,
|
||||
],
|
||||
})
|
||||
@@ -55,17 +54,15 @@
|
||||
history.back()
|
||||
}
|
||||
|
||||
let editor: Readable<Editor>
|
||||
const isAllDay = false
|
||||
let element: HTMLElement
|
||||
let editor: ReturnType<typeof getEditor>
|
||||
let title = ""
|
||||
let location = ""
|
||||
let start: Date
|
||||
let end: Date
|
||||
|
||||
$: loading = $editor?.storage.fileUpload.loading
|
||||
|
||||
onMount(() => {
|
||||
editor = createEditor(getEditorOptions({submit, getPubkeyHints}))
|
||||
editor = getEditor({submit, element, uploading})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -86,13 +83,13 @@
|
||||
slot="input"
|
||||
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">
|
||||
<EditorContent editor={$editor} />
|
||||
<div bind:this={element} />
|
||||
</div>
|
||||
<Button
|
||||
data-tip="Add an image"
|
||||
class="center btn tooltip"
|
||||
on:click={$editor.commands.selectFiles}>
|
||||
{#if $loading}
|
||||
on:click={() => $editor.chain().selectFiles().run()}>
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon="gallery-send" />
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Dialog from "@lib/components/Dialog.svelte"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import LogIn from "@app/components/LogIn.svelte"
|
||||
@@ -33,5 +34,10 @@
|
||||
<div slot="info">Just a few questions and you'll be on your way.</div>
|
||||
</CardButton>
|
||||
</Button>
|
||||
<p class="text-center text-xs opacity-75">
|
||||
By using {PLATFORM_NAME}, you consent to our
|
||||
<Link external class="link" href="/terms.html">Terms of Service</Link> and
|
||||
<Link external class="link" href="/privacy.html">Privacy Policy</Link>.
|
||||
</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
||||
import {Nip46Broker, getPubkey, makeSecret} from "@welshman/signer"
|
||||
import {addSession} from "@welshman/app"
|
||||
import {slideAndFade} from "@lib/transition"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
@@ -63,6 +63,15 @@
|
||||
let input = ""
|
||||
let loading = false
|
||||
|
||||
$: {
|
||||
// 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,
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<SecondaryNavSection class="max-h-screen">
|
||||
<div>
|
||||
<SecondaryNavItem class="w-full !justify-between" on:click={openMenu}>
|
||||
<strong>{displayRelayUrl(url)}</strong>
|
||||
<strong class="ellipsize">{displayRelayUrl(url)}</strong>
|
||||
<Icon icon="alt-arrow-down" />
|
||||
</SecondaryNavItem>
|
||||
{#if showMenu}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<NoteCard {event} class="card2 bg-alt">
|
||||
<Content {event} expandMode="inline" />
|
||||
<div class="flex w-full justify-between gap-2">
|
||||
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-right">
|
||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right">
|
||||
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
||||
<Icon icon="smile-circle" size={4} />
|
||||
</EmojiButton>
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
|
||||
const showSettingsMenu = () => pushModal(MenuSettings)
|
||||
|
||||
const openNotes = () => ($canDecrypt ? goto("/notes") : pushModal(ChatEnable, {next: "/notes"}))
|
||||
|
||||
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
|
||||
|
||||
$: spaceUrls = Array.from($userRoomsByUrl.keys())
|
||||
@@ -58,9 +56,6 @@
|
||||
class="tooltip-right">
|
||||
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem title="Notes" on:click={openNotes} class="tooltip-right">
|
||||
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem
|
||||
title="Messages"
|
||||
on:click={openChat}
|
||||
@@ -81,11 +76,8 @@
|
||||
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="flex gap-2 sm:gap-8">
|
||||
<PrimaryNavItem title="Search" href="/people">
|
||||
<Avatar icon="magnifer" class="!h-10 !w-10" />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem title="Notes" on:click={openNotes}>
|
||||
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
|
||||
<PrimaryNavItem title="Home" href="/home">
|
||||
<Avatar icon="home-smile" class="!h-10 !w-10" />
|
||||
</PrimaryNavItem>
|
||||
<PrimaryNavItem
|
||||
title="Messages"
|
||||
@@ -98,7 +90,7 @@
|
||||
</PrimaryNavItem>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type {SvelteComponent} from "svelte"
|
||||
import {derived} from "svelte/store"
|
||||
import {type Instance} from "tippy.js"
|
||||
import {append, remove, uniq} from "@welshman/lib"
|
||||
import {profileSearch} from "@welshman/app"
|
||||
import {Suggestions} from "@welshman/editor"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Suggestions from "@lib/editor/Suggestions.svelte"
|
||||
import SuggestionProfile from "@lib/editor/SuggestionProfile.svelte"
|
||||
import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
|
||||
import ProfileName from "@app/components/ProfileName.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
@@ -20,6 +21,8 @@
|
||||
let popover: Instance
|
||||
let instance: SvelteComponent
|
||||
|
||||
const search = derived(profileSearch, $profileSearch => $profileSearch.searchValues)
|
||||
|
||||
const selectPubkey = (pubkey: string) => {
|
||||
term = ""
|
||||
popover.hide()
|
||||
@@ -76,9 +79,9 @@
|
||||
component={Suggestions}
|
||||
props={{
|
||||
term,
|
||||
search,
|
||||
select: selectPubkey,
|
||||
search: profileSearch,
|
||||
component: SuggestionProfile,
|
||||
component: ProfileSuggestion,
|
||||
class: "rounded-box",
|
||||
style: `left: 4px; width: ${input?.clientWidth + 12}px`,
|
||||
}}
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {groupBy, uniqBy, batch} from "@welshman/lib"
|
||||
import {REACTION, DELETE} from "@welshman/util"
|
||||
import {groupBy, uniq, uniqBy, batch} from "@welshman/lib"
|
||||
import {REACTION, getTag, REPORT, DELETE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {deriveEvents} from "@welshman/store"
|
||||
import {pubkey, repository, load, displayProfileByPubkey} from "@welshman/app"
|
||||
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 {pushModal} from "@app/modal"
|
||||
|
||||
export let event
|
||||
export let onReactionClick
|
||||
export let relays: string[] = []
|
||||
export let url = ""
|
||||
export let reactionClass = ""
|
||||
export let noTooltip = false
|
||||
|
||||
const reports = deriveEvents(repository, {
|
||||
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(
|
||||
e => e.content,
|
||||
uniqBy(e => e.pubkey + e.content, $reactions),
|
||||
@@ -26,11 +37,11 @@
|
||||
|
||||
onMount(() => {
|
||||
load({
|
||||
relays,
|
||||
filters: [{kinds: [REACTION, DELETE], "#e": [event.id]}],
|
||||
relays: [url],
|
||||
filters: [{kinds: [REACTION, REPORT, DELETE], "#e": [event.id]}],
|
||||
onEvent: batch(300, (events: TrustedEvent[]) => {
|
||||
load({
|
||||
relays,
|
||||
relays: [url],
|
||||
filters: [{kinds: [DELETE], "#e": events.map(e => e.id)}],
|
||||
})
|
||||
}),
|
||||
@@ -38,8 +49,19 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $reactions.length > 0}
|
||||
{#if $reactions.length > 0 || $reports.length > 0}
|
||||
<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]}
|
||||
{@const pubkeys = events.map(e => e.pubkey)}
|
||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<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"
|
||||
@@ -47,7 +48,9 @@
|
||||
<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 this space.</p>
|
||||
<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>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-left" />
|
||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-left" />
|
||||
{#if $deleted}
|
||||
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
|
||||
{:else if thunk}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
||||
import {writable} from "svelte/store"
|
||||
import {createEvent, THREAD} from "@welshman/util"
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {isMobile} from "@lib/html"
|
||||
@@ -12,15 +11,16 @@
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {GENERAL, tagRoom, PROTECTED} from "@app/state"
|
||||
import {getPubkeyHints} from "@app/commands"
|
||||
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
||||
import {getEditor} from "@app/editor"
|
||||
|
||||
export let url
|
||||
|
||||
const uploading = writable(false)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const submit = () => {
|
||||
if ($loading) return
|
||||
if ($uploading) return
|
||||
|
||||
if (!title) {
|
||||
return pushToast({
|
||||
@@ -29,7 +29,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
const content = $editor.getText({blockSeparator: "\n"})
|
||||
const content = $editor.getText({blockSeparator: "\n"}).trim()
|
||||
|
||||
if (!content.trim()) {
|
||||
return pushToast({
|
||||
@@ -38,7 +38,12 @@
|
||||
})
|
||||
}
|
||||
|
||||
const tags = [["title", title], tagRoom(GENERAL, url), ...getEditorTags($editor), PROTECTED]
|
||||
const tags = [
|
||||
...$editor.storage.nostr.getEditorTags(),
|
||||
tagRoom(GENERAL, url),
|
||||
["title", title],
|
||||
PROTECTED,
|
||||
]
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
@@ -49,14 +54,11 @@
|
||||
}
|
||||
|
||||
let title: string
|
||||
let editor: Readable<Editor>
|
||||
|
||||
$: loading = $editor?.storage.fileUpload.loading
|
||||
let element: HTMLElement
|
||||
let editor: ReturnType<typeof getEditor>
|
||||
|
||||
onMount(() => {
|
||||
editor = createEditor(
|
||||
getEditorOptions({submit, getPubkeyHints, placeholder: "What's on your mind?"}),
|
||||
)
|
||||
editor = getEditor({submit, element, uploading, placeholder: "What's on your mind?"})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -81,14 +83,14 @@
|
||||
<Field>
|
||||
<p slot="label">Message*</p>
|
||||
<div slot="input" class="note-editor flex-grow overflow-hidden">
|
||||
<EditorContent editor={$editor} />
|
||||
<div bind:this={element} />
|
||||
</div>
|
||||
</Field>
|
||||
<Button
|
||||
data-tip="Add an image"
|
||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||
on:click={$editor.commands.selectFiles}>
|
||||
{#if $loading}
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon="paperclip" size={3} />
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="-mb-3 h-0 text-end text-xs opacity-75">
|
||||
<p class="mb-3 h-0 text-xs opacity-75">
|
||||
{formatTimestamp(event.created_at)}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import EventReport from "@app/components/EventReport.svelte"
|
||||
import ThreadShare from "@app/components/ThreadShare.svelte"
|
||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
@@ -14,6 +15,11 @@
|
||||
|
||||
const isRoot = event.kind !== COMMENT
|
||||
|
||||
const report = () => {
|
||||
onClick()
|
||||
pushModal(EventReport, {url, event})
|
||||
}
|
||||
|
||||
const showInfo = () => {
|
||||
onClick()
|
||||
pushModal(EventInfo, {event})
|
||||
@@ -52,5 +58,12 @@
|
||||
Delete Message
|
||||
</Button>
|
||||
</li>
|
||||
{:else}
|
||||
<li>
|
||||
<Button class="text-error" on:click={report}>
|
||||
<Icon size={4} icon="danger" />
|
||||
Report Content
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
|
||||
import {writable} from "svelte/store"
|
||||
import {isMobile} from "@lib/html"
|
||||
import {fly, slideAndFade} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import {getEditorOptions, getEditorTags} from "@lib/editor"
|
||||
import {getPubkeyHints, publishComment} from "@app/commands"
|
||||
import {publishComment} from "@app/commands"
|
||||
import {tagRoom, GENERAL, PROTECTED} from "@app/state"
|
||||
import {getEditor} from "@app/editor"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
export let url
|
||||
@@ -17,13 +16,15 @@
|
||||
export let onClose
|
||||
export let onSubmit
|
||||
|
||||
const uploading = writable(false)
|
||||
|
||||
const submit = () => {
|
||||
if ($loading) return
|
||||
if ($uploading) return
|
||||
|
||||
const content = $editor.getText({blockSeparator: "\n"})
|
||||
const tags = [...getEditorTags($editor), tagRoom(GENERAL, url), PROTECTED]
|
||||
const content = $editor.getText({blockSeparator: "\n"}).trim()
|
||||
const tags = [...$editor.storage.nostr.getEditorTags(), tagRoom(GENERAL, url), PROTECTED]
|
||||
|
||||
if (!content.trim()) {
|
||||
if (!content) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please provide a message for your reply.",
|
||||
@@ -33,12 +34,11 @@
|
||||
onSubmit(publishComment({event, content, tags, relays: [url]}))
|
||||
}
|
||||
|
||||
let editor: Readable<Editor>
|
||||
|
||||
$: loading = $editor?.storage.fileUpload.loading
|
||||
let editor: ReturnType<typeof getEditor>
|
||||
let element: HTMLElement
|
||||
|
||||
onMount(() => {
|
||||
editor = createEditor(getEditorOptions({submit, getPubkeyHints, autofocus: !isMobile}))
|
||||
editor = getEditor({element, submit, uploading, autofocus: !isMobile})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -49,13 +49,13 @@
|
||||
class="card2 sticky bottom-2 z-feature mx-2 mt-4 bg-neutral">
|
||||
<div class="relative">
|
||||
<div class="note-editor flex-grow overflow-hidden">
|
||||
<EditorContent editor={$editor} />
|
||||
<div bind:this={element} />
|
||||
</div>
|
||||
<Button
|
||||
data-tip="Add an image"
|
||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||
on:click={$editor.commands.selectFiles}>
|
||||
{#if $loading}
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon="paperclip" size={3} />
|
||||
|
||||
@@ -42,9 +42,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex justify-end px-1 text-xs {$$props.class}">
|
||||
{#if isFailure && failure}
|
||||
{@const [url, {message, status}] = failure}
|
||||
{#if isFailure && failure}
|
||||
{@const [url, {message, status}] = failure}
|
||||
<div class="flex justify-end px-1 text-xs {$$props.class}">
|
||||
<Tippy
|
||||
class="flex items-center {$$props.class}"
|
||||
component={ThunkStatusDetail}
|
||||
@@ -55,7 +55,9 @@
|
||||
<span>Failed to send!</span>
|
||||
</span>
|
||||
</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="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
|
||||
<span class="opacity-50">Sending...</span>
|
||||
@@ -63,5 +65,5 @@
|
||||
<Button class="link" on:click={abort}>Cancel</Button>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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 {
|
||||
userFollows,
|
||||
deriveUserWotScore,
|
||||
deriveProfile,
|
||||
deriveHandleForPubkey,
|
||||
displayHandle,
|
||||
deriveProfileDisplay,
|
||||
} from "@welshman/app"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import WotScore from "@lib/components/WotScore.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
|
||||
export let value
|
||||
|
||||
const pubkey = value
|
||||
const profile = deriveProfile(pubkey)
|
||||
const profileDisplay = deriveProfileDisplay(pubkey)
|
||||
const handle = deriveHandleForPubkey(pubkey)
|
||||
const score = deriveUserWotScore(pubkey)
|
||||
@@ -24,11 +22,11 @@
|
||||
|
||||
<div class="flex max-w-full gap-3">
|
||||
<div class="py-1">
|
||||
<Avatar src={$profile?.picture} size={10} />
|
||||
<ProfileCircle {pubkey} />
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<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}
|
||||
</div>
|
||||
<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)
|
||||
},
|
||||
})
|
||||
+38
-17
@@ -1,15 +1,15 @@
|
||||
import {derived} from "svelte/store"
|
||||
import {synced} from "@welshman/store"
|
||||
import {synced, throttled} from "@welshman/store"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import {prop, sortBy, now} from "@welshman/lib"
|
||||
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {MESSAGE} from "@welshman/util"
|
||||
import {MESSAGE, COMMENT, getTagValue} from "@welshman/util"
|
||||
import {makeSpacePath, makeChatPath, makeThreadPath, makeRoomPath} from "@app/routes"
|
||||
import {
|
||||
THREAD_FILTER,
|
||||
COMMENT_FILTER,
|
||||
chats,
|
||||
getEventsForUrl,
|
||||
getUrlsForEvent,
|
||||
userRoomsByUrl,
|
||||
repositoryStore,
|
||||
} from "@app/state"
|
||||
@@ -25,17 +25,21 @@ export const setChecked = (key: string) => checked.update(state => ({...state, [
|
||||
// Derived notifications state
|
||||
|
||||
export const notifications = derived(
|
||||
[pubkey, checked, chats, userRoomsByUrl, repositoryStore],
|
||||
([$pubkey, $checked, $chats, $userRoomsByUrl, $repository]) => {
|
||||
const hasNotification = (path: string, events: TrustedEvent[]) => {
|
||||
const [latestEvent] = sortBy($e => -$e.created_at, events)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for (const [entryPath, ts] of Object.entries($checked)) {
|
||||
const isMatch = entryPath === "*" || entryPath.startsWith(path)
|
||||
const isMatch =
|
||||
entryPath === "*" ||
|
||||
entryPath.startsWith(path) ||
|
||||
(entryPath === "/chat/*" && path.startsWith("/chat/"))
|
||||
|
||||
if (isMatch && ts > latestEvent.created_at) {
|
||||
return false
|
||||
@@ -50,29 +54,46 @@ export const notifications = derived(
|
||||
for (const {pubkeys, messages} of $chats) {
|
||||
const chatPath = makeChatPath(pubkeys)
|
||||
|
||||
if (hasNotification(chatPath, messages)) {
|
||||
if (hasNotification(chatPath, messages[0])) {
|
||||
paths.add("/chat")
|
||||
paths.add(chatPath)
|
||||
}
|
||||
}
|
||||
|
||||
const allThreadEvents = $repository.query([THREAD_FILTER, COMMENT_FILTER])
|
||||
const allMessageEvents = $repository.query([{kinds: [MESSAGE]}])
|
||||
|
||||
for (const [url, rooms] of $userRoomsByUrl.entries()) {
|
||||
const spacePath = makeSpacePath(url)
|
||||
const threadPath = makeThreadPath(url)
|
||||
const threadFilters = [THREAD_FILTER, COMMENT_FILTER]
|
||||
const threadEvents = getEventsForUrl($repository, url, threadFilters)
|
||||
const threadEvents = allThreadEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||
|
||||
if (hasNotification(threadPath, threadEvents)) {
|
||||
if (hasNotification(threadPath, threadEvents[0])) {
|
||||
paths.add(spacePath)
|
||||
paths.add(threadPath)
|
||||
}
|
||||
|
||||
const commentsByThreadId = groupBy(
|
||||
e => getTagValue("E", e.tags),
|
||||
threadEvents.filter(spec({kind: COMMENT})),
|
||||
)
|
||||
|
||||
for (const [threadId, [comment]] of commentsByThreadId.entries()) {
|
||||
const threadItemPath = makeThreadPath(url, threadId)
|
||||
|
||||
if (hasNotification(threadItemPath, comment)) {
|
||||
paths.add(threadItemPath)
|
||||
}
|
||||
}
|
||||
|
||||
for (const room of rooms) {
|
||||
const roomPath = makeRoomPath(url, room)
|
||||
const roomFilters = [{kinds: [MESSAGE], "#h": [room]}]
|
||||
const roomEvents = getEventsForUrl($repository, url, roomFilters)
|
||||
const latestEvent = allMessageEvents.find(
|
||||
e =>
|
||||
$getUrlsForEvent(e.id).includes(url) && e.tags.find(t => t[0] === "h" && t[1] === room),
|
||||
)
|
||||
|
||||
if (hasNotification(roomPath, roomEvents)) {
|
||||
if (hasNotification(roomPath, latestEvent)) {
|
||||
paths.add(spacePath)
|
||||
paths.add(roomPath)
|
||||
}
|
||||
|
||||
+10
-24
@@ -1,20 +1,23 @@
|
||||
import {get} from "svelte/store"
|
||||
import {partition, assoc, now} from "@welshman/lib"
|
||||
import {MESSAGE, REACTION, DELETE, THREAD, COMMENT} from "@welshman/util"
|
||||
import {MESSAGE, THREAD, COMMENT} from "@welshman/util"
|
||||
import type {Subscription} from "@welshman/net"
|
||||
import type {AppSyncOpts} from "@welshman/app"
|
||||
import {subscribe, repository, load, pull, hasNegentropy} from "@welshman/app"
|
||||
import {userRoomsByUrl, LEGACY_MESSAGE, GENERAL, getEventsForUrl} from "@app/state"
|
||||
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 = getEventsForUrl(repository, url, filters)
|
||||
const events = allEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||
|
||||
if (events.length > 100) {
|
||||
filters = filters.map(assoc("since", events[10]!.created_at))
|
||||
@@ -29,7 +32,6 @@ export const pullConservatively = ({relays, filters}: AppSyncOpts) => {
|
||||
// Application requests
|
||||
|
||||
export const listenForNotifications = () => {
|
||||
const since = now()
|
||||
const subs: Subscription[] = []
|
||||
|
||||
for (const [url, rooms] of userRoomsByUrl.get()) {
|
||||
@@ -46,9 +48,9 @@ export const listenForNotifications = () => {
|
||||
subscribe({
|
||||
relays: [url],
|
||||
filters: [
|
||||
{kinds: [THREAD], since},
|
||||
{kinds: [COMMENT], "#K": [String(THREAD)], since},
|
||||
{kinds: [MESSAGE], "#h": Array.from(rooms), since},
|
||||
{kinds: [THREAD], since: now()},
|
||||
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
|
||||
...Array.from(rooms).map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
|
||||
],
|
||||
}),
|
||||
)
|
||||
@@ -60,19 +62,3 @@ export const listenForNotifications = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const listenForChannelMessages = (url: string, room: string) => {
|
||||
const since = now()
|
||||
const relays = [url]
|
||||
const kinds = [MESSAGE, REACTION, DELETE]
|
||||
const legacyRoom = room === GENERAL ? "general" : room
|
||||
|
||||
// Load legacy immediate so our request doesn't get rejected by nip29 relays
|
||||
load({relays, filters: [{kinds: [LEGACY_MESSAGE], "#~": [legacyRoom]}], delay: 0})
|
||||
|
||||
// Load historical state with negentropy if available
|
||||
pullConservatively({relays, filters: [{kinds, "#h": [room]}]})
|
||||
|
||||
// Listen for new messages
|
||||
return subscribe({relays, filters: [{kinds, "#h": [room], since}]})
|
||||
}
|
||||
|
||||
+1
-1
@@ -607,7 +607,7 @@ export const userSettingValues = withGetter(
|
||||
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(
|
||||
derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => {
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {onMount} from "svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
||||
export let src = ""
|
||||
export let size = 7
|
||||
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>
|
||||
|
||||
{#if src}
|
||||
<div
|
||||
class={cx($$props.class, "shrink-0 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 || ""}`} />
|
||||
{:else}
|
||||
<div
|
||||
class={cx($$props.class, "center !flex rounded-full")}
|
||||
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}
|
||||
<div
|
||||
bind:this={element}
|
||||
class="{$$props.class} relative !flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-cover bg-center"
|
||||
style="width: {rem}px; height: {rem}px; min-width: {rem}px; background-image: url({src}); {$$props.style ||
|
||||
''}">
|
||||
<Icon {icon} class={src ? "hidden" : ""} size={Math.round(size * 0.8)} />
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
import {type Instance} from "tippy.js"
|
||||
import {identity} from "@welshman/lib"
|
||||
import {createSearch} from "@welshman/app"
|
||||
import {Suggestions, SuggestionString} from "@welshman/editor"
|
||||
import Icon from "@lib/components/Icon.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 options: string[]
|
||||
|
||||
@@ -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,42 +0,0 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import type {NodeViewProps} from "@tiptap/core"
|
||||
import {NodeViewWrapper} from "svelte-tiptap"
|
||||
import {always, nthEq} from "@welshman/lib"
|
||||
import {parse, renderAsText, ParsedType} from "@welshman/content"
|
||||
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 renderLink = (href: string, display: string) => display
|
||||
|
||||
const displayEvent = (e: TrustedEvent) => {
|
||||
const content = e?.tags.find(nthEq(0, "alt"))?.[1] || e?.content || ""
|
||||
|
||||
if (content.length < 1) {
|
||||
return fromNostrURI(nevent || naddr).slice(0, 16) + "..."
|
||||
}
|
||||
|
||||
const parsed = parse({...e, content})
|
||||
|
||||
// Try stripping entities, but if we get nothing back go ahead and show them
|
||||
const renderEntity = always(parsed.find(p => p.type === ParsedType.Text) ? "" : "[quote]")
|
||||
|
||||
return renderAsText(parsed, {renderLink, renderEntity})
|
||||
}
|
||||
|
||||
$: ({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,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,5 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let value
|
||||
</script>
|
||||
|
||||
{value}
|
||||
@@ -1,99 +0,0 @@
|
||||
<svelte:options accessors />
|
||||
|
||||
<script lang="ts">
|
||||
import {fly, slide} from "svelte/transition"
|
||||
import {clamp, throttle} from "@welshman/lib"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {theme} from "@app/theme"
|
||||
|
||||
export let term
|
||||
export let search
|
||||
export let select
|
||||
export let component
|
||||
export let loading = false
|
||||
export let allowCreate = false
|
||||
|
||||
let index = 0
|
||||
let element: Element
|
||||
let items: string[] = []
|
||||
|
||||
$: populateItems(term)
|
||||
|
||||
const populateItems = throttle(300, term => {
|
||||
items = $search.searchValues(term).slice(0, 5)
|
||||
})
|
||||
|
||||
const setIndex = (newIndex: number, block: any) => {
|
||||
index = clamp([0, items.length - 1], newIndex)
|
||||
}
|
||||
|
||||
export const onKeyDown = (e: any) => {
|
||||
if (["Enter", "Tab"].includes(e.code)) {
|
||||
const value = items[index]
|
||||
|
||||
if (value) {
|
||||
select(value)
|
||||
return true
|
||||
} else if (term && allowCreate) {
|
||||
select(term)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (e.code === "Space" && term && allowCreate) {
|
||||
select(term)
|
||||
return true
|
||||
}
|
||||
|
||||
if (e.code === "ArrowUp") {
|
||||
setIndex(index - 1, "start")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (e.code === "ArrowDown") {
|
||||
setIndex(index + 1, "start")
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if term}
|
||||
<div
|
||||
data-theme={$theme}
|
||||
bind:this={element}
|
||||
transition:fly|local={{duration: 200}}
|
||||
class="mt-2 max-h-[350px] overflow-y-auto overflow-x-hidden shadow-xl {$$props.class} bg-alt"
|
||||
style={$$props.style}>
|
||||
{#if term && allowCreate && !items.includes(term)}
|
||||
<button
|
||||
class="white-space-nowrap block w-full min-w-0 cursor-pointer overflow-x-hidden text-ellipsis px-4 py-2 text-left transition-all hover:brightness-150"
|
||||
on:mousedown|preventDefault
|
||||
on:click|preventDefault={() => select(term)}>
|
||||
Use "<svelte:component this={component} value={term} />"
|
||||
</button>
|
||||
{/if}
|
||||
{#each items as value, i (value)}
|
||||
<button
|
||||
class="white-space-nowrap block flex w-full min-w-0 cursor-pointer items-center overflow-x-hidden text-ellipsis px-4 py-2 text-left transition-all hover:brightness-150"
|
||||
on:mousedown|preventDefault
|
||||
on:click|preventDefault={() => select(value)}>
|
||||
{#if index === i}
|
||||
<div transition:slide|local={{axis: "x"}} class="flex items-center pr-2">
|
||||
<Icon icon="alt-arrow-right" />
|
||||
</div>
|
||||
{/if}
|
||||
<svelte:component this={component} {value} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if loading}
|
||||
<div transition:slide|local class="flex gap-2 px-4 py-2">
|
||||
<div>
|
||||
<i class="fa fa-circle-notch fa-spin" />
|
||||
</div>
|
||||
Loading more options...
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -1,104 +0,0 @@
|
||||
import type {SvelteComponent, ComponentType} from "svelte"
|
||||
import type {Readable} from "svelte/store"
|
||||
import tippy, {type Instance} from "tippy.js"
|
||||
import type {Editor} from "@tiptap/core"
|
||||
import {PluginKey} from "@tiptap/pm/state"
|
||||
import Suggestion from "@tiptap/suggestion"
|
||||
import type {Search} from "@welshman/app"
|
||||
|
||||
export type SuggestionsOptions = {
|
||||
char: string
|
||||
name: string
|
||||
editor: Editor
|
||||
search: Readable<Search<any, any>>
|
||||
select: (value: any, props: any) => void
|
||||
allowCreate?: boolean
|
||||
suggestionComponent: ComponentType
|
||||
suggestionsComponent: ComponentType
|
||||
}
|
||||
|
||||
export const createSuggestions = (options: SuggestionsOptions) =>
|
||||
Suggestion({
|
||||
char: options.char,
|
||||
editor: options.editor,
|
||||
pluginKey: new PluginKey(`suggest-${options.name}`),
|
||||
command: ({editor, range, props}) => {
|
||||
// increase range.to by one when the next node is of type "text"
|
||||
// and starts with a space character
|
||||
const nodeAfter = editor.view.state.selection.$to.nodeAfter
|
||||
const overrideSpace = nodeAfter?.text?.startsWith(" ")
|
||||
|
||||
if (overrideSpace) {
|
||||
range.to += 1
|
||||
}
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(range, [
|
||||
{type: options.name, attrs: props},
|
||||
{type: "text", text: " "},
|
||||
])
|
||||
.run()
|
||||
|
||||
window.getSelection()?.collapseToEnd()
|
||||
},
|
||||
allow: ({state, range}) => {
|
||||
const $from = state.doc.resolve(range.from)
|
||||
const type = state.schema.nodes[options.name]
|
||||
|
||||
return !!$from.parent.type.contentMatch.matchType(type)
|
||||
},
|
||||
render: () => {
|
||||
let popover: Instance[]
|
||||
let suggestions: SvelteComponent
|
||||
|
||||
const mapProps = (props: any) => ({
|
||||
term: props.query,
|
||||
search: options.search,
|
||||
allowCreate: options.allowCreate,
|
||||
component: options.suggestionComponent,
|
||||
select: (value: string) => options.select(value, props),
|
||||
})
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
const target = document.createElement("div")
|
||||
|
||||
popover = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
appendTo: document.querySelector("dialog[open]") || document.body,
|
||||
content: target,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
placement: "bottom-start",
|
||||
})
|
||||
|
||||
suggestions = new options.suggestionsComponent({target, props: mapProps(props)})
|
||||
},
|
||||
onUpdate: props => {
|
||||
suggestions.$set(mapProps(props))
|
||||
|
||||
if (props.clientRect) {
|
||||
popover[0].setProps({
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
})
|
||||
}
|
||||
},
|
||||
onKeyDown: props => {
|
||||
if (props.event.key === "Escape") {
|
||||
popover[0].hide()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return Boolean(suggestions.onKeyDown?.(props.event))
|
||||
},
|
||||
onExit: () => {
|
||||
popover[0].destroy()
|
||||
suggestions.$destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,143 +0,0 @@
|
||||
import {nprofileEncode} from "nostr-tools/nip19"
|
||||
import {SvelteNodeViewRenderer} from "svelte-tiptap"
|
||||
import Placeholder from "@tiptap/extension-placeholder"
|
||||
import Code from "@tiptap/extension-code"
|
||||
import CodeBlock from "@tiptap/extension-code-block"
|
||||
import Document from "@tiptap/extension-document"
|
||||
import Dropcursor from "@tiptap/extension-dropcursor"
|
||||
import Gapcursor from "@tiptap/extension-gapcursor"
|
||||
import History from "@tiptap/extension-history"
|
||||
import Paragraph from "@tiptap/extension-paragraph"
|
||||
import Text from "@tiptap/extension-text"
|
||||
import HardBreakExtension from "@tiptap/extension-hard-break"
|
||||
import {
|
||||
Bolt11Extension,
|
||||
NProfileExtension,
|
||||
NEventExtension,
|
||||
NAddrExtension,
|
||||
ImageExtension,
|
||||
VideoExtension,
|
||||
TagExtension,
|
||||
} from "nostr-editor"
|
||||
import type {StampedEvent} from "@welshman/util"
|
||||
import {signer, profileSearch} from "@welshman/app"
|
||||
import {FileUploadExtension} from "./FileUpload"
|
||||
import {createSuggestions} from "./Suggestions"
|
||||
import EditMention from "./EditMention.svelte"
|
||||
import EditEvent from "./EditEvent.svelte"
|
||||
import EditImage from "./EditImage.svelte"
|
||||
import EditBolt11 from "./EditBolt11.svelte"
|
||||
import EditVideo from "./EditVideo.svelte"
|
||||
import Suggestions from "./Suggestions.svelte"
|
||||
import SuggestionProfile from "./SuggestionProfile.svelte"
|
||||
import {asInline} from "./util"
|
||||
import {getSetting} from "@app/state"
|
||||
|
||||
export {
|
||||
createSuggestions,
|
||||
EditMention,
|
||||
EditEvent,
|
||||
EditImage,
|
||||
EditBolt11,
|
||||
EditVideo,
|
||||
Suggestions,
|
||||
SuggestionProfile,
|
||||
}
|
||||
export * from "./util"
|
||||
|
||||
type UploadType = "nip96" | "blossom"
|
||||
|
||||
type EditorOptions = {
|
||||
submit: () => void
|
||||
getPubkeyHints: (pubkey: string) => string[]
|
||||
submitOnEnter?: boolean
|
||||
placeholder?: string
|
||||
autofocus?: boolean
|
||||
uploadType?: UploadType
|
||||
defaultUploadUrl?: string
|
||||
}
|
||||
|
||||
export const getEditorOptions = ({
|
||||
submit,
|
||||
getPubkeyHints,
|
||||
submitOnEnter,
|
||||
placeholder = "",
|
||||
autofocus = false,
|
||||
uploadType = getSetting("upload_type") as UploadType,
|
||||
defaultUploadUrl = getSetting("upload_type") == "nip96"
|
||||
? (getSetting("nip96_urls") as string[])[0] || "https://nostr.build"
|
||||
: (getSetting("blossom_urls") as string[])[0] || "https://cdn.satellite.earth",
|
||||
}: EditorOptions) => ({
|
||||
autofocus,
|
||||
content: "",
|
||||
extensions: [
|
||||
Code,
|
||||
CodeBlock,
|
||||
Document,
|
||||
Dropcursor,
|
||||
Gapcursor,
|
||||
History,
|
||||
Paragraph,
|
||||
Text,
|
||||
TagExtension,
|
||||
Placeholder.configure({placeholder}),
|
||||
HardBreakExtension.extend({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Shift-Enter": () => this.editor.commands.setHardBreak(),
|
||||
"Mod-Enter": () => {
|
||||
if (this.editor.getText().trim()) {
|
||||
submit()
|
||||
return true
|
||||
}
|
||||
|
||||
return this.editor.commands.setHardBreak()
|
||||
},
|
||||
Enter: () => {
|
||||
if (submitOnEnter && this.editor.getText().trim()) {
|
||||
submit()
|
||||
return true
|
||||
}
|
||||
|
||||
return this.editor.commands.setHardBreak()
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditBolt11)})),
|
||||
NProfileExtension.extend({
|
||||
addNodeView: () => SvelteNodeViewRenderer(EditMention),
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
createSuggestions({
|
||||
char: "@",
|
||||
name: "nprofile",
|
||||
editor: this.editor,
|
||||
search: profileSearch,
|
||||
select: (pubkey: string, props: any) => {
|
||||
const relays = getPubkeyHints(pubkey)
|
||||
const nprofile = nprofileEncode({pubkey, relays})
|
||||
|
||||
return props.command({pubkey, nprofile, relays})
|
||||
},
|
||||
suggestionComponent: SuggestionProfile,
|
||||
suggestionsComponent: Suggestions,
|
||||
}),
|
||||
]
|
||||
},
|
||||
}),
|
||||
NEventExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
|
||||
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
|
||||
ImageExtension.extend(
|
||||
asInline({addNodeView: () => SvelteNodeViewRenderer(EditImage)}),
|
||||
).configure({defaultUploadUrl, defaultUploadType: uploadType}),
|
||||
VideoExtension.extend(
|
||||
asInline({addNodeView: () => SvelteNodeViewRenderer(EditVideo)}),
|
||||
).configure({defaultUploadUrl, defaultUploadType: uploadType}),
|
||||
FileUploadExtension.configure({
|
||||
immediateUpload: true,
|
||||
allowedMimeTypes: ["image/*", "video/*"],
|
||||
sign: (event: StampedEvent) => signer.get()!.sign(event),
|
||||
}),
|
||||
],
|
||||
})
|
||||
@@ -1,94 +0,0 @@
|
||||
import type {JSONContent, PasteRuleMatch, InputRuleMatch} from "@tiptap/core"
|
||||
import {Editor} from "@tiptap/core"
|
||||
import {ctx} from "@welshman/lib"
|
||||
import {Address} from "@welshman/util"
|
||||
|
||||
export const asInline = (extend: Record<string, any>) => ({
|
||||
inline: true,
|
||||
group: "inline",
|
||||
...extend,
|
||||
})
|
||||
|
||||
export const createInputRuleMatch = <T extends Record<string, unknown>>(
|
||||
match: RegExpMatchArray,
|
||||
data: T,
|
||||
): InputRuleMatch => ({index: match.index!, text: match[0], match, data})
|
||||
|
||||
export const createPasteRuleMatch = <T extends Record<string, unknown>>(
|
||||
match: RegExpMatchArray,
|
||||
data: T,
|
||||
): PasteRuleMatch => ({index: match.index!, text: match[0], match, data})
|
||||
|
||||
export const findNodes = (type: string, json: JSONContent) => {
|
||||
const results: JSONContent[] = []
|
||||
|
||||
for (const node of json.content || []) {
|
||||
if (node.type === type) {
|
||||
results.push(node)
|
||||
}
|
||||
|
||||
for (const result of findNodes(type, node)) {
|
||||
results.push(result)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
export const findMarks = (type: string, json: JSONContent) => {
|
||||
const results: JSONContent[] = []
|
||||
|
||||
for (const node of json.content || []) {
|
||||
for (const mark of node.marks || []) {
|
||||
if (mark.type === type) {
|
||||
results.push(mark)
|
||||
}
|
||||
}
|
||||
|
||||
for (const result of findMarks(type, node)) {
|
||||
results.push(result)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
export const getEditorTags = (editor: Editor) => {
|
||||
const json = editor.getJSON()
|
||||
|
||||
const topicTags = findMarks("tag", json).map(({attrs}: any) => [
|
||||
"t",
|
||||
attrs.tag.replace(/^#/, "").toLowerCase(),
|
||||
])
|
||||
|
||||
const naddrTags = findNodes("naddr", json).map(
|
||||
({attrs: {kind, pubkey, identifier, relays = []}}: any) => {
|
||||
const address = new Address(kind, pubkey, identifier).toString()
|
||||
|
||||
return ["q", address, ctx.app.router.FromRelays(relays).getUrl(), pubkey]
|
||||
},
|
||||
)
|
||||
|
||||
const neventTags = findNodes("nevent", json).map(({attrs: {id, author, relays = []}}: any) => [
|
||||
"q",
|
||||
id,
|
||||
ctx.app.router.FromRelays(relays).getUrl(),
|
||||
author || "",
|
||||
])
|
||||
|
||||
const mentionTags = findNodes("nprofile", json).map(({attrs: {pubkey, relays = []}}: any) => [
|
||||
"p",
|
||||
pubkey,
|
||||
ctx.app.router.FromRelays(relays).getUrl(),
|
||||
"",
|
||||
])
|
||||
|
||||
const imetaTags = findNodes("image", json).map(({attrs: {src, sha256}}: any) => [
|
||||
"imeta",
|
||||
`url ${src}`,
|
||||
`x ${sha256}`,
|
||||
`ox ${sha256}`,
|
||||
])
|
||||
|
||||
return [...topicTags, ...naddrTags, ...neventTags, ...mentionTags, ...imetaTags]
|
||||
}
|
||||
+61
-68
@@ -5,10 +5,12 @@
|
||||
import {get, derived} from "svelte/store"
|
||||
import {dev} from "$app/environment"
|
||||
import {bytesToHex, hexToBytes} from "@noble/hashes/utils"
|
||||
import {identity, sleep, take, sortBy, ago, now, HOUR, WEEK, Worker} from "@welshman/lib"
|
||||
import {identity, sleep, take, sortBy, ago, now, HOUR, WEEK, MONTH, Worker} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {
|
||||
MESSAGE,
|
||||
PROFILE,
|
||||
DELETE,
|
||||
REACTION,
|
||||
ZAP_RESPONSE,
|
||||
FOLLOWS,
|
||||
@@ -18,7 +20,6 @@
|
||||
getPubkeyTagValues,
|
||||
getListTags,
|
||||
} from "@welshman/util"
|
||||
import {throttled} from "@welshman/store"
|
||||
import {
|
||||
relays,
|
||||
handles,
|
||||
@@ -80,80 +81,72 @@
|
||||
...notifications,
|
||||
})
|
||||
|
||||
const getScoreEvent = () => {
|
||||
const ALWAYS_KEEP = Infinity
|
||||
const NEVER_KEEP = 0
|
||||
|
||||
const reactionKinds = [REACTION, ZAP_RESPONSE]
|
||||
const metaKinds = [PROFILE, FOLLOWS, RELAYS, INBOX_RELAYS]
|
||||
const $sessionKeys = new Set(Object.keys(app.sessions.get()))
|
||||
const $userFollows = new Set(getPubkeyTagValues(getListTags(get(app.userFollows))))
|
||||
const $maxWot = get(app.maxWot)
|
||||
|
||||
return (e: TrustedEvent) => {
|
||||
const isFollowing = $userFollows.has(e.pubkey)
|
||||
|
||||
// No need to keep a record of everyone who follows the current user
|
||||
if (e.kind === FOLLOWS && !isFollowing) return NEVER_KEEP
|
||||
|
||||
// Always keep stuff by or tagging a signed in user
|
||||
if ($sessionKeys.has(e.pubkey)) return ALWAYS_KEEP
|
||||
if (e.tags.some(t => $sessionKeys.has(t[1]))) return ALWAYS_KEEP
|
||||
|
||||
// Get rid of irrelevant messages, reactions, and likes
|
||||
if (e.wrap || e.kind === 4 || e.kind === WRAP) return NEVER_KEEP
|
||||
if (reactionKinds.includes(e.kind)) return NEVER_KEEP
|
||||
|
||||
// If the user follows this person, use max wot score
|
||||
let score = isFollowing ? $maxWot : app.getUserWotScore(e.pubkey)
|
||||
|
||||
// Inflate the score for profiles/relays/follows to avoid redundant fetches
|
||||
// Demote non-metadata type events, and introduce recency bias
|
||||
score *= metaKinds.includes(e.kind) ? 2 : e.created_at / now()
|
||||
|
||||
return score
|
||||
}
|
||||
}
|
||||
|
||||
const migrateFreshness = (data: {key: string; value: number}[]) => {
|
||||
const cutoff = ago(HOUR)
|
||||
|
||||
return data.filter(({value}) => value > cutoff)
|
||||
}
|
||||
|
||||
const migratePlaintext = (data: {key: string; value: number}[]) => data.slice(0, 10_000)
|
||||
|
||||
const migrateEvents = (events: TrustedEvent[]) => {
|
||||
if (events.length < 50_000) {
|
||||
return events
|
||||
}
|
||||
|
||||
const scoreEvent = getScoreEvent()
|
||||
|
||||
return take(
|
||||
30_000,
|
||||
sortBy(e => -scoreEvent(e), events),
|
||||
)
|
||||
}
|
||||
|
||||
if (!db) {
|
||||
setupTracking()
|
||||
setupAnalytics()
|
||||
|
||||
ready = initStorage("flotilla", 4, {
|
||||
relays: {keyPath: "url", store: throttled(3000, relays)},
|
||||
handles: {keyPath: "nip05", store: throttled(3000, handles)},
|
||||
ready = initStorage("flotilla", 5, {
|
||||
relays: storageAdapters.fromCollectionStore("url", relays, {throttle: 3000}),
|
||||
handles: storageAdapters.fromCollectionStore("nip05", handles, {throttle: 3000}),
|
||||
freshness: storageAdapters.fromObjectStore(freshness, {
|
||||
throttle: 3000,
|
||||
migrate: migrateFreshness,
|
||||
migrate: (data: {key: string; value: number}[]) => {
|
||||
const cutoff = ago(HOUR)
|
||||
|
||||
return data.filter(({value}) => value > cutoff)
|
||||
},
|
||||
}),
|
||||
plaintext: storageAdapters.fromObjectStore(plaintext, {
|
||||
throttle: 3000,
|
||||
migrate: migratePlaintext,
|
||||
migrate: (data: {key: string; value: number}[]) => data.slice(0, 10_000),
|
||||
}),
|
||||
events: storageAdapters.fromRepositoryAndTracker(repository, tracker, {
|
||||
events2: storageAdapters.fromRepositoryAndTracker(repository, tracker, {
|
||||
throttle: 3000,
|
||||
migrate: migrateEvents,
|
||||
migrate: (events: TrustedEvent[]) => {
|
||||
if (events.length < 15_000) {
|
||||
return events
|
||||
}
|
||||
|
||||
const NEVER_KEEP = 0
|
||||
const ALWAYS_KEEP = Infinity
|
||||
const reactionKinds = [REACTION, ZAP_RESPONSE, DELETE]
|
||||
const metaKinds = [PROFILE, FOLLOWS, RELAYS, INBOX_RELAYS]
|
||||
const $sessionKeys = new Set(Object.keys(app.sessions.get()))
|
||||
const $userFollows = new Set(getPubkeyTagValues(getListTags(get(app.userFollows))))
|
||||
const $maxWot = get(app.maxWot)
|
||||
|
||||
const scoreEvent = (e: TrustedEvent) => {
|
||||
const isFollowing = $userFollows.has(e.pubkey)
|
||||
|
||||
// No need to keep a record of everyone who follows the current user
|
||||
if (e.kind === FOLLOWS && !isFollowing) return NEVER_KEEP
|
||||
|
||||
// Drop room messages after a month, re-load on demand
|
||||
if (e.kind === MESSAGE && e.created_at < ago(MONTH)) return NEVER_KEEP
|
||||
|
||||
// Always keep stuff by or tagging a signed in user
|
||||
if ($sessionKeys.has(e.pubkey)) return ALWAYS_KEEP
|
||||
if (e.tags.some(t => $sessionKeys.has(t[1]))) return ALWAYS_KEEP
|
||||
|
||||
// Get rid of irrelevant messages, reactions, and likes
|
||||
if (e.wrap || e.kind === 4 || e.kind === WRAP) return NEVER_KEEP
|
||||
if (reactionKinds.includes(e.kind)) return NEVER_KEEP
|
||||
|
||||
// If the user follows this person, use max wot score
|
||||
let score = isFollowing ? $maxWot : app.getUserWotScore(e.pubkey)
|
||||
|
||||
// Inflate the score for profiles/relays/follows to avoid redundant fetches
|
||||
// Demote non-metadata type events, and introduce recency bias
|
||||
score *= metaKinds.includes(e.kind) ? 2 : e.created_at / now()
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
return take(
|
||||
10_000,
|
||||
sortBy(e => -scoreEvent(e), events),
|
||||
)
|
||||
},
|
||||
}),
|
||||
}).then(() => sleep(300))
|
||||
|
||||
@@ -195,11 +188,11 @@
|
||||
// Listen for chats, populate chat-based notifications
|
||||
let chatsSub: any
|
||||
|
||||
derived([pubkey, userInboxRelaySelections], identity).subscribe(
|
||||
([$pubkey, $userInboxRelaySelections]) => {
|
||||
derived([pubkey, canDecrypt, userInboxRelaySelections], identity).subscribe(
|
||||
([$pubkey, $canDecrypt, $userInboxRelaySelections]) => {
|
||||
chatsSub?.close()
|
||||
|
||||
if ($pubkey) {
|
||||
if ($pubkey && $canDecrypt) {
|
||||
chatsSub = subscribe({
|
||||
filters: [
|
||||
{kinds: [WRAP], "#p": [$pubkey], since: ago(WEEK, 2)},
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import ContentSearch from "@lib/components/ContentSearch.svelte"
|
||||
import ChatItem from "@app/components/ChatItem.svelte"
|
||||
import ChatStart from "@app/components/ChatStart.svelte"
|
||||
import ChatMenuMobile from "@app/components/ChatMenuMobile.svelte"
|
||||
import {chatSearch} from "@app/state"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {setChecked} from "@app/notifications"
|
||||
@@ -14,6 +15,8 @@
|
||||
|
||||
const startChat = () => pushModal(ChatStart)
|
||||
|
||||
const openMenu = () => pushModal(ChatMenuMobile)
|
||||
|
||||
$: chats = $chatSearch.searchOptions(term).filter(c => c.pubkeys.length > 1)
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -41,8 +44,8 @@
|
||||
<Icon icon="magnifer" />
|
||||
<input bind:value={term} class="grow" type="text" placeholder="Search for conversations..." />
|
||||
</label>
|
||||
<Button class="btn btn-primary" on:click={startChat}>
|
||||
<Icon icon="add-circle" />
|
||||
<Button class="btn btn-primary" on:click={openMenu}>
|
||||
<Icon icon="menu-dots" />
|
||||
</Button>
|
||||
</div>
|
||||
<div slot="content" class="col-2">
|
||||
|
||||
@@ -40,14 +40,14 @@
|
||||
<CardButton>
|
||||
<div slot="icon"><Icon icon="compass" size={7} /></div>
|
||||
<div slot="title">Browse the network</div>
|
||||
<div slot="info">Find your people on the nostr network</div>
|
||||
<div slot="info">Find your people on the nostr network.</div>
|
||||
</CardButton>
|
||||
</Link>
|
||||
<Button on:click={startChat}>
|
||||
<CardButton>
|
||||
<div slot="icon"><Icon icon="chat-round" size={7} /></div>
|
||||
<div slot="title">Start a conversation</div>
|
||||
<div slot="info">Use nostr's encrypted group chats to stay in touch</div>
|
||||
<div slot="info">Use nostr's encrypted group chats to stay in touch.</div>
|
||||
</CardButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
import Chat from "@app/components/Chat.svelte"
|
||||
|
||||
$: id = $pubkey!
|
||||
</script>
|
||||
|
||||
<Page>
|
||||
<Chat {id}>
|
||||
<p slot="info" class="px-4">
|
||||
This is a place for your notes. Everything you write here is encrypted and stored on the nostr
|
||||
network.
|
||||
</p>
|
||||
</Chat>
|
||||
</Page>
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {now} from "@welshman/lib"
|
||||
import {ago, WEEK} from "@welshman/lib"
|
||||
import {GROUPS, MESSAGE, DELETE} from "@welshman/util"
|
||||
import {subscribe} from "@welshman/app"
|
||||
import {DELETE, REACTION, GROUPS} from "@welshman/util"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||
import MenuSpace from "@app/components/MenuSpace.svelte"
|
||||
@@ -12,11 +12,14 @@
|
||||
import {pushModal} from "@app/modal"
|
||||
import {setChecked} from "@app/notifications"
|
||||
import {checkRelayConnection, checkRelayAuth, checkRelayAccess} from "@app/commands"
|
||||
import {decodeRelay} from "@app/state"
|
||||
import {decodeRelay, userRoomsByUrl, THREAD_FILTER, COMMENT_FILTER} from "@app/state"
|
||||
import {pullConservatively} from "@app/requests"
|
||||
import {notifications} from "@app/notifications"
|
||||
|
||||
const url = decodeRelay($page.params.relay)
|
||||
|
||||
const rooms = Array.from($userRoomsByUrl.get(url) || [])
|
||||
|
||||
const checkConnection = async () => {
|
||||
const connectionError = await checkRelayConnection(url)
|
||||
|
||||
@@ -43,11 +46,27 @@
|
||||
onMount(() => {
|
||||
checkConnection()
|
||||
|
||||
const sub = subscribe({
|
||||
relays: [url],
|
||||
filters: [{kinds: [GROUPS]}, {kinds: [DELETE, REACTION], since: now()}],
|
||||
const relays = [url]
|
||||
const since = ago(WEEK)
|
||||
|
||||
// Load all groups for this space to populate navigation
|
||||
pullConservatively({relays, filters: [{kinds: [GROUPS]}]})
|
||||
|
||||
// Load threads and comments
|
||||
pullConservatively({
|
||||
relays,
|
||||
filters: [
|
||||
{...THREAD_FILTER, since},
|
||||
{...COMMENT_FILTER, since},
|
||||
],
|
||||
})
|
||||
|
||||
// Load recent messages for user rooms to help with a quick page transition
|
||||
pullConservatively({relays, filters: rooms.map(r => ({kinds: [MESSAGE], "#h": [r], since}))})
|
||||
|
||||
// Listen for deletes that would apply to messages we already have, and new groups
|
||||
const sub = subscribe({relays, filters: [{kinds: [DELETE, GROUPS], since}]})
|
||||
|
||||
return () => {
|
||||
sub.close()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {page} from "$app/stores"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
import {fade} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
@@ -79,11 +80,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="min-w-0">
|
||||
<h2 class="ellipsize whitespace-nowrap text-xl">
|
||||
<RelayName {url} />
|
||||
</h2>
|
||||
<p class="text-sm opacity-75">{url}</p>
|
||||
<p class="ellipsize text-sm opacity-75">{displayRelayUrl(url)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<RelayDescription {url} />
|
||||
@@ -157,9 +158,9 @@
|
||||
</div>
|
||||
</Link>
|
||||
{/each}
|
||||
<Button on:click={addRoom} class="btn btn-neutral">
|
||||
<Button on:click={addRoom} class="btn btn-neutral whitespace-nowrap">
|
||||
<Icon icon="add-circle" />
|
||||
Create Room
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
{#if pubkey}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<script lang="ts">
|
||||
import {nip19} from "nostr-tools"
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {onMount} from "svelte"
|
||||
import {derived} from "svelte/store"
|
||||
import type {Editor} from "svelte-tiptap"
|
||||
import {page} from "$app/stores"
|
||||
import {sleep, ctx} from "@welshman/lib"
|
||||
import {sleep, now, ctx} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {throttled} from "@welshman/store"
|
||||
import {createEvent, MESSAGE} from "@welshman/util"
|
||||
import type {Subscription} from "@welshman/net"
|
||||
import {formatTimestampAsDate, publishThunk, deriveRelay} from "@welshman/app"
|
||||
import {feedsFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
||||
import {createEvent, MESSAGE, DELETE, REACTION} from "@welshman/util"
|
||||
import {
|
||||
formatTimestampAsDate,
|
||||
createFeedController,
|
||||
subscribe,
|
||||
publishThunk,
|
||||
deriveRelay,
|
||||
} from "@welshman/app"
|
||||
import {slide} from "@lib/transition"
|
||||
import {createScroller, type Scroller} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
@@ -18,6 +22,7 @@
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import type {getEditor} from "@app/editor"
|
||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import ChannelMessage from "@app/components/ChannelMessage.svelte"
|
||||
@@ -34,7 +39,6 @@
|
||||
} from "@app/state"
|
||||
import {setChecked} from "@app/notifications"
|
||||
import {nip29, addRoomMembership, removeRoomMembership, getThunkError} from "@app/commands"
|
||||
import {listenForChannelMessages} from "@app/requests"
|
||||
import {PROTECTED, hasNip29} from "@app/state"
|
||||
import {popKey} from "@app/implicit"
|
||||
import {pushToast} from "@app/toast"
|
||||
@@ -44,6 +48,8 @@
|
||||
const url = decodeRelay($page.params.relay)
|
||||
const relay = deriveRelay(url)
|
||||
const legacyRoom = room === GENERAL ? "general" : room
|
||||
const feeds = feedsFromFilter({kinds: [MESSAGE], "#h": [room]})
|
||||
|
||||
const events = throttled(
|
||||
300,
|
||||
deriveEventsForUrl(url, [
|
||||
@@ -52,6 +58,14 @@
|
||||
]),
|
||||
)
|
||||
|
||||
const ctrl = createFeedController({
|
||||
useWindowing: true,
|
||||
feed: makeIntersectionFeed(makeRelayFeed(url), ...feeds),
|
||||
onExhausted: () => {
|
||||
loading = false
|
||||
},
|
||||
})
|
||||
|
||||
const assertEvent = (e: any) => e as TrustedEvent
|
||||
|
||||
const joinRoom = async () => {
|
||||
@@ -90,12 +104,12 @@
|
||||
delay: $userSettingValues.send_delay,
|
||||
})
|
||||
|
||||
let limit = 30
|
||||
let loading = sleep(5000)
|
||||
let sub: Subscription
|
||||
let limit = 100
|
||||
let loading = true
|
||||
let unmounted = false
|
||||
let element: HTMLElement
|
||||
let scroller: Scroller
|
||||
let editor: Readable<Editor>
|
||||
let editor: ReturnType<typeof getEditor>
|
||||
|
||||
const elements = derived(events, $events => {
|
||||
const $elements = []
|
||||
@@ -122,30 +136,39 @@
|
||||
previousPubkey = pubkey
|
||||
}
|
||||
|
||||
return $elements.reverse().slice(0, limit)
|
||||
return $elements.reverse()
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
// Sveltekiiit
|
||||
await sleep(100)
|
||||
onMount(() => {
|
||||
// Element is frequently not defined. I don't know why
|
||||
sleep(1000).then(() => {
|
||||
if (!unmounted) {
|
||||
scroller = createScroller({
|
||||
element,
|
||||
delay: 300,
|
||||
threshold: 10_000,
|
||||
onScroll: () => {
|
||||
limit += 100
|
||||
|
||||
scroller = createScroller({
|
||||
element,
|
||||
delay: 300,
|
||||
threshold: 3000,
|
||||
onScroll: () => {
|
||||
limit += 30
|
||||
loading = sleep(5000)
|
||||
},
|
||||
if ($events.length - limit < 100) {
|
||||
ctrl.load(200)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
sub = listenForChannelMessages(url, room)
|
||||
})
|
||||
const sub = subscribe({
|
||||
relays: [url],
|
||||
filters: [{kinds: [DELETE, REACTION, MESSAGE], "#h": [room], since: now()}],
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
setChecked($page.url.pathname)
|
||||
scroller?.stop()
|
||||
sub?.close()
|
||||
return () => {
|
||||
unmounted = true
|
||||
setChecked($page.url.pathname)
|
||||
scroller?.stop()
|
||||
sub.close()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -177,7 +200,7 @@
|
||||
<div
|
||||
class="scroll-container -mt-2 flex flex-grow flex-col-reverse overflow-auto py-2"
|
||||
bind:this={element}>
|
||||
{#each $elements as { type, id, value, showPubkey } (id)}
|
||||
{#each $elements.slice(0, limit) as { type, id, value, showPubkey } (id)}
|
||||
{#if type === "date"}
|
||||
<Divider>{value}</Divider>
|
||||
{:else}
|
||||
@@ -187,11 +210,11 @@
|
||||
{/if}
|
||||
{/each}
|
||||
<p class="flex h-10 items-center justify-center py-20">
|
||||
{#await loading}
|
||||
{#if loading}
|
||||
<Spinner loading>Looking for messages...</Spinner>
|
||||
{:then}
|
||||
{:else}
|
||||
<Spinner>End of message history</Spinner>
|
||||
{/await}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<ChannelCompose bind:editor {content} {onSubmit} />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import {sortBy, min, nthEq, sleep} from "@welshman/lib"
|
||||
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||
import {throttled} from "@welshman/store"
|
||||
import {feedsFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
||||
import {feedFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
||||
import {createFeedController, userMutes} from "@welshman/app"
|
||||
import {createScroller, type Scroller} from "@lib/html"
|
||||
import {fly} from "@lib/transition"
|
||||
@@ -21,7 +21,7 @@
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const url = decodeRelay($page.params.relay)
|
||||
const feeds = feedsFromFilters([THREAD_FILTER, COMMENT_FILTER])
|
||||
const feed = feedFromFilters([THREAD_FILTER, COMMENT_FILTER])
|
||||
const threads = deriveEventsForUrl(url, [THREAD_FILTER])
|
||||
const comments = deriveEventsForUrl(url, [COMMENT_FILTER])
|
||||
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
const ctrl = createFeedController({
|
||||
useWindowing: true,
|
||||
feed: makeIntersectionFeed(makeRelayFeed(url), feeds),
|
||||
feed: makeIntersectionFeed(makeRelayFeed(url), feed),
|
||||
onExhausted: () => {
|
||||
loading = false
|
||||
},
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<h1>Privacy Policy for Flotilla</h1>
|
||||
<p>At Flotilla, accessible from https://flotilla.social, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by Flotilla and how we use it.</p>
|
||||
<p>This Privacy Policy applies only to our online activities and is valid for users of our application with regards to the information that they shared and/or collect in Flotilla. This policy is not applicable to any information collected offline or via channels other than this application.</p>
|
||||
<h2>Consent</h2>
|
||||
<p>By using our application, you hereby consent to our Privacy Policy and agree to its terms.</p>
|
||||
<h2>Information we collect</h2>
|
||||
<p>The personal information that you are asked to provide, and the reasons why you are asked to provide it, will be made clear to you at the point we ask you to provide your personal information.</p>
|
||||
<p>If you contact us directly, we may receive additional information about you such as your name, email address, phone number, the contents of the message and/or attachments you may send us, and any other information you may choose to provide.</p>
|
||||
<p>When you register for an Account, we may ask for your contact information, including items such as name, company name, address, email address, and telephone number.</p>
|
||||
<h2>How we use your information</h2>
|
||||
<p>We use the information we collect in various ways, including to:</p>
|
||||
<ul>
|
||||
<li>Provide, operate, and maintain our application</li>
|
||||
<li>Improve, personalize, and expand our application</li>
|
||||
<li>Understand and analyze how you use our application</li>
|
||||
<li>Develop new products, services, features, and functionality</li>
|
||||
<li>Communicate with you, either directly or through one of our partners, including for customer application, to provide you with updates and other information relating to the application, and for marketing and promotional purposes</li>
|
||||
<li>Send you emails</li>
|
||||
<li>Find and prevent fraud</li>
|
||||
</ul>
|
||||
<h2>Log Files</h2>
|
||||
<p>Flotilla follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' movement within the application, and gathering demographic information.</p>
|
||||
<h2>Cookies and Web Beacons</h2>
|
||||
<p>Like any other website, Flotilla uses "cookies". These cookies are used to store information including visitors' preferences, and the pages on the website that the visitor accessed or visited. The information is used to optimize the users' experience by customizing your web page content based on visitors' browser type and/or other information.</p>
|
||||
<h2>Third Party Privacy Policies</h2>
|
||||
<p>Flotilla's Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options.</p>
|
||||
<p>You can choose to disable cookies through your individual browser options. To know more detailed information about cookie management with specific web browsers, it can be found at the browsers' respective websites.</p>
|
||||
<h2>CCPA Privacy Rights (Do Not Sell My Personal Information)</h2>
|
||||
<p>Under the CCPA, among other rights, California consumers have the right to:</p>
|
||||
<p>Request that a business that collects a consumer's personal data disclose the categories and specific pieces of personal data that a business has collected about consumers.</p>
|
||||
<p>Request that a business delete any personal data about the consumer that a business has collected.</p>
|
||||
<p>Request that a business that sells a consumer's personal data, not sell the consumer's personal data.</p>
|
||||
<p>If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us.</p>
|
||||
<h2>GDPR Data Protection Rights</h2>
|
||||
<p>We would like to make sure you are fully aware of all of your data protection rights. Every user is entitled to the following:</p>
|
||||
<p>The right to access – You have the right to request copies of your personal data. We may charge you a small fee for this service.</p>
|
||||
<p>The right to rectification – You have the right to request that we correct any information you believe is inaccurate. You also have the right to request that we complete the information you believe is incomplete.</p>
|
||||
<p>The right to erasure – You have the right to request that we erase your personal data, under certain conditions.</p>
|
||||
<p>The right to restrict processing – You have the right to request that we restrict the processing of your personal data, under certain conditions.</p>
|
||||
<p>The right to object to processing – You have the right to object to our processing of your personal data, under certain conditions.</p>
|
||||
<p>The right to data portability – You have the right to request that we transfer the data that we have collected to another organization, or directly to you, under certain conditions.</p>
|
||||
<p>If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us.</p>
|
||||
<h2>Children's Information</h2>
|
||||
<p>Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, and/or monitor and guide their online activity.</p>
|
||||
<p>Flotilla does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.</p>
|
||||
<h2>Changes to This Privacy Policy</h2>
|
||||
<p>We may update our Privacy Policy from time to time. Thus, we advise you to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately, after they are posted on this page.</p>
|
||||
<h2>Contact Us</h2>
|
||||
<p>If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.</p>
|
||||
@@ -0,0 +1,82 @@
|
||||
<h1>Terms and Conditions</h1>
|
||||
<p>Last updated: January 15, 2025</p>
|
||||
<p>Please read these terms and conditions carefully before using Our Service.</p>
|
||||
<h1>Interpretation and Definitions</h1>
|
||||
<h2>Interpretation</h2>
|
||||
<p>The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.</p>
|
||||
<h2>Definitions</h2>
|
||||
<p>For the purposes of these Terms and Conditions:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p><strong>Affiliate</strong> means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Country</strong> refers to: Idaho, United States</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Company</strong> (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to Coracle Social LLC.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Device</strong> means any device that can access the Service such as a computer, a cellphone or a digital tablet.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Service</strong> refers to Flotilla, accessible from <a href="https://flotilla.social" rel="external nofollow noopener" target="_blank">https://flotilla.social</a>, or via native applications released by Company.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Terms and Conditions</strong> (also referred as "Terms") mean these Terms and Conditions that form the entire agreement between You and the Company regarding the use of the Service.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Third-party Social Media Service</strong> means any services or content (including data, information, products or services) provided by a third-party that may be displayed, included or made available by the Service.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>You</strong> means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h1>Acknowledgment</h1>
|
||||
<p>These are the Terms and Conditions governing the use of this Service and the agreement that operates between You and the Company. These Terms and Conditions set out the rights and obligations of all users regarding the use of the Service.</p>
|
||||
<p>Your access to and use of the Service is conditioned on Your acceptance of and compliance with these Terms and Conditions. These Terms and Conditions apply to all visitors, users and others who access or use the Service.</p>
|
||||
<p>By accessing or using the Service You agree to be bound by these Terms and Conditions. If You disagree with any part of these Terms and Conditions then You may not access the Service.</p>
|
||||
<p>You represent that you are over the age of 18. The Company does not permit those under 18 to use the Service.</p>
|
||||
<p>Your access to and use of the Service is also conditioned on Your acceptance of and compliance with the Privacy Policy of the Company. Our Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your personal information when You use the Service and tells You about Your privacy rights and how the law protects You. Please read Our Privacy Policy carefully before using Our Service.</p>
|
||||
<h1>Links to Other Websites</h1>
|
||||
<p>Our Service may contain links and embedded content to third-party web sites, users, or services that are not owned or controlled by the Company.</p>
|
||||
<p>The Company has no control over, and assumes no responsibility for, the content, privacy policies, or practices of any third party web sites, users, or services. You further acknowledge and agree that the Company shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with the use of or reliance on any such content, goods or services available on or through any such web sites, users, or services.</p>
|
||||
<p>We strongly advise You to read the terms and conditions and privacy policies of any third-party web sites or services that You visit.</p>
|
||||
<h1>User-Generated Content</h1>
|
||||
<p>Content you produce may be shared with other users. Pornography, graphic violence, and other illegal content is prohibited. You also agree to abide by the terms imposed by third-party administrators whose servers you access through Flotilla.</p>
|
||||
<h1>Termination</h1>
|
||||
<p>We may terminate or suspend Your access immediately, without prior notice or liability, for any reason whatsoever, including without limitation if You breach these Terms and Conditions.</p>
|
||||
<p>Upon termination, Your right to use the Service will cease immediately.</p>
|
||||
<h1>Limitation of Liability</h1>
|
||||
<p>Notwithstanding any damages that You might incur, the entire liability of the Company and any of its suppliers under any provision of this Terms and Your exclusive remedy for all of the foregoing shall be limited to the amount actually paid by You through the Service or 100 USD if You haven't purchased anything through the Service.</p>
|
||||
<p>To the maximum extent permitted by applicable law, in no event shall the Company or its suppliers be liable for any special, incidental, indirect, or consequential damages whatsoever (including, but not limited to, damages for loss of profits, loss of data or other information, for business interruption, for personal injury, loss of privacy arising out of or in any way related to the use of or inability to use the Service, third-party software and/or third-party hardware used with the Service, or otherwise in connection with any provision of this Terms), even if the Company or any supplier has been advised of the possibility of such damages and even if the remedy fails of its essential purpose.</p>
|
||||
<p>Some states do not allow the exclusion of implied warranties or limitation of liability for incidental or consequential damages, which means that some of the above limitations may not apply. In these states, each party's liability will be limited to the greatest extent permitted by law.</p>
|
||||
<h1>"AS IS" and "AS AVAILABLE" Disclaimer</h1>
|
||||
<p>The Service is provided to You "AS IS" and "AS AVAILABLE" and with all faults and defects without warranty of any kind. To the maximum extent permitted under applicable law, the Company, on its own behalf and on behalf of its Affiliates and its and their respective licensors and service providers, expressly disclaims all warranties, whether express, implied, statutory or otherwise, with respect to the Service, including all implied warranties of merchantability, fitness for a particular purpose, title and non-infringement, and warranties that may arise out of course of dealing, course of performance, usage or trade practice. Without limitation to the foregoing, the Company provides no warranty or undertaking, and makes no representation of any kind that the Service will meet Your requirements, achieve any intended results, be compatible or work with any other software, applications, systems or services, operate without interruption, meet any performance or reliability standards or be error free or that any errors or defects can or will be corrected.</p>
|
||||
<p>Without limiting the foregoing, neither the Company nor any of the company's provider makes any representation or warranty of any kind, express or implied: (i) as to the operation or availability of the Service, or the information, content, and materials or products included thereon; (ii) that the Service will be uninterrupted or error-free; (iii) as to the accuracy, reliability, or currency of any information or content provided through the Service; or (iv) that the Service, its servers, the content, or e-mails sent from or on behalf of the Company are free of viruses, scripts, trojan horses, worms, malware, timebombs or other harmful components.</p>
|
||||
<p>Some jurisdictions do not allow the exclusion of certain types of warranties or limitations on applicable statutory rights of a consumer, so some or all of the above exclusions and limitations may not apply to You. But in such a case the exclusions and limitations set forth in this section shall be applied to the greatest extent enforceable under applicable law.</p>
|
||||
<h1>Governing Law</h1>
|
||||
<p>The laws of the Country, excluding its conflicts of law rules, shall govern this Terms and Your use of the Service. Your use of the Service may also be subject to other local, state, national, or international laws.</p>
|
||||
<h1>Disputes Resolution</h1>
|
||||
<p>If You have any concern or dispute about the Service, You agree to first try to resolve the dispute informally by contacting the Company.</p>
|
||||
<h1>For European Union (EU) Users</h1>
|
||||
<p>If You are a European Union consumer, you will benefit from any mandatory provisions of the law of the country in which you are resident in.</p>
|
||||
<h1>United States Legal Compliance</h1>
|
||||
<p>You represent and warrant that (i) You are not located in a country that is subject to the United States government embargo, or that has been designated by the United States government as a "terrorist supporting" country, and (ii) You are not listed on any United States government list of prohibited or restricted parties.</p>
|
||||
<h1>Severability and Waiver</h1>
|
||||
<h2>Severability</h2>
|
||||
<p>If any provision of these Terms is held to be unenforceable or invalid, such provision will be changed and interpreted to accomplish the objectives of such provision to the greatest extent possible under applicable law and the remaining provisions will continue in full force and effect.</p>
|
||||
<h2>Waiver</h2>
|
||||
<p>Except as provided herein, the failure to exercise a right or to require performance of an obligation under these Terms shall not effect a party's ability to exercise such right or require such performance at any time thereafter nor shall the waiver of a breach constitute a waiver of any subsequent breach.</p>
|
||||
<h1>Translation Interpretation</h1>
|
||||
<p>These Terms and Conditions may have been translated if We have made them available to You on our Service.
|
||||
You agree that the original English text shall prevail in the case of a dispute.
|
||||
</p>
|
||||
<h1>Changes to These Terms and Conditions</h1>
|
||||
<p>We reserve the right, at Our sole discretion, to modify or replace these Terms at any time. If a revision is material We will make reasonable efforts to provide at least 30 days' notice prior to any new terms taking effect. What constitutes a material change will be determined at Our sole discretion.</p>
|
||||
<p>By continuing to access or use Our Service after those revisions become effective, You agree to be bound by the revised terms. If You do not agree to the new terms, in whole or in part, please stop using the Service.</p>
|
||||
<h1>Contact Us</h1>
|
||||
<p>If you have any questions about these Terms and Conditions, You can contact us:</p>
|
||||
<ul>
|
||||
<li>By email: hello@coracle.social</li>
|
||||
</ul>
|
||||
Reference in New Issue
Block a user