diff --git a/.fdignore b/.fdignore new file mode 100644 index 00000000..d00d567c --- /dev/null +++ b/.fdignore @@ -0,0 +1 @@ +src/assets diff --git a/README.md b/README.md index c90081b9..20ab4dfb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # Flotilla A discord-like nostr client. WIP. + + +Figure out state management. Add fetched_at to all events. `fetch` batches and loads, `get` gets the value, `derive` returns a store. For optimization, create getters for everything that uses `get` a lot. diff --git a/package-lock.json b/package-lock.json index 193b7838..578f7d5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,15 +8,19 @@ "name": "flotilla", "version": "0.0.1", "dependencies": { + "@noble/curves": "^1.5.0", + "@noble/hashes": "^1.4.0", "@poppanator/sveltekit-svg": "^4.2.1", + "@types/throttle-debounce": "^5.0.2", "@welshman/lib": "^0.0.13", "@welshman/net": "^0.0.17", "@welshman/signer": "^0.0.2", "@welshman/store": "^0.0.1", "@welshman/util": "^0.0.24", "daisyui": "^4.12.10", - "nostr-login": "^1.5.2", - "prettier-plugin-tailwindcss": "^0.6.5" + "nostr-tools": "^2.7.2", + "prettier-plugin-tailwindcss": "^0.6.5", + "throttle-debounce": "^5.0.2" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.0.0", @@ -626,17 +630,17 @@ } }, "node_modules/@noble/ciphers": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz", - "integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.5.0.tgz", + "integrity": "sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==", "dependencies": { "@noble/hashes": "1.4.0" }, @@ -655,14 +659,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/secp256k1": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.1.0.tgz", - "integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw==", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -698,25 +694,6 @@ "node": ">= 8" } }, - "node_modules/@nostr-dev-kit/ndk": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.8.2.tgz", - "integrity": "sha512-+dOEyuYvO5/MoI5iTi8C5HifmvfeEvpybNesluVYyu+o+koFdfc+WSYH050V8+9KlOgx8nOZAaqXnHz0KY1gBA==", - "dependencies": { - "@noble/curves": "^1.4.0", - "@noble/hashes": "^1.3.1", - "@noble/secp256k1": "^2.0.0", - "@scure/base": "^1.1.1", - "debug": "^4.3.4", - "light-bolt11-decoder": "^3.0.0", - "node-fetch": "^3.3.1", - "nostr-tools": "^1.15.0", - "tseep": "^1.1.1", - "typescript-lru-cache": "^2.0.0", - "utf8-buffer": "^1.0.0", - "websocket-polyfill": "^0.0.3" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1444,82 +1421,6 @@ "nostr-tools": "^2.3.2" } }, - "node_modules/@welshman/util/node_modules/@noble/ciphers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", - "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@welshman/util/node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@welshman/util/node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@welshman/util/node_modules/@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@welshman/util/node_modules/@scure/base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, - "node_modules/@welshman/util/node_modules/nostr-tools": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.1.tgz", - "integrity": "sha512-4qAvlHSqBAA8lQMwRWE6dalSNdQT77Xut9lPiJZgEcb9RAlR69wR2+KVBAgnZVaabVYH7FJ7gOQXLw/jQBAYBg==", - "dependencies": { - "@noble/ciphers": "^0.5.1", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.1", - "@scure/base": "1.1.1", - "@scure/bip32": "1.3.1", - "@scure/bip39": "1.2.1" - }, - "optionalDependencies": { - "nostr-wasm": "v0.1.0" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -1765,6 +1666,8 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -2036,18 +1939,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", - "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/daisyui": { "version": "4.12.10", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.10.tgz", @@ -2066,18 +1957,11 @@ "url": "https://opencollective.com/daisyui" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2237,49 +2121,12 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", - "hasInstallScript": true, - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, "node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", "dev": true }, - "node_modules/es6-symbol": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", - "dependencies": { - "d": "^1.0.2", - "ext": "^1.7.0" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -2484,20 +2331,6 @@ "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", "dev": true }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/espree": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", @@ -2565,15 +2398,6 @@ "node": ">=0.10.0" } }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -2582,14 +2406,6 @@ "node": ">=0.8.x" } }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "dependencies": { - "type": "^2.7.2" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2650,28 +2466,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2747,17 +2541,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3053,11 +2836,6 @@ "@types/estree": "*" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3163,25 +2941,6 @@ "node": ">= 0.8.0" } }, - "node_modules/light-bolt11-decoder": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.1.1.tgz", - "integrity": "sha512-sLg/KCwYkgsHWkefWd6KqpCHrLFWWaXTOX3cf6yD2hAzL0SLpX+lFcaFK2spkjbgzG6hhijKfORDc9WoUHwX0A==", - "dependencies": { - "@scure/base": "1.1.1" - } - }, - "node_modules/light-bolt11-decoder/node_modules/@scure/base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -3336,7 +3095,8 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/mz": { "version": "2.7.0", @@ -3372,50 +3132,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-gyp-build": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "optional": true, + "peer": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -3446,28 +3168,21 @@ "node": ">=0.10.0" } }, - "node_modules/nostr-login": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/nostr-login/-/nostr-login-1.5.2.tgz", - "integrity": "sha512-iHifY5T6v49zUN4R8gydg+O/MCDCmtWVxcIfFiwDadmH2VkNafemTYNAOaUeQNjeZ9YQ3/pI0OVT5JYWLBMEXQ==", - "dependencies": { - "@nostr-dev-kit/ndk": "^2.3.1", - "nostr-tools": "^1.17.0", - "tseep": "^1.2.1" - } - }, "node_modules/nostr-tools": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz", - "integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==", + "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==", "dependencies": { - "@noble/ciphers": "0.2.0", - "@noble/curves": "1.1.0", + "@noble/ciphers": "^0.5.1", + "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1" }, + "optionalDependencies": { + "nostr-wasm": "v0.1.0" + }, "peerDependencies": { "typescript": ">=5.0.0" }, @@ -3478,11 +3193,22 @@ } }, "node_modules/nostr-tools/node_modules/@noble/curves": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "dependencies": { - "@noble/hashes": "1.3.1" + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -4890,21 +4616,6 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, - "node_modules/tseep": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tseep/-/tseep-1.2.2.tgz", - "integrity": "sha512-GgPFuNx+08UaYBYmJQmuI86ykYa2PUUtfXAYb4MLRHGunSCp8k9N+dbsR4PK1yk4/zV9q4e4PrNg8ymXqGYaYA==" - }, - "node_modules/tstl": { - "version": "2.5.16", - "resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.16.tgz", - "integrity": "sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw==" - }, - "node_modules/type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4917,14 +4628,6 @@ "node": ">= 0.8.0" } }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", @@ -4961,11 +4664,6 @@ } } }, - "node_modules/typescript-lru-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/typescript-lru-cache/-/typescript-lru-cache-2.0.0.tgz", - "integrity": "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==" - }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -5010,6 +4708,8 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -5017,14 +4717,6 @@ "node": ">=6.14.2" } }, - "node_modules/utf8-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/utf8-buffer/-/utf8-buffer-1.0.0.tgz", - "integrity": "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg==", - "engines": { - "node": ">=8" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5099,52 +4791,6 @@ } } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/websocket": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", - "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", - "dependencies": { - "bufferutil": "^4.0.1", - "debug": "^2.2.0", - "es5-ext": "^0.10.63", - "typedarray-to-buffer": "^3.1.5", - "utf-8-validate": "^5.0.2", - "yaeti": "^0.0.6" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/websocket-polyfill": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/websocket-polyfill/-/websocket-polyfill-0.0.3.tgz", - "integrity": "sha512-pF3kR8Uaoau78MpUmFfzbIRxXj9PeQrCuPepGE6JIsfsJ/o/iXr07Q2iQNzKSSblQJ0FiGWlS64N4pVSm+O3Dg==", - "dependencies": { - "tstl": "^2.0.7", - "websocket": "^1.0.28" - } - }, - "node_modules/websocket/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/websocket/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5289,14 +4935,6 @@ } } }, - "node_modules/yaeti": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", - "engines": { - "node": ">=0.10.32" - } - }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/package.json b/package.json index c56ee907..a3cac12d 100644 --- a/package.json +++ b/package.json @@ -34,14 +34,18 @@ }, "type": "module", "dependencies": { + "@noble/curves": "^1.5.0", + "@noble/hashes": "^1.4.0", "@poppanator/sveltekit-svg": "^4.2.1", + "@types/throttle-debounce": "^5.0.2", "@welshman/lib": "^0.0.13", "@welshman/net": "^0.0.17", "@welshman/signer": "^0.0.2", "@welshman/store": "^0.0.1", "@welshman/util": "^0.0.24", "daisyui": "^4.12.10", - "nostr-login": "^1.5.2", - "prettier-plugin-tailwindcss": "^0.6.5" + "nostr-tools": "^2.7.2", + "prettier-plugin-tailwindcss": "^0.6.5", + "throttle-debounce": "^5.0.2" } } diff --git a/src/app.css b/src/app.css index 0c915b0d..681d3bb4 100644 --- a/src/app.css +++ b/src/app.css @@ -89,3 +89,11 @@ .subheading { @apply text-xl text-stark-content text-center; } + +.link { + @apply text-primary underline cursor-pointer; +} + +.input input::placeholder { + opacity: 0.5; +} diff --git a/src/app/base.ts b/src/app/base.ts index 1db13fa4..81dab283 100644 --- a/src/app/base.ts +++ b/src/app/base.ts @@ -1,6 +1,15 @@ +import {derived} from "svelte/store" +import {memoize} from '@welshman/lib' import type {SignedEvent} from "@welshman/util" -import {Repository, Relay} from "@welshman/util" +import {Repository, createEvent, Relay} from "@welshman/util" +import {getter} from "@welshman/store" import {NetworkContext, Tracker} from "@welshman/net" +import type {ISigner} from "@welshman/signer" +import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from '@welshman/signer' +import {synced} from '@lib/util' +import type {Session} from "@app/types" + +export const INDEXER_RELAYS = ["wss://purplepag.es", "wss://relay.damus.io"] export const DUFFLEPUD_URL = "https://dufflepud.onrender.com" @@ -10,29 +19,55 @@ export const relay = new Relay(repository) export const tracker = new Tracker() +export const pk = synced('pk', null) + +export const sessions = synced>('sessions', {}) + +export const session = derived([pk, sessions], ([$pk, $sessions]) => $pk ? $sessions[$pk] : null) + +export const getSession = getter(session) + +export const makeSigner = memoize((session: Session) => { + switch (session?.method) { + case "extension": + return new Nip07Signer() + case "privkey": + return new Nip01Signer(session.secret!) + case "connect": + return new Nip46Signer(Nip46Broker.get(session.pubkey, session.secret!, session.handler!)) + default: + return null + } +}) + +export const signer = derived(session, makeSigner) + +export const getSigner = getter(signer) + const seenChallenges = new Set() Object.assign(NetworkContext, { onEvent: (url: string, event: SignedEvent) => tracker.track(event.id, url), isDeleted: (url: string, event: SignedEvent) => repository.isDeleted(event), - // onAuth: async (url, challenge) => { - // if (seenChallenges.has(challenge)) { - // return - // } + onAuth: async (url: string, challenge: string) => { + if (seenChallenges.has(challenge)) { + return + } - // seenChallenges.add(challenge) + seenChallenges.add(challenge) - // const event = await signer.get().signAsUser( - // createEvent(22242, { - // tags: [ - // ["relay", url], - // ["challenge", challenge], - // ], - // }), - // ) + const event = await getSigner()!.sign( + createEvent(22242, { + tags: [ + ["relay", url], + ["challenge", challenge], + ], + }), + ) - // NetworkContext.pool.get(url).send(["AUTH", event]) + NetworkContext.pool.get(url).send(["AUTH", event]) - // return event - // }, + return event + }, }) + diff --git a/src/app/commands.ts b/src/app/commands.ts index 2a2001a3..c24543c8 100644 --- a/src/app/commands.ts +++ b/src/app/commands.ts @@ -1,16 +1,175 @@ -import {batch, postJson} from "@welshman/lib" -import {normalizeRelayUrl} from "@welshman/util" -import {DUFFLEPUD_URL} from "@app/base" -import {relayInfo} from "@app/state" +import {get} from 'svelte/store' +import type {SignedEvent} from '@welshman/util' +import {batcher, uniq, now, postJson, assoc} from "@welshman/lib" +import {normalizeRelayUrl, PROFILE, FOLLOWS, MUTES, GROUP_META} from "@welshman/util" +import {subscribe} from "@welshman/net" +import type {RelayInfo, HandleInfo, Session} from "@app/types" +import {splitGroupId} from "@app/domain" +import {DUFFLEPUD_URL, INDEXER_RELAYS, repository, pk, sessions} from "@app/base" +import {relayInfo, handleInfo, groupsById, profilesByPubkey, mutesByPubkey} from "@app/state" -export const loadRelay = batch(1000, async (urls: string[]) => { - const data = await postJson(`${DUFFLEPUD_URL}/relay/info`, {urls}) +// Session + +export const addSession = (session: Session) => { + sessions.update(assoc(session.pubkey, session)) + pk.set(session.pubkey) +} + +// Handle info + +export const loadHandleInfo = batcher(800, async (handles: string[]) => { + const res = await postJson(`${DUFFLEPUD_URL}/handle/info`, {handles: uniq(handles)}) + const data: {handle: string, info: HandleInfo}[] = res?.data || [] + + handleInfo.update($handleInfo => { + for (const {handle, info} of data) { + $handleInfo.set(handle, {...info, fetched_at: now()}) + } + + return $handleInfo + }) + + return data.map(item => item.info) +}) + +export const getHandleInfo = (handle: string) => { + const info = get(handleInfo).get(handle) + + if (info?.fetched_at > now() - 3600) { + return info + } + + return loadHandleInfo(handle) +} + +// Relay info + +export const loadRelayInfo = batcher(800, async (urls: string[]) => { + const res = await postJson(`${DUFFLEPUD_URL}/relay/info`, {urls: uniq(urls)}) + const data: {url: string, info: RelayInfo}[] = res?.data || [] relayInfo.update($relayInfo => { for (const {url, info} of data) { - $relayInfo.set(normalizeRelayUrl(url), info) + $relayInfo.set(normalizeRelayUrl(url), {...info, fetched_at: now()}) } return $relayInfo }) + + return data.map(item => item.info) }) + +export const getRelayInfo = (url: string) => { + const info = get(relayInfo).get(url) + + if (info?.fetched_at > now() - 3600) { + return info + } + + return loadRelayInfo(url) +} + +// Group meta + +export const getGroup = (groupId: string) => { + const group = get(groupsById).get(groupId) + + if (group?.event.fetched_at > now() - 3600) { + return group + } + + const [url, nom] = splitGroupId(groupId) + + const sub = subscribe({ + relays: [url], + filters: [{kinds: [GROUP_META], '#d': [groupId]}], + closeOnEose: true, + }) + + sub.emitter.on('event', (url: string, e: SignedEvent) => { + e.fetched_at = now() + repository.publish(e) + console.log(e) + }) +} + +// Profile + +export const getProfile = (pubkey: string, relays = []) => { + const profile = get(profilesByPubkey).get(pubkey) + + if (profile?.event.fetched_at > now() - 3600) { + return profile + } + + return new Promise(resolve => { + const sub = subscribe({ + relays: [...relays, ...INDEXER_RELAYS], + filters: [{kinds: [PROFILE], authors: [pubkey]}], + closeOnEose: true, + }) + + sub.emitter.on('event', (url: string, e: SignedEvent) => { + e.fetched_at = now() + repository.publish(e) + console.log(e) + resolve(e) + }) + + sub.emitter.on('close', () => resolve(null)) + }) +} + +// Follows + +export const getFollows = (pubkey: string, relays = []) => { + const follows = get(followsByPubkey).get(pubkey) + + if (follows?.event.fetched_at > now() - 3600) { + return follows + } + + return new Promise(resolve => { + const sub = subscribe({ + relays: [...relays, ...INDEXER_RELAYS], + filters: [{kinds: [FOLLOWS], authors: [pubkey]}], + closeOnEose: true, + }) + + sub.emitter.on('event', (url: string, e: SignedEvent) => { + e.fetched_at = now() + repository.publish(e) + console.log(e) + resolve(e) + }) + + sub.emitter.on('close', () => resolve(null)) + }) +} + +// Mutes + +export const getMutes = (pubkey: string, relays = []) => { + const mutes = get(mutesByPubkey).get(pubkey) + + if (mutes?.event.fetched_at > now() - 3600) { + return mutes + } + + return new Promise(resolve => { + const sub = subscribe({ + relays: [...relays, ...INDEXER_RELAYS], + filters: [{kinds: [MUTES], authors: [pubkey]}], + closeOnEose: true, + }) + + sub.emitter.on('event', (url: string, e: SignedEvent) => { + e.fetched_at = now() + repository.publish(e) + console.log(e) + resolve(e) + }) + + sub.emitter.on('close', () => resolve(null)) + }) +} diff --git a/src/app/components/InfoNip29.svelte b/src/app/components/InfoNip29.svelte index a3bc3faf..78b1f600 100644 --- a/src/app/components/InfoNip29.svelte +++ b/src/app/components/InfoNip29.svelte @@ -1,11 +1,14 @@
-

What is a relay?

+
+

What is a relay?

+

Flotilla hosts spaces on the Nostr protocol. Nostr uses "relays" to host data, which are special-purpose servers that speak nostr's language. @@ -19,11 +22,11 @@

devrelay.highlighter.com - +
- +
diff --git a/src/app/components/InfoNostr.svelte b/src/app/components/InfoNostr.svelte new file mode 100644 index 00000000..a4edd4d1 --- /dev/null +++ b/src/app/components/InfoNostr.svelte @@ -0,0 +1,25 @@ + + +
+
+

What is Nostr?

+
+

+ Nostr is way to build social apps that talk to eachother. + Users own their social identity instead of renting it from a tech company, and can bring it with them from + app to app. +

+

+ This can be a little confusing when you're just getting started, but as long as you're using Flotilla, it + will work just like a normal app. When you're ready to start exploring nostr, visit your settings page to + learn more. +

+ +
diff --git a/src/app/components/Landing.svelte b/src/app/components/Landing.svelte new file mode 100644 index 00000000..73862630 --- /dev/null +++ b/src/app/components/Landing.svelte @@ -0,0 +1,23 @@ + + +
+
+

Welcome to Flotilla!

+

The chat app built for sovereign communities.

+
+ + If you've been here before, you know the drill. + + + Just a few questions and you'll be on your way. + +
diff --git a/src/app/components/LogIn.svelte b/src/app/components/LogIn.svelte new file mode 100644 index 00000000..b60340ec --- /dev/null +++ b/src/app/components/LogIn.svelte @@ -0,0 +1,99 @@ + + +
+
+

Log in with Nostr

+

+ Flotilla is built using the + , which allows you to own your social identity. +

+
+ +
+ + @{handler.domain} +
+
+
+ +
+ Need an account? + +
+
+
diff --git a/src/app/components/PrimaryNav.svelte b/src/app/components/PrimaryNav.svelte index 1a2be170..287a0cb4 100644 --- a/src/app/components/PrimaryNav.svelte +++ b/src/app/components/PrimaryNav.svelte @@ -10,32 +10,32 @@ import Icon from "@lib/components/Icon.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import SpaceAdd from '@app/components/SpaceAdd.svelte' - import {getGroupName, getGroupPicture, makeGroupId} from "@app/domain" - import {userGroupRelaysByNom, groupsById} from "@app/state" + import {makeGroupId} from "@app/domain" + import {session} from "@app/base" + import {userGroupRelaysByNom, groupsById, deriveProfile} from "@app/state" import {pushModal} from "@app/modal" export const addSpace = () => pushModal(SpaceAdd) export const browseSpaces = () => goto("/browse") + + const profile = deriveProfile($session?.pubkey)
- +
- +
{#each $userGroupRelaysByNom.entries() as [nom, relays] (nom)} - {@const event = $groupsById.get(makeGroupId(relays[0], nom))} - {@const name = getGroupName(event)} - + {@const group = $groupsById.get(makeGroupId(relays[0], nom))} +
- {name} + {group.name}
{/each} diff --git a/src/app/components/SignUp.svelte b/src/app/components/SignUp.svelte new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/app/components/SignUp.svelte @@ -0,0 +1 @@ + diff --git a/src/app/components/SpaceAdd.svelte b/src/app/components/SpaceAdd.svelte index 70e4bdce..cc82e42a 100644 --- a/src/app/components/SpaceAdd.svelte +++ b/src/app/components/SpaceAdd.svelte @@ -1,4 +1,5 @@
-

Add a Space

-

Spaces are places where communities come together to work, play, and hang out.

+
+

Add a Space

+

Spaces are places where communities come together to work, play, and hang out.

+
Just a few questions and you'll be on your way.

Have an invite?

- +
diff --git a/src/app/components/SpaceCreate.svelte b/src/app/components/SpaceCreate.svelte index c46fb8a1..7dbb9a0b 100644 --- a/src/app/components/SpaceCreate.svelte +++ b/src/app/components/SpaceCreate.svelte @@ -1,5 +1,6 @@ -
-

Customize your Space

-

- Give people a few details to go on. You can always change this later. -

+
+
+

Customize your Space

+

+ Give people a few details to go on. You can always change this later. +

+
@@ -40,19 +41,19 @@

This should be a NIP-29 compatible nostr relay where you'd like to host your space. - +

- - + +
-
+ diff --git a/src/app/components/SpaceJoin.svelte b/src/app/components/SpaceJoin.svelte index d019ffdb..d69fb439 100644 --- a/src/app/components/SpaceJoin.svelte +++ b/src/app/components/SpaceJoin.svelte @@ -1,45 +1,85 @@ -
-

Join a Space

-

- Enter an invite link below to join an existing space. -

+
+
+

Join a Space

+

+ Enter an invite link below to join an existing space. +

+

Invite Link*

Browse other spaces on the discover page.
- - + + +
-
+ diff --git a/src/app/components/Toast.svelte b/src/app/components/Toast.svelte index 68928207..d1078124 100644 --- a/src/app/components/Toast.svelte +++ b/src/app/components/Toast.svelte @@ -6,7 +6,7 @@ {#if $toast} {#key $toast.id}
- diff --git a/src/app/domain.ts b/src/app/domain.ts index 9130525d..63eb959c 100644 --- a/src/app/domain.ts +++ b/src/app/domain.ts @@ -7,6 +7,12 @@ export const GROUP_DELIMITER = `'` export const makeGroupId = (url: string, nom: string) => [stripProtocol(url), nom].join(GROUP_DELIMITER) +export const splitGroupId = (groupId: string) => { + const [url, nom] = groupId.split(GROUP_DELIMITER) + + return [normalizeRelayUrl(url), nom] +} + export const getGroupNom = (e: TrustedEvent) => getIdentifier(e)?.split(GROUP_DELIMITER)[1] export const getGroupUrl = (e: TrustedEvent) => { diff --git a/src/app/modal.ts b/src/app/modal.ts index af47d176..bdcd969a 100644 --- a/src/app/modal.ts +++ b/src/app/modal.ts @@ -1,7 +1,9 @@ import type {ComponentType} from "svelte" -import {randomId} from "@welshman/lib" +import {randomId, Emitter} from "@welshman/lib" import {goto} from "$app/navigation" +export const emitter = new Emitter() + export const modals = new Map() export const pushModal = (component: ComponentType, props: Record = {}) => { @@ -14,4 +16,7 @@ export const pushModal = (component: ComponentType, props: Record = return id } -export const clearModal = () => goto('#') +export const clearModal = () => { + goto('#') + emitter.emit('close') +} diff --git a/src/app/state.ts b/src/app/state.ts index 9577eb94..e10998e7 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -1,27 +1,66 @@ import {writable, derived} from "svelte/store" import {pushToMapKey, indexBy} from "@welshman/lib" -import {getIdentifier, GROUP_META, GROUPS, getGroupTagValues} from "@welshman/util" +import {getIdentifier, getPubkeyTagValues, GROUP_META, PROFILE, FOLLOWS, MUTES, GROUPS, getGroupTagValues} from "@welshman/util" import {deriveEvents} from "@welshman/store" -import {repository} from "@app/base" -import {getGroupUrl, GROUP_DELIMITER} from "@app/domain" - -export const pk = writable(null) - -export const sessions = writable(new Map()) - -export const session = derived([pk, sessions], ([$pk, $sessions]) => $sessions.get($pk)) +import {synced, parseJson} from '@lib/util' +import type {Session} from '@app/types' +import {repository, pk} from "@app/base" +import {getGroupNom, getGroupUrl, getGroupName, getGroupPicture, GROUP_DELIMITER} from "@app/domain" export const relayInfo = writable(new Map()) +export const handleInfo = writable(new Map()) + +export const profileEvents = deriveEvents(repository, { + filters: [{kinds: [PROFILE]}], +}) + +export const profiles = derived(profileEvents, $profileEvents => + $profileEvents.map(event => ({...parseJson(event.content), event})) +) + +export const profilesByPubkey = derived(profiles, $profiles => indexBy(profile => profile.event.pubkey, $profiles)) + +export const deriveProfile = (pubkey: string) => derived(profilesByPubkey, $m => $m.get(pubkey)) + +export const followEvents = deriveEvents(repository, { + filters: [{kinds: [FOLLOWS]}], +}) + +export const follows = derived(followEvents, $followEvents => + $followEvents.map(event => ({pubkeys: new Set(getPubkeyTagValues(event.tags)), event})) +) + +export const followsByPubkey = derived(follows, $follows => indexBy(follow => follow.event.pubkey, $follows)) + +export const muteEvents = deriveEvents(repository, { + filters: [{kinds: [MUTES]}], +}) + +export const mutes = derived(muteEvents, $muteEvents => + $muteEvents.map(event => ({pubkeys: new Set(getPubkeyTagValues(event.tags)), event})) +) + +export const mutesByPubkey = derived(mutes, $mutes => indexBy(mute => mute.event.pubkey, $mutes)) + export const groupEvents = deriveEvents(repository, { filters: [{kinds: [GROUP_META]}], }) export const groups = derived([relayInfo, groupEvents], ([$relayInfo, $groupEvents]) => - $groupEvents.filter(e => $relayInfo.get(getGroupUrl(e))?.pubkey === e.pubkey), + $groupEvents + .map(event => ({ + event, + id: getIdentifier(event), + nom: getGroupNom(event), + url: getGroupUrl(event), + name: getGroupName(event), + picture: getGroupPicture(event), + })) + .filter(group => $relayInfo.get(group.url)?.pubkey === group.event.pubkey) ) -export const groupsById = derived(groups, $groups => indexBy(getIdentifier, $groups)) +export const groupsById = derived(groups, $groups => indexBy(group => group.id, $groups)) export const groupsEvents = deriveEvents(repository, { filters: [{kinds: [GROUPS]}], diff --git a/src/app/toast.ts b/src/app/toast.ts index 50fea98e..4ef32423 100644 --- a/src/app/toast.ts +++ b/src/app/toast.ts @@ -2,25 +2,24 @@ import {writable} from "svelte/store" import {randomId} from "@welshman/lib" import {copyToClipboard} from '@lib/html' -export type Toast = { - id: string +export type ToastParams = { message: string - options: ToastOptions + timeout?: number + theme?: "error" } -export type ToastOptions = { - timeout?: number +export type Toast = ToastParams & { + id: string } export const toast = writable(null) -export const pushToast = ( - {message = "", id = randomId()}: Partial, - options: ToastOptions = {}, -) => { - toast.set({id, message, options}) +export const pushToast = (params: ToastParams) => { + const id = randomId() - setTimeout(() => popToast(id), options.timeout || 5000) + toast.set({id, ...params}) + + setTimeout(() => popToast(id), params.timeout || 5000) return id } diff --git a/src/app/types.ts b/src/app/types.ts new file mode 100644 index 00000000..1958e9b4 --- /dev/null +++ b/src/app/types.ts @@ -0,0 +1,20 @@ +import type {Nip46Handler} from "@welshman/signer" + +export type Session = { + method: string + pubkey: string + secret?: string + handler?: Nip46Handler +} + +export type RelayInfo = { + fetched_at: number +} + +export type HandleInfo = { + pubkey: string + nip05: string + nip46: string[] + relays: string[] + fetched_at: number +} diff --git a/src/assets/icons/Login 2.svg b/src/assets/icons/Login 2.svg new file mode 100644 index 00000000..0cddf0d5 --- /dev/null +++ b/src/assets/icons/Login 2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/Login.svg b/src/assets/icons/Login.svg new file mode 100644 index 00000000..01f12ef8 --- /dev/null +++ b/src/assets/icons/Login.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/User Rounded.svg b/src/assets/icons/User Rounded.svg new file mode 100644 index 00000000..1bd56cbb --- /dev/null +++ b/src/assets/icons/User Rounded.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte new file mode 100644 index 00000000..ad17ede5 --- /dev/null +++ b/src/lib/components/Button.svelte @@ -0,0 +1,3 @@ + diff --git a/src/lib/components/CardButton.svelte b/src/lib/components/CardButton.svelte index b75ac071..7f2e783b 100644 --- a/src/lib/components/CardButton.svelte +++ b/src/lib/components/CardButton.svelte @@ -1,12 +1,13 @@ - + diff --git a/src/lib/components/Icon.svelte b/src/lib/components/Icon.svelte index 267fd074..e645963d 100644 --- a/src/lib/components/Icon.svelte +++ b/src/lib/components/Icon.svelte @@ -23,11 +23,14 @@ import HomeSmile from "@assets/icons/Home Smile.svg?dataurl" import InfoCircle from "@assets/icons/Info Circle.svg?dataurl" import LinkRound from "@assets/icons/Link Round.svg?dataurl" + import Login from "@assets/icons/Login.svg?dataurl" + import Login2 from "@assets/icons/Login 2.svg?dataurl" import Plain from "@assets/icons/Plain.svg?dataurl" import RemoteControllerMinimalistic from "@assets/icons/Remote Controller Minimalistic.svg?dataurl" import Settings from "@assets/icons/Settings.svg?dataurl" import UFO3 from "@assets/icons/UFO 3.svg?dataurl" import UserHeart from "@assets/icons/User Heart.svg?dataurl" + import UserRounded from "@assets/icons/User Rounded.svg?dataurl" import WiFiRouterRound from "@assets/icons/Wi-Fi Router Round.svg?dataurl" export let icon @@ -51,11 +54,14 @@ "home-smile": HomeSmile, "info-circle": InfoCircle, "link-round": LinkRound, + "login": Login, + "login-2": Login2, plain: Plain, 'remote-controller-minimalistic': RemoteControllerMinimalistic, settings: Settings, "ufo-3": UFO3, "user-heart": UserHeart, + "user-rounded": UserRounded, "wifi-router-round": WiFiRouterRound, }) @@ -65,5 +71,5 @@
+ class={$$props.class} + style="mask-image: url({data}); width: {px}px; height: {px}px; min-width: {px}px; min-height: {px}px; background-color: currentcolor;" /> diff --git a/src/lib/components/ModalBox.svelte b/src/lib/components/ModalBox.svelte index d9da9d0f..4bce3106 100644 --- a/src/lib/components/ModalBox.svelte +++ b/src/lib/components/ModalBox.svelte @@ -1,8 +1,12 @@ diff --git a/src/lib/components/PrimaryNavItem.svelte b/src/lib/components/PrimaryNavItem.svelte index 20aaf931..48323d56 100644 --- a/src/lib/components/PrimaryNavItem.svelte +++ b/src/lib/components/PrimaryNavItem.svelte @@ -1,11 +1,13 @@ - + diff --git a/src/lib/util.ts b/src/lib/util.ts new file mode 100644 index 00000000..22be6d28 --- /dev/null +++ b/src/lib/util.ts @@ -0,0 +1,31 @@ +import {throttle} from 'throttle-debounce' +import {browser} from '$app/environment' +import {writable} from 'svelte/store' + +export const parseJson = (json: string) => { + if (!json) return null + + try { + return JSON.parse(json) + } catch (e) { + return null + } +} + +export const getJson = (k: string) => + browser ? parseJson(localStorage.getItem(k) || "") : null + +export const setJson = (k: string, v: any) => { + if (browser) { + localStorage.setItem(k, JSON.stringify(v)) + } +} + +export const synced = (key: string, defaultValue: T, delay = 300) => { + const init = getJson(key) + const store = writable(init === null ? defaultValue : init) + + store.subscribe(throttle(delay, (value: T) => setJson(key, value))) + + return store +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 25d5f887..6b785030 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,31 +5,22 @@ import {fly} from "@lib/transition" import ModalBox from "@lib/components/ModalBox.svelte" import Toast from "@app/components/Toast.svelte" + import Landing from "@app/components/Landing.svelte" import PrimaryNav from "@app/components/PrimaryNav.svelte" import SecondaryNav from "@app/components/SecondaryNav.svelte" import {modals, clearModal} from "@app/modal" - - const login = async () => { - const nl = await import("nostr-login") - - nl.init({ - noBanner: true, - title: "Welcome to Flotilla!", - description: "Log in with your Nostr account or sign up to join.", - methods: ["connect", "extension", "local"], - onAuth(npub: string) { - console.log(npub) - }, - }) - - nl.launch() - } + import {session} from "@app/base" let dialog: HTMLDialogElement - $: modal = $page.url.hash.slice(1) + $: modalId = $page.url.hash.slice(1) + $: modal = modals.get(modalId) $: { + if (!modal && !$session) { + modal = {component: Landing} + } + if (modal) { dialog?.showModal() } else { @@ -57,13 +48,15 @@ {#if modal} {#key modal} - + {/key} {/if} -