Merge pull request #91 from luminous-devs/feat/ui-patch

v1.2.6
This commit is contained in:
Ren Amamiya 2023-09-28 08:28:34 +07:00 committed by GitHub
commit 0e6fc65b08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 3532 additions and 2448 deletions

View File

@ -18,49 +18,52 @@
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
},
"dependencies": {
"@dnd-kit/core": "^6.0.8",
"@getalby/sdk": "^2.4.0",
"@nostr-dev-kit/ndk": "^1.2.1",
"@nostr-dev-kit/ndk": "^1.3.0",
"@nostr-fetch/adapter-ndk": "^0.12.2",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-tooltip": "^1.0.6",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^4.35.3",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.4.0",
"@tiptap/extension-image": "^2.1.10",
"@tiptap/extension-mention": "^2.1.10",
"@tiptap/extension-placeholder": "^2.1.10",
"@tiptap/pm": "^2.1.10",
"@tiptap/react": "^2.1.10",
"@tiptap/starter-kit": "^2.1.10",
"@tiptap/suggestion": "^2.1.10",
"@tiptap/extension-image": "^2.1.11",
"@tiptap/extension-mention": "^2.1.11",
"@tiptap/extension-placeholder": "^2.1.11",
"@tiptap/pm": "^2.1.11",
"@tiptap/react": "^2.1.11",
"@tiptap/starter-kit": "^2.1.11",
"@tiptap/suggestion": "^2.1.11",
"dayjs": "^1.11.10",
"destr": "^2.0.1",
"get-urls": "^12.1.0",
"html-to-text": "^9.0.5",
"light-bolt11-decoder": "^3.0.0",
"lru-cache": "^10.0.1",
"minidenticons": "^4.2.0",
"nostr-fetch": "^0.13.0",
"nostr-tools": "^1.15.0",
"nostr-tools": "^1.16.0",
"qrcode.react": "^3.1.0",
"re-resizable": "^6.9.11",
"react": "^18.2.0",
"react-currency-input-field": "^3.6.11",
"react-dom": "^18.2.0",
"react-hook-form": "^7.46.1",
"react-hook-form": "^7.46.2",
"react-markdown": "^8.0.7",
"react-player": "^2.13.0",
"react-router-dom": "^6.16.0",
"react-textarea-autosize": "^8.5.3",
"react-virtuoso": "^4.6.0",
"reactflow": "^11.9.2",
"remark-gfm": "^3.0.1",
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql#v1",
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1",
"tauri-plugin-stronghold-api": "github:tauri-apps/tauri-plugin-stronghold#v1",
"tauri-plugin-upload-api": "github:tauri-apps/tauri-plugin-upload#v1",
"tippy.js": "^6.3.7",
"virtua": "^0.9.1",
"zustand": "^4.4.1"
},
"devDependencies": {
@ -68,19 +71,19 @@
"@tauri-apps/cli": "^1.4.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/html-to-text": "^9.0.2",
"@types/node": "^20.6.2",
"@types/react": "^18.2.22",
"@types/react-dom": "^18.2.7",
"@types/youtube-player": "^5.5.7",
"@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/parser": "^6.7.2",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"@types/node": "^20.7.1",
"@types/react": "^18.2.23",
"@types/react-dom": "^18.2.8",
"@types/youtube-player": "^5.5.8",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",
"@vitejs/plugin-react-swc": "^3.4.0",
"autoprefixer": "^10.4.16",
"clsx": "^2.0.0",
"cross-env": "^7.0.3",
"csstype": "^3.1.2",
"encoding": "^0.1.13",
"eslint": "^8.49.0",
"eslint": "^8.50.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",

File diff suppressed because it is too large Load Diff

251
src-tauri/Cargo.lock generated
View File

@ -77,9 +77,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0"
checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
dependencies = [
"memchr",
]
@ -192,7 +192,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b"
dependencies = [
"event-listener",
"event-listener 2.5.3",
"futures-core",
]
@ -203,20 +203,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener",
"event-listener 2.5.3",
"futures-core",
]
[[package]]
name = "async-executor"
version = "1.5.1"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb"
checksum = "78f2db9467baa66a700abce2a18c5ad793f6f83310aca1284796fc3921d113fd"
dependencies = [
"async-lock",
"async-task",
"concurrent-queue",
"fastrand 1.9.0",
"fastrand 2.0.1",
"futures-lite",
"slab",
]
@ -259,24 +259,23 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
dependencies = [
"event-listener",
"event-listener 2.5.3",
]
[[package]]
name = "async-process"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9"
checksum = "bf012553ce51eb7aa6dc2143804cc8252bd1cb681a1c5cb7fa94ca88682dee1d"
dependencies = [
"async-io",
"async-lock",
"autocfg",
"async-signal",
"blocking",
"cfg-if",
"event-listener",
"event-listener 3.0.0",
"futures-lite",
"rustix 0.37.23",
"signal-hook",
"rustix 0.38.14",
"windows-sys 0.48.0",
]
@ -292,10 +291,29 @@ dependencies = [
]
[[package]]
name = "async-task"
version = "4.4.0"
name = "async-signal"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae"
checksum = "4af361a844928cb7d36590d406709473a1b574f443094422ef166daa3b493208"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"concurrent-queue",
"futures-core",
"futures-io",
"libc",
"signal-hook-registry",
"slab",
"windows-sys 0.48.0",
]
[[package]]
name = "async-task"
version = "4.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9441c6b2fe128a7c2bf680a44c34d0df31ce09e5b7e401fcca3faa483dbc921"
[[package]]
name = "async-trait"
@ -343,15 +361,15 @@ dependencies = [
[[package]]
name = "atomic-waker"
version = "1.1.1"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "auto-launch"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5904a4d734f0235edf29aab320a14899f3e090446e594ff96508a6215f76f89c"
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
dependencies = [
"dirs",
"thiserror",
@ -481,17 +499,18 @@ dependencies = [
[[package]]
name = "blocking"
version = "1.3.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65"
checksum = "94c4ef1f913d78636d78d538eec1f18de81e481f44b1be0a81060090530846e1"
dependencies = [
"async-channel",
"async-lock",
"async-task",
"atomic-waker",
"fastrand 1.9.0",
"fastrand 2.0.1",
"futures-io",
"futures-lite",
"log",
"piper",
"tracing",
]
[[package]]
@ -717,9 +736,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.4"
version = "4.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136"
checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3"
dependencies = [
"clap_builder",
"clap_derive",
@ -727,9 +746,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.4.4"
version = "4.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56"
checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab"
dependencies = [
"anstream",
"anstyle",
@ -800,15 +819,14 @@ dependencies = [
[[package]]
name = "cocoa-foundation"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6"
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
dependencies = [
"bitflags 1.3.2",
"block",
"core-foundation",
"core-graphics-types",
"foreign-types 0.3.2",
"libc",
"objc",
]
@ -837,9 +855,9 @@ dependencies = [
[[package]]
name = "concurrent-queue"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400"
dependencies = [
"crossbeam-utils",
]
@ -1431,6 +1449,17 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "event-listener"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29e56284f00d94c1bc7fd3c77027b4623c88c1f53d8d2394c6199f2921dea325"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "1.9.0"
@ -1442,9 +1471,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "fd-lock"
@ -1453,7 +1482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
dependencies = [
"cfg-if",
"rustix 0.38.13",
"rustix 0.38.14",
"windows-sys 0.48.0",
]
@ -2317,9 +2346,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
@ -2375,7 +2404,7 @@ dependencies = [
"hmac",
"pbkdf2",
"serde",
"sha2 0.10.7",
"sha2 0.10.8",
"unicode-normalization",
"x25519-dalek",
"zeroize",
@ -2623,6 +2652,8 @@ dependencies = [
name = "lume"
version = "1.2.5"
dependencies = [
"cocoa 0.25.0",
"objc",
"rust-argon2",
"serde",
"serde_json",
@ -2724,10 +2755,11 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "md-5"
version = "0.10.5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest 0.10.7",
]
@ -3202,9 +3234,9 @@ dependencies = [
[[package]]
name = "parking"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e"
checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067"
[[package]]
name = "parking_lot"
@ -3405,6 +3437,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
dependencies = [
"atomic-waker",
"fastrand 2.0.1",
"futures-io",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
@ -3694,9 +3737,9 @@ checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
[[package]]
name = "rayon"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
dependencies = [
"either",
"rayon-core",
@ -3704,14 +3747,12 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.11.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]]
@ -3955,9 +3996,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.13"
version = "0.38.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662"
checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
dependencies = [
"bitflags 2.4.0",
"errno",
@ -3988,9 +4029,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.101.5"
version = "0.101.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed"
checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe"
dependencies = [
"ring",
"untrusted",
@ -4123,9 +4164,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.18"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
dependencies = [
"serde",
]
@ -4203,7 +4244,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.0.0",
"indexmap 2.0.1",
"serde",
"serde_json",
"serde_with_macros",
@ -4256,9 +4297,9 @@ dependencies = [
[[package]]
name = "sha1"
version = "0.10.5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
@ -4280,9 +4321,9 @@ dependencies = [
[[package]]
name = "sha2"
version = "0.10.7"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
@ -4291,23 +4332,13 @@ dependencies = [
[[package]]
name = "sharded-slab"
version = "0.1.4"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
checksum = "c1b21f559e07218024e7e9f90f96f601825397de0e25420135f7f952453fed0b"
dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@ -4350,9 +4381,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
[[package]]
name = "socket2"
@ -4491,7 +4522,7 @@ dependencies = [
"crossbeam-queue",
"dotenvy",
"either",
"event-listener",
"event-listener 2.5.3",
"futures-channel",
"futures-core",
"futures-intrusive",
@ -4499,7 +4530,7 @@ dependencies = [
"futures-util",
"hashlink",
"hex",
"indexmap 2.0.0",
"indexmap 2.0.1",
"log",
"memchr",
"once_cell",
@ -4509,7 +4540,7 @@ dependencies = [
"rustls-pemfile",
"serde",
"serde_json",
"sha2 0.10.7",
"sha2 0.10.8",
"smallvec",
"sqlformat",
"thiserror",
@ -4549,7 +4580,7 @@ dependencies = [
"quote",
"serde",
"serde_json",
"sha2 0.10.7",
"sha2 0.10.8",
"sqlx-core",
"sqlx-mysql",
"sqlx-postgres",
@ -4593,7 +4624,7 @@ dependencies = [
"rsa",
"serde",
"sha1",
"sha2 0.10.7",
"sha2 0.10.8",
"smallvec",
"sqlx-core",
"stringprep",
@ -4633,7 +4664,7 @@ dependencies = [
"serde",
"serde_json",
"sha1",
"sha2 0.10.7",
"sha2 0.10.8",
"smallvec",
"sqlx-core",
"stringprep",
@ -5024,7 +5055,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"sha2 0.10.7",
"sha2 0.10.8",
"tauri-utils",
"thiserror",
"time",
@ -5049,7 +5080,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-autostart"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#76832e60bfba44c24d6af8a5099be123886ba63d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#3d279094d44be78cdc5d1de3938f1414e13db6b0"
dependencies = [
"auto-launch",
"log",
@ -5062,7 +5093,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-single-instance"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#76832e60bfba44c24d6af8a5099be123886ba63d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#3d279094d44be78cdc5d1de3938f1414e13db6b0"
dependencies = [
"log",
"serde",
@ -5076,7 +5107,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-sql"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#76832e60bfba44c24d6af8a5099be123886ba63d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#3d279094d44be78cdc5d1de3938f1414e13db6b0"
dependencies = [
"futures-core",
"log",
@ -5092,7 +5123,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-store"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#76832e60bfba44c24d6af8a5099be123886ba63d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#3d279094d44be78cdc5d1de3938f1414e13db6b0"
dependencies = [
"log",
"serde",
@ -5104,7 +5135,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-stronghold"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#76832e60bfba44c24d6af8a5099be123886ba63d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#3d279094d44be78cdc5d1de3938f1414e13db6b0"
dependencies = [
"hex",
"iota-crypto 0.23.0",
@ -5123,7 +5154,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-upload"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#76832e60bfba44c24d6af8a5099be123886ba63d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#3d279094d44be78cdc5d1de3938f1414e13db6b0"
dependencies = [
"futures-util",
"log",
@ -5234,9 +5265,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
dependencies = [
"cfg-if",
"fastrand 2.0.0",
"fastrand 2.0.1",
"redox_syscall 0.3.5",
"rustix 0.38.13",
"rustix 0.38.14",
"windows-sys 0.48.0",
]
@ -5259,18 +5290,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]]
name = "thiserror"
version = "1.0.48"
version = "1.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.48"
version = "1.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
dependencies = [
"proc-macro2",
"quote",
@ -5289,9 +5320,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe"
dependencies = [
"deranged",
"itoa 1.0.9",
@ -5302,15 +5333,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.14"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
dependencies = [
"time-core",
]
@ -5381,9 +5412,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.8"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
dependencies = [
"bytes",
"futures-core",
@ -5429,7 +5460,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.0.0",
"indexmap 2.0.1",
"serde",
"serde_spanned",
"toml_datetime",
@ -5685,9 +5716,9 @@ dependencies = [
[[package]]
name = "waker-fn"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
[[package]]
name = "walkdir"
@ -5940,9 +5971,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
@ -6346,7 +6377,7 @@ dependencies = [
"once_cell",
"serde",
"serde_json",
"sha2 0.10.7",
"sha2 0.10.8",
"soup2",
"tao",
"thiserror",
@ -6439,7 +6470,7 @@ dependencies = [
"byteorder",
"derivative",
"enumflags2",
"event-listener",
"event-listener 2.5.3",
"futures-core",
"futures-sink",
"futures-util",

View File

@ -16,11 +16,10 @@ tauri-build = { version = "1.4", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4", features = [
tauri = { version = "1.4", features = [ "macos-private-api",
"window-close",
"window-print",
"window-create",
"macos-private-api",
"fs-read-dir",
"fs-read-file",
"window-start-dragging",
@ -55,6 +54,10 @@ sqlx-cli = { version = "0.7.0", default-features = false, features = [
rust-argon2 = "1.0"
webpage = { version = "1.6.0", features = ["serde"] }
[target.'cfg(any(target_os = "macos"))'.dependencies]
cocoa = "0.25.0"
objc = "0.2.7"
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL

View File

@ -0,0 +1,9 @@
-- Add migration script here
CREATE TABLE
metadata (
id TEXT NOT NULL PRIMARY KEY,
event TEXT NOT NULL,
author TEXT NOT NULL,
kind NUMBER NOT NULL DEFAULt 0,
created_at INTEGER NOT NULL
);

View File

@ -3,15 +3,25 @@
windows_subsystem = "windows"
)]
use tauri::Manager;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
use std::time::Duration;
use tauri::{Manager, WindowEvent};
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_sql::{Migration, MigrationKind};
use webpage::{Webpage, WebpageOptions};
use std::time::Duration;
#[cfg(target_os = "macos")]
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
#[cfg(target_os = "macos")]
use traffic_light::TrafficLight;
#[cfg(target_os = "macos")]
mod traffic_light;
#[derive(Clone, serde::Serialize)]
struct Payload {
args: Vec<String>,
@ -102,8 +112,20 @@ fn main() {
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, None)
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
#[cfg(target_os = "macos")]
window.set_transparent_titlebar(true);
#[cfg(target_os = "macos")]
window.position_traffic_lights(16.0, 25.0);
Ok(())
})
.on_window_event(|e| {
#[cfg(target_os = "macos")]
if let WindowEvent::Resized(..) = e.event() {
let window = e.window();
window.position_traffic_lights(16.0, 25.0);
}
})
.plugin(
tauri_plugin_sql::Builder::default()
.add_migrations(
@ -217,6 +239,12 @@ fn main() {
sql: include_str!("../migrations/20230918235335_add_uniq_to_relay.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230921085234,
description: "add metadata",
sql: include_str!("../migrations/20230921085234_add_metadata_table.sql"),
kind: MigrationKind::Up,
},
],
)
.build(),

View File

@ -0,0 +1,60 @@
use tauri::{Runtime, Window};
pub trait TrafficLight {
#[cfg(target_os = "macos")]
fn set_transparent_titlebar(&self, transparent: bool);
fn position_traffic_lights(&self, x: f64, y: f64);
}
impl<R: Runtime> TrafficLight for Window<R> {
#[cfg(target_os = "macos")]
fn set_transparent_titlebar(&self, transparent: bool) {
use cocoa::appkit::{NSWindow, NSWindowTitleVisibility};
let window = self.ns_window().unwrap() as cocoa::base::id;
unsafe {
window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
if transparent {
window.setTitlebarAppearsTransparent_(cocoa::base::YES);
} else {
window.setTitlebarAppearsTransparent_(cocoa::base::NO);
}
}
}
#[cfg(target_os = "macos")]
fn position_traffic_lights(&self, x: f64, y: f64) {
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
use cocoa::foundation::NSRect;
let window = self.ns_window().unwrap() as cocoa::base::id;
unsafe {
let close = window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
let miniaturize = window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
let zoom = window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
let title_bar_container_view = close.superview().superview();
let close_rect: NSRect = msg_send![close, frame];
let button_height = close_rect.size.height;
let title_bar_frame_height = button_height + y;
let mut title_bar_rect = NSView::frame(title_bar_container_view);
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y = NSView::frame(window).size.height - title_bar_frame_height;
let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect];
let window_buttons = vec![close, miniaturize, zoom];
let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
for (i, button) in window_buttons.into_iter().enumerate() {
let mut rect: NSRect = NSView::frame(button);
rect.origin.x = x + (i as f64 * space_between);
button.setFrameOrigin(rect.origin);
}
}
}
}

View File

@ -136,7 +136,6 @@
"enableTauriAPI": true
}
]
},
"macOSPrivateApi": true
}
}
}

View File

@ -1,6 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"macOSPrivateApi": true,
"windows": [
{
"width": 400,

View File

@ -1,11 +1,15 @@
import { message } from '@tauri-apps/api/dialog';
import { RouterProvider, createBrowserRouter, redirect } from 'react-router-dom';
import 'reactflow/dist/style.css';
import { AuthCreateScreen } from '@app/auth/create';
import { AuthImportScreen } from '@app/auth/import';
import { OnboardingScreen } from '@app/auth/onboarding';
import { BrowseScreen } from '@app/browse';
import { ErrorScreen } from '@app/error';
import { useStorage } from '@libs/storage/provider';
import { Frame } from '@shared/frame';
import { LoaderIcon } from '@shared/icons';
import { AppLayout } from '@shared/layouts/app';
@ -13,254 +17,279 @@ import { AuthLayout } from '@shared/layouts/auth';
import { NoteLayout } from '@shared/layouts/note';
import { SettingsLayout } from '@shared/layouts/settings';
import { checkActiveAccount } from '@utils/checkActiveAccount';
import './index.css';
async function Loader() {
try {
const account = await checkActiveAccount();
const stronghold = sessionStorage.getItem('stronghold');
const privkey = JSON.parse(stronghold).state.privkey || null;
const onboarding = localStorage.getItem('onboarding');
const step = JSON.parse(onboarding).state.step || null;
if (!account) {
return redirect('/auth/welcome');
} else {
if (step) {
return redirect(step);
}
if (!privkey) {
return redirect('/auth/unlock');
}
}
return null;
} catch (e) {
await message(e, { title: 'An unexpected error has occurred', type: 'error' });
}
}
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
errorElement: <ErrorScreen />,
loader: Loader,
children: [
{
path: '',
async lazy() {
const { SpaceScreen } = await import('@app/space');
return { Component: SpaceScreen };
},
},
{
path: 'users/:pubkey',
async lazy() {
const { UserScreen } = await import('@app/users');
return { Component: UserScreen };
},
},
{
path: 'chats/:pubkey',
async lazy() {
const { ChatScreen } = await import('@app/chats');
return { Component: ChatScreen };
},
},
{
path: 'notifications',
async lazy() {
const { NotificationScreen } = await import('@app/notifications');
return { Component: NotificationScreen };
},
},
{
path: 'nwc',
async lazy() {
const { NWCScreen } = await import('@app/nwc');
return { Component: NWCScreen };
},
},
],
},
{
path: '/notes',
element: <NoteLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'text/:id',
async lazy() {
const { TextNoteScreen } = await import('@app/notes/text');
return { Component: TextNoteScreen };
},
},
{
path: 'article/:id',
async lazy() {
const { ArticleNoteScreen } = await import('@app/notes/article');
return { Component: ArticleNoteScreen };
},
},
],
},
{
path: '/splashscreen',
errorElement: <ErrorScreen />,
async lazy() {
const { SplashScreen } = await import('@app/splash');
return { Component: SplashScreen };
},
},
{
path: '/auth',
element: <AuthLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'welcome',
async lazy() {
const { WelcomeScreen } = await import('@app/auth/welcome');
return { Component: WelcomeScreen };
},
},
{
path: 'import',
element: <AuthImportScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { ImportStep1Screen } = await import('@app/auth/import/step-1');
return { Component: ImportStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { ImportStep2Screen } = await import('@app/auth/import/step-2');
return { Component: ImportStep2Screen };
},
},
{
path: 'step-3',
async lazy() {
const { ImportStep3Screen } = await import('@app/auth/import/step-3');
return { Component: ImportStep3Screen };
},
},
],
},
{
path: 'create',
element: <AuthCreateScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { CreateStep1Screen } = await import('@app/auth/create/step-1');
return { Component: CreateStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { CreateStep2Screen } = await import('@app/auth/create/step-2');
return { Component: CreateStep2Screen };
},
},
{
path: 'step-3',
async lazy() {
const { CreateStep3Screen } = await import('@app/auth/create/step-3');
return { Component: CreateStep3Screen };
},
},
],
},
{
path: 'onboarding',
element: <OnboardingScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { OnboardStep1Screen } = await import('@app/auth/onboarding/step-1');
return { Component: OnboardStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { OnboardStep2Screen } = await import('@app/auth/onboarding/step-2');
return { Component: OnboardStep2Screen };
},
},
],
},
{
path: 'complete',
async lazy() {
const { CompleteScreen } = await import('@app/auth/complete');
return { Component: CompleteScreen };
},
},
{
path: 'unlock',
async lazy() {
const { UnlockScreen } = await import('@app/auth/unlock');
return { Component: UnlockScreen };
},
},
{
path: 'migrate',
async lazy() {
const { MigrateScreen } = await import('@app/auth/migrate');
return { Component: MigrateScreen };
},
},
{
path: 'reset',
async lazy() {
const { ResetScreen } = await import('@app/auth/reset');
return { Component: ResetScreen };
},
},
],
},
{
path: '/settings',
element: <SettingsLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { GeneralSettingsScreen } = await import('@app/settings/general');
return { Component: GeneralSettingsScreen };
},
},
{
path: 'backup',
async lazy() {
const { AccountSettingsScreen } = await import('@app/settings/account');
return { Component: AccountSettingsScreen };
},
},
],
},
]);
export default function App() {
const { db } = useStorage();
const accountLoader = async () => {
try {
const account = await db.checkAccount();
const stronghold = sessionStorage.getItem('stronghold');
const privkey = JSON.parse(stronghold).state.privkey || null;
const onboarding = localStorage.getItem('onboarding');
const step = JSON.parse(onboarding).state.step || null;
if (!account) {
return redirect('/auth/welcome');
} else {
if (step) {
return redirect(step);
}
if (!privkey) {
return redirect('/auth/unlock');
}
}
return null;
} catch (e) {
await message(e, { title: 'An unexpected error has occurred', type: 'error' });
}
};
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
errorElement: <ErrorScreen />,
loader: accountLoader,
children: [
{
path: '',
async lazy() {
const { SpaceScreen } = await import('@app/space');
return { Component: SpaceScreen };
},
},
{
path: 'browse',
element: <BrowseScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { BrowseUsersScreen } = await import('@app/browse/users');
return { Component: BrowseUsersScreen };
},
},
{
path: 'relays',
async lazy() {
const { BrowseRelaysScreen } = await import('@app/browse/relays');
return { Component: BrowseRelaysScreen };
},
},
],
},
{
path: 'users/:pubkey',
async lazy() {
const { UserScreen } = await import('@app/users');
return { Component: UserScreen };
},
},
{
path: 'chats/:pubkey',
async lazy() {
const { ChatScreen } = await import('@app/chats');
return { Component: ChatScreen };
},
},
{
path: 'notifications',
async lazy() {
const { NotificationScreen } = await import('@app/notifications');
return { Component: NotificationScreen };
},
},
{
path: 'nwc',
async lazy() {
const { NWCScreen } = await import('@app/nwc');
return { Component: NWCScreen };
},
},
],
},
{
path: '/notes',
element: <NoteLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'text/:id',
async lazy() {
const { TextNoteScreen } = await import('@app/notes/text');
return { Component: TextNoteScreen };
},
},
{
path: 'article/:id',
async lazy() {
const { ArticleNoteScreen } = await import('@app/notes/article');
return { Component: ArticleNoteScreen };
},
},
],
},
{
path: '/splashscreen',
errorElement: <ErrorScreen />,
async lazy() {
const { SplashScreen } = await import('@app/splash');
return { Component: SplashScreen };
},
},
{
path: '/auth',
element: <AuthLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'welcome',
async lazy() {
const { WelcomeScreen } = await import('@app/auth/welcome');
return { Component: WelcomeScreen };
},
},
{
path: 'import',
element: <AuthImportScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { ImportStep1Screen } = await import('@app/auth/import/step-1');
return { Component: ImportStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { ImportStep2Screen } = await import('@app/auth/import/step-2');
return { Component: ImportStep2Screen };
},
},
{
path: 'step-3',
async lazy() {
const { ImportStep3Screen } = await import('@app/auth/import/step-3');
return { Component: ImportStep3Screen };
},
},
],
},
{
path: 'create',
element: <AuthCreateScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { CreateStep1Screen } = await import('@app/auth/create/step-1');
return { Component: CreateStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { CreateStep2Screen } = await import('@app/auth/create/step-2');
return { Component: CreateStep2Screen };
},
},
{
path: 'step-3',
async lazy() {
const { CreateStep3Screen } = await import('@app/auth/create/step-3');
return { Component: CreateStep3Screen };
},
},
],
},
{
path: 'onboarding',
element: <OnboardingScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { OnboardStep1Screen } = await import(
'@app/auth/onboarding/step-1'
);
return { Component: OnboardStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { OnboardStep2Screen } = await import(
'@app/auth/onboarding/step-2'
);
return { Component: OnboardStep2Screen };
},
},
],
},
{
path: 'complete',
async lazy() {
const { CompleteScreen } = await import('@app/auth/complete');
return { Component: CompleteScreen };
},
},
{
path: 'unlock',
async lazy() {
const { UnlockScreen } = await import('@app/auth/unlock');
return { Component: UnlockScreen };
},
},
{
path: 'migrate',
async lazy() {
const { MigrateScreen } = await import('@app/auth/migrate');
return { Component: MigrateScreen };
},
},
{
path: 'reset',
async lazy() {
const { ResetScreen } = await import('@app/auth/reset');
return { Component: ResetScreen };
},
},
],
},
{
path: '/settings',
element: <SettingsLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { GeneralSettingsScreen } = await import('@app/settings/general');
return { Component: GeneralSettingsScreen };
},
},
{
path: 'backup',
async lazy() {
const { AccountSettingsScreen } = await import('@app/settings/account');
return { Component: AccountSettingsScreen };
},
},
],
},
]);
return (
<RouterProvider
router={router}
@ -269,6 +298,7 @@ export default function App() {
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
</Frame>
}
future={{ v7_startTransition: true }}
/>
);
}

View File

@ -16,7 +16,7 @@ export function ImportStep3Screen() {
const setStep = useOnboarding((state) => state.setStep);
const { db } = useStorage();
const { fetchUserData, prefetchEvents } = useNostr();
const { fetchUserData } = useNostr();
const [loading, setLoading] = useState(false);
@ -27,16 +27,14 @@ export function ImportStep3Screen() {
// prefetch data
const user = await fetchUserData();
const data = await prefetchEvents();
// create default widget
await db.createWidget(WidgetKinds.other.learnNostr, 'Learn Nostr', '');
// redirect to next step
if (user.status === 'ok' && data.status === 'ok') {
if (user.status === 'ok') {
navigate('/auth/onboarding/step-2', { replace: true });
} else {
console.log('error: ', data.message);
setLoading(false);
}
} catch (e) {
@ -83,7 +81,7 @@ export function ImportStep3Screen() {
</button>
<span className="text-center text-sm text-white/50">
By clicking &apos;Continue&apos;, Lume will download your old relay list and
all events from the last 24 hours. It may take a bit
metadata. It may take a bit
</span>
</div>
</div>

View File

@ -16,7 +16,7 @@ export function OnboardStep1Screen() {
const navigate = useNavigate();
const setStep = useOnboarding((state) => state.setStep);
const { publish, fetchUserData, prefetchEvents } = useNostr();
const { publish, fetchUserData } = useNostr();
const { db } = useStorage();
const { status, data } = useQuery(['trending-profiles-widget'], async () => {
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
@ -46,14 +46,12 @@ export function OnboardStep1Screen() {
// prefetch data
const user = await fetchUserData(follows);
const data = await prefetchEvents();
// redirect to next step
if (event && user.status === 'ok' && data.status === 'ok') {
if (event && user.status === 'ok') {
navigate('/auth/onboarding/step-2', { replace: true });
} else {
setLoading(false);
console.log('error: ', data.message);
}
} catch (e) {
setLoading(false);
@ -70,7 +68,7 @@ export function OnboardStep1Screen() {
<div className="flex h-full w-full flex-col justify-center">
<div className="mx-auto mb-4 w-full max-w-md border-b border-white/10 pb-4">
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
{loading ? 'Prefetching data...' : 'Enrich your network'}
{loading ? 'Loading...' : 'Enrich your network'}
</h1>
<p className="text-white/70">
Choose the account you want to follow. These accounts are trending in the last
@ -127,19 +125,12 @@ export function OnboardStep1Screen() {
</>
)}
</button>
{!loading ? (
<Link
to="/auth/onboarding/step-2"
className="inline-flex h-12 w-full items-center justify-center rounded-lg border-t border-white/10 bg-white/20 px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/30 focus:outline-none"
>
Skip, you can add later
</Link>
) : (
<span className="text-center text-sm text-white/50">
By clicking &apos;Continue&apos;, Lume will download all events related to
your follows from the last 24 hours. It may take a bit
</span>
)}
<Link
to="/auth/onboarding/step-2"
className="inline-flex h-12 w-full items-center justify-center rounded-lg border-t border-white/10 bg-white/20 px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/30 focus:outline-none"
>
Skip, you can add later
</Link>
</div>
</div>
</div>

View File

@ -70,6 +70,13 @@ export function UnlockScreen() {
}
};
const logout = async () => {
// remove account
db.accountLogout();
// redirect to welcome screen
navigate('/auth/welcome');
};
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md">
@ -126,12 +133,30 @@ export function UnlockScreen() {
</>
)}
</button>
<Link
to="/auth/reset"
className="mt-1 inline-flex h-12 w-full items-center justify-center rounded-lg text-center text-white/70 hover:bg-white/20"
>
Reset password
</Link>
<div className="mt-8 w-full">
<div className="flex items-center gap-2.5">
<div className="h-px flex-1 bg-white/10" />
<p className="shrink-0 text-sm font-medium text-white/50">
Forgot password?
</p>
<div className="h-px flex-1 bg-white/10" />
</div>
<div className="mt-2 flex flex-col">
<Link
to="/auth/reset"
className="inline-flex h-10 w-full items-center justify-center rounded-lg text-center text-sm font-medium text-white/70 hover:bg-white/20"
>
Reset password if you still have private key
</Link>
<button
type="button"
onClick={logout}
className="inline-flex h-10 w-full items-center justify-center rounded-lg text-center text-sm font-medium text-white/70 hover:bg-white/20"
>
Login with another account
</button>
</div>
</div>
</div>
</form>
</div>

View File

@ -1,34 +1,10 @@
import { LogicalSize, getCurrent } from '@tauri-apps/api/window';
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
export function WelcomeScreen() {
const appWindow = getCurrent();
async function setWindow() {
await appWindow.setSize(new LogicalSize(400, 500));
await appWindow.setResizable(false);
await appWindow.center();
}
async function resetWindow() {
await appWindow.setSize(new LogicalSize(1080, 800));
await appWindow.setResizable(false);
await appWindow.center();
}
useEffect(() => {
setWindow();
return () => {
resetWindow();
};
}, []);
return (
<div className="flex h-screen w-full flex-col justify-between">
<div className="mx-auto flex h-screen w-full max-w-md flex-col justify-center">
<div className="flex flex-col gap-10 pt-16">
<div className="flex flex-col gap-1.5 text-center">
<h1 className="text-3xl font-semibold text-white">Welcome to Lume</h1>
@ -54,8 +30,8 @@ export function WelcomeScreen() {
</Link>
</div>
</div>
<div className="flex flex-1 items-end justify-center pb-6">
<img src="/lume.png" alt="lume" className="h-auto w-1/4" />
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 transform">
<img src="/lume.png" alt="lume" className="mx-auto h-auto w-1/4" />
</div>
</div>
);

View File

@ -0,0 +1,29 @@
import { BaseEdge, EdgeProps, getBezierPath } from 'reactflow';
export function Edge({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
}: EdgeProps) {
const [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{ ...style, stroke: '#71717a' }}
/>
);
}

View File

@ -0,0 +1,17 @@
import { memo } from 'react';
import { useProfile } from '@utils/hooks/useProfile';
export const GroupTitle = memo(function GroupTitle({ pubkey }: { pubkey: string }) {
const { status, user } = useProfile(pubkey);
if (status === 'loading') {
return <div className="h-3 w-24 animate-pulse rounded bg-white/10" />;
}
return (
<h3 className="text-sm font-semibold text-fuchsia-500">{`${
user.name || user.display_name
}'s network`}</h3>
);
});

View File

@ -0,0 +1,14 @@
export function Line({ fromX, fromY, toX, toY }) {
return (
<g>
<path
fill="none"
stroke="#f5d0fe"
strokeWidth={1.5}
className="animated"
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
/>
<circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#f5d0fe" strokeWidth={1.5} />
</g>
);
}

View File

@ -0,0 +1,34 @@
import { Handle, Position } from 'reactflow';
import { UserWithDrawer } from '@app/browse/components/userWithDrawer';
import { GroupTitle } from './groupTitle';
export function UserGroupNode({ data }) {
return (
<>
<Handle
type="target"
position={Position.Top}
className="h-2 w-5 rounded-full border-none !bg-fuchsia-400"
/>
<div className="relative mx-3 my-3 flex flex-col gap-1">
{data.title ? (
<h3 className="text-sm font-semibold text-fuchsia-500">{data.title}</h3>
) : (
<GroupTitle pubkey={data.pubkey} />
)}
<div className="grid grid-cols-5 gap-6 rounded-lg border border-fuchsia-500/50 bg-fuchsia-500/10 p-4">
{data.list.map((user: string) => (
<UserWithDrawer key={user} pubkey={user} />
))}
</div>
</div>
<Handle
type="source"
position={Position.Bottom}
className="h-2 w-5 rounded-full border-none !bg-fuchsia-400"
/>
</>
);
}

View File

@ -0,0 +1,80 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useCallback } from 'react';
import { LoaderIcon } from '@shared/icons';
import {
ArticleNote,
FileNote,
NoteWrapper,
Repost,
TextNote,
UnknownNote,
} from '@shared/notes';
import { useNostr } from '@utils/hooks/useNostr';
export function UserLatestPosts({ pubkey }: { pubkey: string }) {
const { getEventsByPubkey } = useNostr();
const { status, data } = useQuery(['user-posts', pubkey], async () => {
return await getEventsByPubkey(pubkey);
});
const renderItem = useCallback(
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return <Repost key={event.id} event={event} />;
case 1063:
return (
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
[data]
);
return (
<div className="mt-4 border-t border-white/5 pt-3">
<h3 className="mb-4 px-3 font-semibold text-white">Latest post</h3>
<div>
{status === 'loading' ? (
<div className="px-3">
<div className="inline-flex h-16 w-full items-center justify-center gap-1.5 rounded-lg bg-white/10 text-sm font-medium text-white/70">
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
Loading latest posts...
</div>
</div>
) : data.length < 1 ? (
<div className="px-3">
<div className="inline-flex h-16 w-full items-center justify-center rounded-lg bg-white/10 text-sm font-medium text-white/70">
No posts from 24 hours ago
</div>
</div>
) : (
data.map((event) => renderItem(event))
)}
</div>
</div>
);
}

View File

@ -0,0 +1,21 @@
import { Handle, Position } from 'reactflow';
import { User } from '@shared/user';
export function UserNode({ data }) {
return (
<>
<div className="relative mx-3 my-3 inline-flex h-12 w-12 shrink-0 items-center justify-center">
<span className="absolute inline-flex h-8 w-8 animate-ping rounded-lg bg-green-400 opacity-75"></span>
<div className="relative z-10">
<User pubkey={data.pubkey} variant="avatar" />
</div>
</div>
<Handle
type="source"
position={Position.Bottom}
className="h-2 w-2 rounded-full border-none !bg-white/20"
/>
</>
);
}

View File

@ -0,0 +1,130 @@
import * as Dialog from '@radix-ui/react-dialog';
import { memo, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import { Image } from '@shared/image';
import { NIP05 } from '@shared/nip05';
import { TextNote } from '@shared/notes';
import { User } from '@shared/user';
import { useNostr } from '@utils/hooks/useNostr';
import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey';
import { UserLatestPosts } from './userLatestPosts';
export const UserWithDrawer = memo(function UserWithDrawer({
pubkey,
}: {
pubkey: string;
}) {
const { addContact, removeContact } = useNostr();
const { db } = useStorage();
const { status, user } = useProfile(pubkey);
const [followed, setFollowed] = useState(false);
const followUser = (pubkey: string) => {
try {
addContact(pubkey);
// update state
setFollowed(true);
} catch (error) {
console.log(error);
}
};
const unfollowUser = (pubkey: string) => {
try {
removeContact(pubkey);
// update state
setFollowed(false);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
if (db.account.follows.includes(pubkey)) {
setFollowed(true);
}
}, []);
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button type="button">
<User pubkey={pubkey} variant="avatar" />
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content className="fixed right-0 top-0 z-50 flex h-full w-[400px] animate-slideRightAndFade items-center justify-center px-4 pb-4 pt-16 transition-all">
<div className="h-full w-full overflow-y-auto rounded-lg border-t border-white/10 bg-white/20 py-3 backdrop-blur-3xl">
{status === 'loading' ? (
<div>
<p>Loading...</p>
</div>
) : (
<>
<div className="flex flex-col gap-3 px-3">
<Image
src={user?.picture || user?.image}
alt={pubkey}
className="h-12 w-12 rounded-lg"
/>
<div className="flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-1.5">
<h5 className="text-lg font-semibold leading-none">
{user?.displayName || user?.name || 'No name'}
</h5>
{user?.nip05 ? (
<NIP05
pubkey={pubkey}
nip05={user?.nip05}
className="max-w-[15rem] truncate text-sm leading-none text-white/50"
/>
) : (
<span className="max-w-[15rem] truncate text-sm leading-none text-white/50">
{displayNpub(pubkey, 16)}
</span>
)}
{user?.about ? <TextNote content={user?.about} /> : null}
</div>
<div className="mt-3 inline-flex items-center gap-2">
{followed ? (
<button
type="button"
onClick={() => unfollowUser(pubkey)}
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium backdrop-blur-xl hover:bg-fuchsia-500"
>
Unfollow
</button>
) : (
<button
type="button"
onClick={() => followUser(pubkey)}
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium backdrop-blur-xl hover:bg-fuchsia-500"
>
Follow
</button>
)}
<Link
to={`/chats/${pubkey}`}
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium backdrop-blur-xl hover:bg-fuchsia-500"
>
Message
</Link>
</div>
</div>
</div>
<UserLatestPosts pubkey={pubkey} />
</>
)}
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
});

43
src/app/browse/index.tsx Normal file
View File

@ -0,0 +1,43 @@
import { NavLink, Outlet } from 'react-router-dom';
import { ReactFlowProvider } from 'reactflow';
import { twMerge } from 'tailwind-merge';
export function BrowseScreen() {
return (
<ReactFlowProvider>
<div className="relative h-full w-full">
<div className="absolute left-0 right-0 top-4 z-30 flex w-full items-center justify-between px-3">
<div className="w-10" />
<div className="inline-flex gap-1 rounded-full border-t border-white/10 bg-white/20 p-1 backdrop-blur-xl">
<NavLink
to="/browse/"
className={({ isActive }) =>
twMerge(
'inline-flex h-7 w-20 items-center justify-center rounded-full text-sm font-semibold',
isActive ? 'bg-white/10 hover:bg-white/20' : ' hover:bg-white/5'
)
}
>
Users
</NavLink>
<NavLink
to="/browse/relays"
className={({ isActive }) =>
twMerge(
'inline-flex h-7 w-20 items-center justify-center rounded-full text-sm font-semibold',
isActive ? 'bg-white/10 hover:bg-white/20' : ' hover:bg-white/5'
)
}
>
Relays
</NavLink>
</div>
<div className="w-10" />
</div>
<div className="relative z-20 h-full w-full">
<Outlet />
</div>
</div>
</ReactFlowProvider>
);
}

12
src/app/browse/relays.tsx Normal file
View File

@ -0,0 +1,12 @@
export function BrowseRelaysScreen() {
return (
<div className="grid h-full w-full grid-cols-3">
<div className="col-span-2 border-r border-white/5 pt-16">
<p>Content</p>
</div>
<div className="col-span-1 px-3 pt-6">
<h3 className="font-semibold text-white">Your relays</h3>
</div>
</div>
);
}

116
src/app/browse/users.tsx Normal file
View File

@ -0,0 +1,116 @@
import { useCallback, useMemo, useRef } from 'react';
import ReactFlow, {
Background,
ConnectionMode,
addEdge,
useEdgesState,
useNodesState,
useReactFlow,
} from 'reactflow';
import { Edge } from '@app/browse//components/edge';
import { UserGroupNode } from '@app/browse//components/userGroupNode';
import { Line } from '@app/browse/components/line';
import { UserNode } from '@app/browse/components/userNode';
import { useStorage } from '@libs/storage/provider';
import { useNostr } from '@utils/hooks/useNostr';
import { getMultipleRandom } from '@utils/transform';
let id = 2;
const getId = () => `${id++}`;
const nodeTypes = { user: UserNode, userGroup: UserGroupNode };
const edgeTypes = { buttonedge: Edge };
export function BrowseUsersScreen() {
const { db } = useStorage();
const { getContactsByPubkey } = useNostr();
const { project } = useReactFlow();
const defaultContacts = useMemo(() => getMultipleRandom(db.account.follows, 10), []);
const reactFlowWrapper = useRef(null);
const connectingNodeId = useRef(null);
const initialNodes = [
{
id: '0',
type: 'user',
position: { x: 141, y: 0 },
data: { list: [], title: '', pubkey: db.account.pubkey },
},
{
id: '1',
type: 'userGroup',
position: { x: 0, y: 200 },
data: { list: defaultContacts, title: 'Starting Point', pubkey: '' },
},
];
const initialEdges = [{ id: 'e0-1', type: 'buttonedge', source: '0', target: '1' }];
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []);
const onConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd = useCallback(
async (event) => {
const targetIsPane = event.target.classList.contains('react-flow__pane');
if (targetIsPane) {
const { top, left } = reactFlowWrapper.current.getBoundingClientRect();
const id = getId();
const prevData = nodes.slice(-1)[0];
const randomPubkey = getMultipleRandom(prevData.data.list, 1)[0];
const newContactList = await getContactsByPubkey(randomPubkey);
const newNode = {
id,
type: 'userGroup',
position: project({ x: event.clientX - left, y: event.clientY - top }),
data: { list: newContactList, title: null, pubkey: randomPubkey },
};
setNodes((nds) => nds.concat(newNode));
setEdges((eds) =>
eds.concat({
id,
type: 'buttonedge',
source: connectingNodeId.current,
target: id,
})
);
}
},
[project]
);
return (
<div className="h-full w-full" ref={reactFlowWrapper}>
<ReactFlow
proOptions={{ hideAttribution: true }}
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
connectionLineComponent={Line}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
connectionMode={ConnectionMode.Loose}
minZoom={0.8}
maxZoom={1.2}
fitView
>
<Background color="#3f3f46" />
</ReactFlow>
</div>
);
}

View File

@ -11,7 +11,7 @@ export function ChatsListItem({ pubkey }: { pubkey: string }) {
if (status === 'loading') {
return (
<div className="inline-flex h-10 items-center gap-2.5 rounded-md px-2">
<div className="inline-flex h-10 items-center gap-2.5 rounded-md px-3">
<div className="relative h-7 w-7 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
<div className="h-2.5 w-2/3 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
</div>
@ -24,7 +24,7 @@ export function ChatsListItem({ pubkey }: { pubkey: string }) {
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-2',
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-3',
isActive
? 'border-fuchsia-500 bg-white/5 text-white'
: 'border-transparent text-white/70'
@ -38,7 +38,10 @@ export function ChatsListItem({ pubkey }: { pubkey: string }) {
/>
<div className="inline-flex w-full flex-1 items-center justify-between">
<h5 className="max-w-[10rem] truncate">
{user?.name || user?.display_name || displayNpub(pubkey, 16)}
{user?.name ||
user?.display_name ||
user?.displayName ||
displayNpub(pubkey, 16)}
</h5>
</div>
</NavLink>

View File

@ -7,6 +7,8 @@ import { UnknownsModal } from '@app/chats/components/unknowns';
import { useStorage } from '@libs/storage/provider';
import { LoaderIcon } from '@shared/icons';
import { useNostr } from '@utils/hooks/useNostr';
export function ChatsList() {
@ -33,8 +35,10 @@ export function ChatsList() {
return (
<div className="flex flex-col">
<div className="inline-flex h-10 items-center gap-2.5 border-l-2 border-transparent pl-4">
<div className="relative h-7 w-7 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
<div className="h-4 w-full animate-pulse rounded bg-white/10 backdrop-blur-xl" />
<div className="relative inline-flex h-7 w-7 shrink-0 items-center justify-center">
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
</div>
<h5 className="text-white/50">Loading messages...</h5>
</div>
</div>
);
@ -42,8 +46,8 @@ export function ChatsList() {
return (
<div className="flex flex-col">
{chats.follows.map((item) => renderItem(item))}
{chats.unknowns.length > 0 && <UnknownsModal data={chats.unknowns} />}
{chats?.follows?.map((item) => renderItem(item))}
{chats?.unknowns?.length > 0 && <UnknownsModal data={chats.unknowns} />}
<NewMessageModal />
</div>
);

View File

@ -2,6 +2,7 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage';
import { TextNote } from '@shared/notes';
import { User } from '@shared/user';
export function ChatMessageItem({
@ -20,13 +21,12 @@ export function ChatMessageItem({
}
return (
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 backdrop-blur-xl hover:bg-white/10">
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-white/10">
<div className="flex flex-col">
<User pubkey={message.pubkey} time={message.created_at} variant="chat" />
<div className="-mt-[20px] pl-[49px]">
<p className="select-text whitespace-pre-line break-words text-base text-white">
{message.content}
</p>
<div className="-mt-5 flex items-start gap-3">
<div className="w-10 shrink-0" />
<TextNote content={message.content} />
</div>
</div>
</div>

View File

@ -23,7 +23,7 @@ export function NewMessageModal() {
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 border-transparent pl-4 pr-2"
className="inline-flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 border-transparent pl-4 pr-3"
>
<div className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<PlusIcon className="h-4 w-4 text-white" />

View File

@ -22,7 +22,7 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<h3 className="text-lg font-semibold leading-none">
{user?.display_name || user?.name}
{user?.name || user?.display_name || user?.displayName}
</h3>
{user?.nip05 ? (
<NIP05

View File

@ -21,7 +21,7 @@ export function UnknownsModal({ data }: { data: string[] }) {
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 border-transparent pl-4 pr-2"
className="inline-flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 border-transparent pl-4 pr-3"
>
<div className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<StrangersIcon className="h-4 w-4 text-white" />

View File

@ -1,8 +1,8 @@
import { NDKSubscription } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { Virtuoso } from 'react-virtuoso';
import { VList, VListHandle } from 'virtua';
import { ChatMessageForm } from '@app/chats/components/messages/form';
import { ChatMessageItem } from '@app/chats/components/messages/item';
@ -18,7 +18,7 @@ import { useStronghold } from '@stores/stronghold';
import { useNostr } from '@utils/hooks/useNostr';
export function ChatScreen() {
const virtuosoRef = useRef(null);
const listRef = useRef<VListHandle>(null);
const userPrivkey = useStronghold((state) => state.privkey);
const { db } = useStorage();
@ -29,10 +29,8 @@ export function ChatScreen() {
return await fetchNIP04Messages(pubkey);
});
const itemContent = useCallback(
(index: string | number) => {
const message = data[index];
if (!message) return;
const renderItem = useCallback(
(message: NDKEvent) => {
return (
<ChatMessageItem
message={message}
@ -44,12 +42,9 @@ export function ChatScreen() {
[data]
);
const computeItemKey = useCallback(
(index: string | number) => {
return data[index].id;
},
[data]
);
useEffect(() => {
if (data.length > 0) listRef.current?.scrollToIndex(data.length);
}, [data]);
useEffect(() => {
const sub: NDKSubscription = ndk.subscribe(
@ -86,22 +81,17 @@ export function ChatScreen() {
<p className="text-sm font-medium text-white/50">Loading messages</p>
</div>
</div>
) : data.length === 0 ? (
<div className="absolute left-1/2 top-1/2 flex w-full -translate-x-1/2 -translate-y-1/2 transform flex-col gap-1 text-center">
<h3 className="mb-2 text-4xl">🙌</h3>
<p className="leading-none text-white/50">
You two didn&apos;t talk yet, let&apos;s send first message
</p>
</div>
) : (
<Virtuoso
ref={virtuosoRef}
data={data}
itemContent={itemContent}
computeItemKey={computeItemKey}
initialTopMostItemIndex={data.length - 1}
alignToBottom={true}
followOutput={true}
overscan={50}
increaseViewportBy={{ top: 200, bottom: 200 }}
className="scrollbar-hide relative overflow-y-auto"
components={{
EmptyPlaceholder: () => Empty,
}}
/>
<VList ref={listRef} className="scrollbar-hide h-full" mode="reverse">
{data.map((message) => renderItem(message))}
</VList>
)}
</div>
<div className="z-50 shrink-0 rounded-b-xl border-t border-white/5 bg-white/10 p-3 px-5 backdrop-blur-xl">
@ -120,12 +110,3 @@ export function ChatScreen() {
</div>
);
}
const Empty = (
<div className="absolute left-1/2 top-1/2 flex w-full -translate-x-1/2 -translate-y-1/2 transform flex-col gap-1 text-center">
<h3 className="mb-2 text-4xl">🙌</h3>
<p className="leading-none text-white/50">
You two didn&apos;t talk yet, let&apos;s send first message
</p>
</div>
);

View File

@ -25,7 +25,7 @@ export function NotiUser({ pubkey }: { pubkey: string }) {
className="h-8 w-8 shrink-0 rounded-md object-cover"
/>
<span className="max-w-[10rem] truncate font-medium leading-none text-white">
{user?.name || user?.display_name || displayNpub(pubkey, 16)}
{user?.name || user?.display_name || user?.displayName || displayNpub(pubkey, 16)}
</span>
</div>
);

View File

@ -21,7 +21,7 @@ export function NWCScreen() {
};
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex h-full w-full items-center justify-center bg-white/5">
<div className="flex w-full flex-col gap-5">
<div className="text-center">
<h3 className="text-2xl font-bold leading-tight">

View File

@ -65,9 +65,9 @@ export function SpaceScreen() {
case WidgetKinds.nostrBand.trendingNotes:
return <TrendingNotesWidget key={widget.id} params={widget} />;
case WidgetKinds.tmp.xfeed:
return <XhashtagWidget key={widget.id} params={widget} />;
case WidgetKinds.tmp.xhashtag:
return <XfeedsWidget key={widget.id} params={widget} />;
case WidgetKinds.tmp.xhashtag:
return <XhashtagWidget key={widget.id} params={widget} />;
case WidgetKinds.tmp.list:
return <WidgetList key={widget.id} params={widget} />;
case WidgetKinds.other.learnNostr:

View File

@ -12,7 +12,7 @@ import { useNostr } from '@utils/hooks/useNostr';
export function SplashScreen() {
const { db } = useStorage();
const { ndk } = useNDK();
const { fetchUserData, prefetchEvents } = useNostr();
const { fetchUserData } = useNostr();
const [isLoading, setIsLoading] = useState<boolean>(true);
@ -20,27 +20,8 @@ export function SplashScreen() {
await invoke('close_splashscreen');
};
const prefetch = async () => {
try {
const [user, events] = await Promise.all([fetchUserData(), prefetchEvents()]);
if (user.status === 'ok' && events.status === 'ok') {
// update last login = current time
await db.updateLastLogin();
// close splash screen and open main app screen
await invoke('close_splashscreen');
}
} catch (e) {
setIsLoading(false);
await message(e, {
title: 'An unexpected error has occurred',
type: 'error',
});
}
};
useEffect(() => {
async function initial() {
async function syncUserData() {
if (!db.account) {
await invoke('close_splashscreen');
} else {
@ -50,31 +31,47 @@ export function SplashScreen() {
if (step) {
await invoke('close_splashscreen');
} else {
console.log('prefetching...');
prefetch();
try {
const userData = await fetchUserData();
if (userData.status === 'ok') {
// update last login = current time
await db.updateLastLogin();
// close splash screen and open main app screen
await invoke('close_splashscreen');
}
} catch (e) {
setIsLoading(false);
await message(e, {
title: 'An unexpected error has occurred',
type: 'error',
});
}
}
}
}
if (ndk) {
initial();
syncUserData();
}
}, [ndk, db.account]);
return (
<div className="relative flex h-screen w-screen items-center justify-center bg-black">
<div data-tauri-drag-region className="absolute left-0 top-0 z-10 h-11 w-full" />
<div className="flex min-h-0 w-full flex-1 items-center justify-center">
<div className="flex min-h-0 w-full flex-1 items-center justify-center px-8">
<div className="flex flex-col items-center justify-center gap-4">
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
{isLoading ? (
<div className="flex flex-col gap-1 text-center">
<div className="flex flex-col gap-2 text-center">
<h3 className="text-lg font-semibold leading-none text-white">
{!ndk ? 'Connecting to relay...' : 'Fetching events from the last login.'}
{!ndk ? 'Connecting to relay...' : 'Syncing user data...'}
</h3>
<p className="text-sm text-white/50">
This may take a few seconds, please don&apos;t close app.
</p>
{ndk ? (
<p className="text-sm text-white/50">
Ensure all your data is sync across all Nostr clients, it may take a few
seconds, please don&apos;t close app.
</p>
) : null}
</div>
) : (
<div className="mt-2 flex flex-col gap-1 text-center">

View File

@ -71,7 +71,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
<div className="flex flex-col items-center gap-1">
<div className="inline-flex flex-col items-center gap-1.5">
<h5 className="text-center text-xl font-semibold leading-none">
{user.display_name || user.displayName || user.name || 'No name'}
{user.name || user.display_name || user.displayName || 'No name'}
</h5>
{user.nip05 ? (
<NIP05

View File

@ -1,8 +1,8 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { VList } from 'virtua';
import { UserProfile } from '@app/users/components/profile';
@ -32,79 +32,35 @@ export function UserScreen() {
return [...events] as unknown as NDKEvent[];
});
const parentRef = useRef();
const virtualizer = useVirtualizer({
count: data ? data.length : 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={event.id} event={event} />
</div>
);
return <Repost key={event.id} event={event} />;
case 1063:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@ -112,10 +68,7 @@ export function UserScreen() {
);
return (
<div
ref={parentRef}
className="scrollbar-hide relative h-full w-full overflow-y-auto bg-white/10 backdrop-blur-xl"
>
<div className="scrollbar-hide relative h-full w-full overflow-y-auto bg-white/10 backdrop-blur-xl">
<div data-tauri-drag-region className="absolute left-0 top-0 h-11 w-full" />
<UserProfile pubkey={pubkey} />
<div className="mt-6 h-full w-full border-t border-white/5 px-1.5">
@ -129,7 +82,7 @@ export function UserScreen() {
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
) : data.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
@ -140,22 +93,10 @@ export function UserScreen() {
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
<VList className="scrollbar-hide h-full">
{data.map((item) => renderItem(item))}
<div className="h-16" />
</VList>
)}
</div>
</div>

View File

@ -2,6 +2,10 @@
@tailwind components;
@tailwind utilities;
.border {
background-clip: padding-box;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}

View File

@ -1,50 +1,55 @@
// inspire by: https://github.com/nostr-dev-kit/ndk-react/
import NDK from '@nostr-dev-kit/ndk';
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
import { message } from '@tauri-apps/api/dialog';
import { fetch } from '@tauri-apps/api/http';
import { NostrFetcher } from 'nostr-fetch';
import { useEffect, useMemo, useState } from 'react';
import TauriAdapter from '@libs/ndk/cache';
import { useStorage } from '@libs/storage/provider';
export const NDKInstance = () => {
const { db } = useStorage();
const [ndk, setNDK] = useState<NDK | undefined>(undefined);
const [relayUrls, setRelayUrls] = useState<string[]>([]);
const { db } = useStorage();
const cacheAdapter = useMemo(() => new TauriAdapter(), [ndk]);
const cacheAdapter = useMemo(() => new TauriAdapter(), []);
const fetcher = useMemo(
() => (ndk ? NostrFetcher.withCustomPool(ndkAdapter(ndk)) : null),
[ndk]
);
// TODO: fully support NIP-11
async function getExplicitRelays() {
try {
// get relays
const relays = await db.getExplicitRelayUrls();
const requests = relays.map((relay) => {
const onlineRelays = new Set(relays);
for (const relay of relays) {
const url = new URL(relay);
return fetch(`https://${url.hostname + url.pathname}`, {
method: 'GET',
timeout: 10,
headers: {
Accept: 'application/nostr+json',
},
});
});
try {
const res = await fetch(`https://${url.hostname}`, {
method: 'GET',
timeout: { secs: 5, nanos: 0 },
headers: {
Accept: 'application/nostr+json',
},
});
const responses = await Promise.all(requests);
const successes = responses.filter((res) => res.ok);
const verifiedRelays: string[] = successes.map((res) => {
// TODO: support payment
// @ts-expect-error, not have type yet
if (!res.data.limitation?.payment_required) {
const url = new URL(res.url);
if (url.protocol === 'http:') return `ws://${url.hostname + url.pathname}`;
if (url.protocol === 'https:') return `wss://${url.hostname + url.pathname}`;
if (!res.ok) {
console.info(`${relay} is not working, skipping...`);
onlineRelays.delete(relay);
}
} catch {
console.warn(`${relay} is not working, skipping...`);
onlineRelays.delete(relay);
}
});
}
// return all validated relays
return verifiedRelays;
// return all online relays
return [...onlineRelays];
} catch (e) {
console.error(e);
}
@ -81,5 +86,6 @@ export const NDKInstance = () => {
return {
ndk,
relayUrls,
fetcher,
};
};

View File

@ -1,5 +1,6 @@
// source: https://github.com/nostr-dev-kit/ndk-react/
import NDK from '@nostr-dev-kit/ndk';
import { NostrFetcher } from 'nostr-fetch';
import { PropsWithChildren, createContext, useContext } from 'react';
import { NDKInstance } from '@libs/ndk/instance';
@ -7,21 +8,24 @@ import { NDKInstance } from '@libs/ndk/instance';
interface NDKContext {
ndk: undefined | NDK;
relayUrls: string[];
fetcher: NostrFetcher;
}
const NDKContext = createContext<NDKContext>({
ndk: undefined,
relayUrls: [],
fetcher: undefined,
});
const NDKProvider = ({ children }: PropsWithChildren<object>) => {
const { ndk, relayUrls } = NDKInstance();
const { ndk, relayUrls, fetcher } = NDKInstance();
return (
<NDKContext.Provider
value={{
ndk,
relayUrls,
fetcher,
}}
>
{children}

View File

@ -1,4 +1,4 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk';
import { BaseDirectory, removeFile } from '@tauri-apps/api/fs';
import { Platform } from '@tauri-apps/api/os';
import Database from 'tauri-plugin-sql-api';
@ -6,19 +6,20 @@ import { Stronghold } from 'tauri-plugin-stronghold-api';
import { FULL_RELAYS } from '@stores/constants';
import { toRawEvent } from '@utils/rawEvent';
import { Account, DBEvent, Relays, Widget } from '@utils/types';
export class LumeStorage {
public db: Database;
public platform: Platform;
public secureDB: Stronghold;
public account: Account | null = null;
public account: Account | null;
public platform: Platform | null;
constructor(sqlite: Database, platform: Platform, stronghold?: Stronghold) {
constructor(sqlite: Database, platform?: Platform, stronghold?: Stronghold) {
this.db = sqlite;
this.platform = platform ?? undefined;
this.secureDB = stronghold ?? undefined;
this.account = null;
this.platform = platform ?? undefined;
}
private async getSecureClient(key?: string) {
@ -58,6 +59,13 @@ export class LumeStorage {
return await removeFile('lume.stronghold', { dir: BaseDirectory.AppConfig });
}
public async checkAccount() {
const result: Array<Account> = await this.db.select(
'SELECT * FROM accounts WHERE is_active = 1;'
);
return result.length > 0;
}
public async getActiveAccount() {
const results: Array<Account> = await this.db.select(
'SELECT * FROM accounts WHERE is_active = "1" ORDER BY id DESC LIMIT 1;'
@ -282,6 +290,38 @@ export class LumeStorage {
return results.length < 1;
}
public async createMetadata(event: NDKEvent) {
const rawEvent = toRawEvent(event);
return await this.db.execute(
'INSERT OR IGNORE INTO metadata (id, event, author, kind, created_at) VALUES ($1, $2, $3, $4, $5);',
[
rawEvent.id,
JSON.stringify(rawEvent),
rawEvent.pubkey,
rawEvent.kind,
rawEvent.created_at,
]
);
}
public async createProfile(pubkey: string, profile: NDKUserProfile) {
return await this.db.execute(
'INSERT OR REPLACE INTO metadata (id, event, author, kind, created_at) VALUES ($1, $2, $3, $4, $5);',
[pubkey, JSON.stringify(profile), pubkey, 0, Math.round(Date.now() / 1000)]
);
}
public async getMetadataByPubkey(pubkey: string) {
const results: DBEvent[] = await this.db.select(
'SELECT * FROM metadata WHERE author = $1 AND kind = "0" LIMIT 1;',
[pubkey]
);
if (results.length < 1) return null;
return JSON.parse(results[0].event as string) as NDKEvent;
}
public async getExplicitRelayUrls() {
if (!this.account) return FULL_RELAYS;
@ -311,6 +351,10 @@ export class LumeStorage {
}
public async accountLogout() {
// delete all events
await this.db.execute('DELETE FROM events WHERE account_id = $1;', [this.account.id]);
// update current account status
await this.db.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [
this.account.id,
]);

View File

@ -1,6 +1,5 @@
import { message } from '@tauri-apps/api/dialog';
import { platform } from '@tauri-apps/api/os';
import { appConfigDir } from '@tauri-apps/api/path';
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
@ -17,15 +16,12 @@ const StorageContext = createContext<StorageContext>({
const StorageProvider = ({ children }: PropsWithChildren<object>) => {
const [db, setDB] = useState<LumeStorage>(undefined);
async function initLumeStorage() {
const initLumeStorage = async () => {
try {
const dir = await appConfigDir();
const sqlite = await Database.load('sqlite:lume.db');
const platformName = await platform();
const lumeStorage = new LumeStorage(sqlite, platformName);
console.log('App config dir: ', dir);
if (!lumeStorage.account) await lumeStorage.getActiveAccount();
setDB(lumeStorage);
} catch (e) {
@ -34,7 +30,7 @@ const StorageProvider = ({ children }: PropsWithChildren<object>) => {
type: 'error',
});
}
}
};
useEffect(() => {
if (!db) initLumeStorage();

View File

@ -1,10 +1,12 @@
import { NDKFilter, NDKKind } from '@nostr-dev-kit/ndk';
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import { AccountMoreActions } from '@shared/accounts/more';
import { SettingsIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { Logout } from '@shared/logout';
import { useActivities } from '@stores/activities';
@ -23,34 +25,31 @@ export function ActiveAccount() {
useEffect(() => {
const filter: NDKFilter = {
'#p': [db.account.pubkey],
kinds: [
NDKKind.Text,
NDKKind.Contacts,
NDKKind.Repost,
NDKKind.Reaction,
NDKKind.Zap,
],
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
since: Math.floor(Date.now() / 1000),
};
sub(filter, async (event) => {
addActivity(event);
sub(
filter,
async (event) => {
console.log('new notify: ', event);
addActivity(event);
switch (event.kind) {
case NDKKind.Text:
return await sendNativeNotification('Mention');
case NDKKind.Contacts:
return await sendNativeNotification("You've a new follower");
case NDKKind.Repost:
return await sendNativeNotification('Repost');
case NDKKind.Reaction:
return await sendNativeNotification('Reaction');
case NDKKind.Zap:
return await sendNativeNotification('Zap');
default:
break;
}
});
switch (event.kind) {
case NDKKind.Text:
return await sendNativeNotification('Mention');
case NDKKind.Repost:
return await sendNativeNotification('Repost');
case NDKKind.Reaction:
return await sendNativeNotification('Reaction');
case NDKKind.Zap:
return await sendNativeNotification('Zap');
default:
break;
}
},
false
);
}, []);
if (status === 'loading') {
@ -63,23 +62,31 @@ export function ActiveAccount() {
}
return (
<div className="flex h-16 items-center justify-between border-l-2 border-transparent pb-2 pl-4 pr-2">
<div className="flex items-center gap-2.5">
<div className="flex h-16 items-center justify-between border-l-2 border-transparent pb-2 pl-4 pr-3">
<Link to={`/users/${db.account.pubkey}`} className="flex items-center gap-1.5">
<Image
src={user?.picture || user?.image}
alt={db.account.npub}
className="h-10 w-10 shrink-0 rounded-lg object-cover"
className="h-9 w-9 shrink-0 rounded-lg object-cover"
/>
<div className="flex w-full flex-1 flex-col items-start gap-1.5">
<p className="max-w-[10rem] truncate font-bold leading-none text-white">
{user?.name || user?.display_name}
<div className="flex w-full flex-1 flex-col items-start gap-0.5">
<p className="max-w-[10rem] truncate font-semibold leading-none text-white">
{user?.name || user?.display_name || user?.displayName}
</p>
<span className="max-w-[8rem] truncate text-sm leading-none text-white/50">
{displayNpub(db.account.pubkey, 16)}
<span className="max-w-[7rem] truncate text-sm leading-none text-white/50">
{user?.nip05 || displayNpub(db.account.pubkey, 12)}
</span>
</div>
</Link>
<div className="inline-flex divide-x divide-white/5 rounded-lg border-t border-white/10 bg-white/20">
<Link
to="/settings/"
className="inline-flex h-9 w-9 items-center justify-center hover:bg-white/10"
>
<SettingsIcon className="h-4 w-4 text-white" />
</Link>
<Logout />
</div>
<AccountMoreActions pubkey={db.account.pubkey} />
</div>
);
}

View File

@ -3,4 +3,6 @@ export * from './modal';
export * from './composer';
export * from './mention/item';
export * from './mention/popup';
export * from './mention/suggestion';
export * from './mention/inlineList';
export * from './mediaUploader';

View File

@ -0,0 +1,83 @@
import { type SuggestionProps } from '@tiptap/suggestion';
import {
ForwardedRef,
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import { twMerge } from 'tailwind-merge';
import { MentionItem } from '@shared/composer';
export const MentionInlineList = forwardRef(
(props: SuggestionProps, ref: ForwardedRef<unknown>) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index) => {
const item = props.items[index];
if (item) {
props.command({ id: item });
}
};
const upHandler = () => {
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
};
const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length);
};
const enterHandler = () => {
selectItem(selectedIndex);
};
useEffect(() => setSelectedIndex(0), [props.items]);
useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (event.key === 'ArrowUp') {
upHandler();
return true;
}
if (event.key === 'ArrowDown') {
downHandler();
return true;
}
if (event.key === 'Enter') {
enterHandler();
return true;
}
return false;
},
}));
return (
<div className="flex w-[250px] flex-col rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
{props.items.length ? (
props.items.map((item: string, index: number) => (
<button
className={twMerge(
'h-11 w-full rounded-lg px-2 text-start text-sm font-medium hover:bg-white/10',
`${index === selectedIndex ? 'is-selected' : ''}`
)}
key={index}
onClick={() => selectItem(index)}
>
<MentionItem embed={item} />
</button>
))
) : (
<div>No result</div>
)}
</div>
);
}
);
MentionInlineList.displayName = 'MentionList';

View File

@ -3,12 +3,12 @@ import { Image } from '@shared/image';
import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey';
export function MentionItem({ pubkey }: { pubkey: string }) {
const { status, user } = useProfile(pubkey);
export function MentionItem({ pubkey, embed }: { pubkey: string; embed?: string }) {
const { status, user } = useProfile(pubkey, embed);
if (status === 'loading') {
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2.5 px-2">
<div className="relative h-8 w-8 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
<span className="h-4 w-1/2 animate-pulse rounded bg-white/10 backdrop-blur-xl" />

View File

@ -0,0 +1,68 @@
import { ReactRenderer } from '@tiptap/react';
import tippy from 'tippy.js';
import { MentionInlineList } from '@shared/composer';
export const Suggestion = {
items: async ({ query }) => {
const users = [];
return users
.filter((item) => item.ident.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5);
},
render: () => {
let component;
let popup;
return {
onStart: (props) => {
component = new ReactRenderer(MentionInlineList, {
props,
editor: props.editor,
});
if (!props.clientRect) {
return;
}
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
});
},
onUpdate(props) {
component.updateProps(props);
if (!props.clientRect) {
return;
}
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup[0].hide();
return true;
}
return component.ref?.onKeyDown(props);
},
onExit() {
popup[0].destroy();
component.destroy();
},
};
},
};

View File

@ -28,12 +28,10 @@ export function ComposerModal() {
<Dialog.Trigger asChild>
<button
type="button"
className="flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 border-transparent pl-4 pr-2 text-white/80"
className="flex h-9 items-center gap-2 rounded-full border-t border-white/10 bg-white/20 px-4 text-sm font-semibold text-white/80 hover:bg-fuchsia-500 hover:text-white"
>
<span className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<ComposeIcon className="h-4 w-4 text-white" />
</span>
New postr
New
<ComposeIcon className="h-4 w-4 text-white" />
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">

View File

@ -14,7 +14,7 @@ export function ComposerUser({ pubkey }: { pubkey: string }) {
className="h-10 w-10 shrink-0 rounded-lg"
/>
<h5 className="text-base font-semibold leading-none text-white">
{user?.name || user?.display_name || displayNpub(pubkey, 16)}
{user?.name || user?.display_name || user?.displayName || displayNpub(pubkey, 16)}
</h5>
</div>
);

20
src/shared/icons/dots.tsx Normal file
View File

@ -0,0 +1,20 @@
import { SVGProps } from 'react';
export function DotsPattern(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg {...props}>
<pattern
id="pattern-circles"
width="30"
height="30"
x="0"
y="0"
patternContentUnits="userSpaceOnUse"
patternUnits="userSpaceOnUse"
>
<circle cx="2" cy="2" r="1.626" fill="currentColor"></circle>
</pattern>
<rect width="100%" height="100%" x="0" y="0" fill="url(#pattern-circles)"></rect>
</svg>
);
}

View File

@ -64,3 +64,5 @@ export * from './follows';
export * from './alby';
export * from './stars';
export * from './nwc';
export * from './timeline';
export * from './dots';

View File

@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function TimeLineIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="1.5"
d="M22.25 15C17.215 15 15 17.215 15 22.25 15 17.215 12.785 15 7.75 15 12.785 15 15 12.785 15 7.75c0 5.035 2.215 7.25 7.25 7.25zM11.25 6.5c-3.299 0-4.75 1.451-4.75 4.75 0-3.299-1.451-4.75-4.75-4.75 3.299 0 4.75-1.451 4.75-4.75 0 3.299 1.451 4.75 4.75 4.75z"
clipRule="evenodd"
></path>
</svg>
);
}

View File

@ -3,6 +3,8 @@ import { useNavigate } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import { LogoutIcon } from '@shared/icons';
import { useStronghold } from '@stores/stronghold';
export function Logout() {
@ -25,12 +27,12 @@ export function Logout() {
<AlertDialog.Trigger asChild>
<button
type="button"
className="inline-flex h-10 items-center rounded-lg px-2 text-sm font-medium text-white hover:bg-white/10"
className="inline-flex h-9 w-9 items-center justify-center hover:bg-white/10"
>
Logout
<LogoutIcon className="h-4 w-4 text-white" />
</button>
</AlertDialog.Trigger>
<AlertDialog.Portal className="relative z-10">
<AlertDialog.Portal>
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
<AlertDialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-md rounded-xl bg-white/10 backdrop-blur-xl">

View File

@ -4,12 +4,10 @@ import { twMerge } from 'tailwind-merge';
import { ChatsList } from '@app/chats/components/list';
import { useStorage } from '@libs/storage/provider';
import { ActiveAccount } from '@shared/accounts/active';
import { ComposerModal } from '@shared/composer';
import { Frame } from '@shared/frame';
import { BellIcon, NavArrowDownIcon, NwcIcon, SpaceIcon } from '@shared/icons';
import { BellIcon, NavArrowDownIcon, NwcIcon, SpaceIcon, WorldIcon } from '@shared/icons';
import { useActivities } from '@stores/activities';
import { useSidebar } from '@stores/sidebar';
@ -17,9 +15,8 @@ import { useSidebar } from '@stores/sidebar';
import { compactNumber } from '@utils/number';
export function Navigation() {
const { db } = useStorage();
const totalNewActivities = useActivities((state) => state.totalNewActivities);
const [totalNewActivities] = useActivities((state) => [state.totalNewActivities]);
const [chats, toggleChats] = useSidebar((state) => [state.chats, state.toggleChats]);
const [integrations, toggleIntegrations] = useSidebar((state) => [
state.integrations,
@ -27,19 +24,27 @@ export function Navigation() {
]);
return (
<Frame className="relative flex h-full w-[232px] flex-col" lighter>
{db.platform === 'darwin' ? (
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
) : null}
<div className="scrollbar-hide flex h-full flex-1 flex-col gap-6 overflow-y-auto pb-32">
<div className="flex flex-col pr-2">
<ComposerModal />
<Frame
className="relative flex h-full w-[232px] flex-col border-r border-white/5"
lighter
>
<div
data-tauri-drag-region
className="inline-flex h-16 w-full items-center justify-end px-3"
>
<ComposerModal />
</div>
<div
data-tauri-drag-region
className="scrollbar-hide flex h-full flex-1 flex-col gap-6 overflow-y-auto pb-32"
>
<div className="flex flex-col pr-3">
<NavLink
to="/"
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-2',
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-3',
isActive
? 'border-fuchsia-500 bg-white/5 text-white'
: 'border-transparent text-white/70'
@ -49,14 +54,31 @@ export function Navigation() {
<span className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<SpaceIcon className="h-4 w-4 text-white" />
</span>
Space
Home
</NavLink>
<NavLink
to="/browse/"
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-3',
isActive
? 'border-fuchsia-500 bg-white/5 text-white'
: 'border-transparent text-white/70'
)
}
>
<span className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<WorldIcon className="h-4 w-4 text-white" />
</span>
Browse
</NavLink>
<NavLink
to="/notifications"
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
'flex h-10 items-center justify-between rounded-r-lg border-l-2 pl-4 pr-2',
'flex h-10 items-center justify-between rounded-r-lg border-l-2 pl-4 pr-3',
isActive
? 'border-fuchsia-500 bg-white/5 text-white'
: 'border-transparent text-white/70'
@ -81,7 +103,7 @@ export function Navigation() {
</NavLink>
</div>
<Collapsible.Root open={integrations} onOpenChange={toggleIntegrations}>
<div className="flex flex-col gap-1 pr-2">
<div className="flex flex-col gap-1 pr-3">
<Collapsible.Trigger asChild>
<button className="flex items-center gap-1 pl-[20px] pr-4">
<div
@ -103,7 +125,7 @@ export function Navigation() {
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-2',
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-3',
isActive
? 'border-fuchsia-500 bg-white/5 text-white'
: 'border-transparent text-white/70'
@ -119,7 +141,7 @@ export function Navigation() {
</div>
</Collapsible.Root>
<Collapsible.Root open={chats} onOpenChange={toggleChats}>
<div className="flex flex-col gap-1 pr-2">
<div className="flex flex-col gap-1 pr-3">
<Collapsible.Trigger asChild>
<button className="flex items-center gap-1 pl-[20px] pr-4">
<div

View File

@ -45,7 +45,7 @@ export function NoteActions({
to={`/notes/text/${id}`}
className="group inline-flex h-7 w-7 items-center justify-center"
>
<FocusIcon className="h-5 w-5 text-white group-hover:text-fuchsia-400" />
<FocusIcon className="h-5 w-5 text-white/80 group-hover:text-fuchsia-400" />
</Link>
</Tooltip.Trigger>
<Tooltip.Portal>
@ -68,7 +68,7 @@ export function NoteActions({
}
className="group inline-flex h-7 w-7 items-center justify-center"
>
<ThreadIcon className="h-5 w-5 text-white group-hover:text-fuchsia-400" />
<ThreadIcon className="h-5 w-5 text-white/80 group-hover:text-fuchsia-400" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>

View File

@ -30,9 +30,9 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
<DropdownMenu.Trigger asChild>
<button
type="button"
className="group ml-auto inline-flex h-7 w-7 items-center justify-center"
className="group ml-auto inline-flex h-7 w-7 items-center justify-center text-white/80"
>
<HorizontalDotsIcon className="h-5 w-5 text-white group-hover:text-fuchsia-400" />
<HorizontalDotsIcon className="h-5 w-5 text-white/80 group-hover:text-fuchsia-400" />
</button>
</DropdownMenu.Trigger>
</Tooltip.Trigger>

View File

@ -62,12 +62,12 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
<Popover.Trigger asChild>
<button
type="button"
className="group inline-flex h-7 w-7 items-center justify-center"
className="group inline-flex h-7 w-7 items-center justify-center text-white/80"
>
{reaction ? (
<img src={getReactionImage(reaction)} alt={reaction} className="h-6 w-6" />
) : (
<ReactionIcon className="h-5 w-5 text-white group-hover:text-red-400" />
<ReactionIcon className="h-5 w-5 text-white/80 group-hover:text-red-400" />
)}
</button>
</Popover.Trigger>

View File

@ -21,9 +21,9 @@ export function NoteReply({
<button
type="button"
onClick={() => setReply(id, pubkey, root)}
className="group inline-flex h-7 w-7 items-center justify-center"
className="group inline-flex h-7 w-7 items-center justify-center text-white/80"
>
<ReplyIcon className="h-5 w-5 text-white group-hover:text-green-500" />
<ReplyIcon className="h-5 w-5 text-white/80 group-hover:text-green-500" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>

View File

@ -44,12 +44,12 @@ export function NoteRepost({ id, pubkey }: { id: string; pubkey: string }) {
<AlertDialog.Trigger asChild>
<button
type="button"
className="group inline-flex h-7 w-7 items-center justify-center"
className="group inline-flex h-7 w-7 items-center justify-center text-white/80"
>
<RepostIcon
className={twMerge(
'h-5 w-5 group-hover:text-blue-400',
isRepost ? 'text-blue-400' : 'text-white'
'h-5 w-5 group-hover:text-blue-500',
isRepost ? 'text-blue-500' : 'text-white/80'
)}
/>
</button>

View File

@ -60,7 +60,7 @@ export function NoteZap({ id, pubkey }: { id: string; pubkey: string }) {
if (send) {
await sendNativeNotification(
`You've tipped ${compactNumber.format(send.amount)} sats to ${
user?.display_name || user?.name
user?.name || user?.display_name || user?.displayName
}`
);
@ -94,9 +94,9 @@ export function NoteZap({ id, pubkey }: { id: string; pubkey: string }) {
<Dialog.Trigger asChild>
<button
type="button"
className="group inline-flex h-7 w-7 items-center justify-center"
className="group inline-flex h-7 w-7 items-center justify-center text-white/80"
>
<ZapIcon className="h-5 w-5 text-white group-hover:text-orange-400" />
<ZapIcon className="h-5 w-5 text-white/80 group-hover:text-orange-400" />
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
@ -106,7 +106,7 @@ export function NoteZap({ id, pubkey }: { id: string; pubkey: string }) {
<div className="inline-flex w-full shrink-0 items-center justify-between px-5 py-3">
<div className="w-6" />
<Dialog.Title className="text-center text-sm font-semibold leading-none text-white">
Send tip to {user?.display_name || user?.name}
Send tip to {user?.name || user?.display_name || user?.displayName}
</Dialog.Title>
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
<CancelIcon className="h-4 w-4 text-white/50" />

View File

@ -56,8 +56,8 @@ export function ChildNote({ id, root }: { id: string; root?: string }) {
Lume <span className="text-green-500">(System)</span>
</h5>
</div>
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="-mt-5 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div>
<div className="relative z-20 mt-1 flex-1 select-text">
<div className="mb-1 select-text rounded-lg bg-white/5 p-1.5 text-sm">
@ -78,11 +78,11 @@ export function ChildNote({ id, root }: { id: string; root?: string }) {
return (
<>
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" />
<div className="mb-5 flex flex-col">
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.6rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" />
<div className="mb-6 flex flex-col">
<User pubkey={data.pubkey} time={data.created_at} />
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="-mt-5 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div className="relative z-20 flex-1">
{renderKind(data)}
<NoteActions id={data.id} pubkey={data.pubkey} root={root} />

View File

@ -22,5 +22,6 @@ export * from './child';
export * from './skeleton';
export * from './actions';
export * from './mentions/hashtag';
export * from './mentions/boost';
export * from './stats';
export * from './wrapper';

View File

@ -4,19 +4,19 @@ import { Link } from 'react-router-dom';
import { Image } from '@shared/image';
export function ArticleNote({ event }: { event: NDKEvent }) {
export function ArticleNote(props: { event?: NDKEvent }) {
const metadata = useMemo(() => {
const title = event.tags.find((tag) => tag[0] === 'title')?.[1];
const image = event.tags.find((tag) => tag[0] === 'image')?.[1];
const summary = event.tags.find((tag) => tag[0] === 'summary')?.[1];
const title = props.event.tags.find((tag) => tag[0] === 'title')?.[1];
const image = props.event.tags.find((tag) => tag[0] === 'image')?.[1];
const summary = props.event.tags.find((tag) => tag[0] === 'summary')?.[1];
let publishedAt: Date | string | number = event.tags.find(
let publishedAt: Date | string | number = props.event.tags.find(
(tag) => tag[0] === 'published_at'
)?.[1];
if (publishedAt) {
publishedAt = new Date(parseInt(publishedAt)).toLocaleDateString('en-US');
} else {
publishedAt = new Date(event.created_at * 1000).toLocaleDateString('en-US');
publishedAt = new Date(props.event.created_at * 1000).toLocaleDateString('en-US');
}
return {
@ -25,15 +25,11 @@ export function ArticleNote({ event }: { event: NDKEvent }) {
publishedAt,
summary,
};
}, [event.id]);
}, [props.event.id]);
return (
<Link
to={`/notes/article/${event.id}`}
preventScrollReset={true}
className="mb-2 mt-3 rounded-lg"
>
<div className="flex flex-col rounded-lg">
<Link to={`/notes/article/${props.event.id}`} preventScrollReset={true}>
<div className="mb-2 mt-3 flex flex-col rounded-lg">
{metadata.image && (
<Image
src={metadata.image}

View File

@ -6,8 +6,8 @@ import { Image } from '@shared/image';
import { fileType } from '@utils/nip94';
export function FileNote({ event }: { event: NDKEvent }) {
const url = event.tags.find((el) => el[0] === 'url')[1];
export function FileNote(props: { event?: NDKEvent }) {
const url = props.event.tags.find((el) => el[0] === 'url')[1];
const type = fileType(url);
if (type === 'image') {
@ -15,7 +15,7 @@ export function FileNote({ event }: { event: NDKEvent }) {
<div className="mb-2 mt-3">
<Image
src={url}
alt={event.content}
alt={props.event.content}
className="h-auto w-full rounded-lg object-cover"
/>
</div>

View File

@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools';
import { useCallback } from 'react';
import { Link } from 'react-router-dom';
import { twMerge } from 'tailwind-merge';
import { useNDK } from '@libs/ndk/provider';
@ -17,7 +18,13 @@ import {
} from '@shared/notes';
import { User } from '@shared/user';
export function Repost({ event }: { event: NDKEvent }) {
export function Repost({
event,
lighter = false,
}: {
event: NDKEvent;
lighter?: boolean;
}) {
const embedEvent: null | NDKEvent =
event.content.length > 0 ? JSON.parse(event.content) : null;
@ -55,12 +62,17 @@ export function Repost({ event }: { event: NDKEvent }) {
if (embedEvent) {
return (
<div className="h-min w-full px-3 pb-3">
<div className="relative flex flex-col gap-10 overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<div
className={twMerge(
'relative flex flex-col gap-1 overflow-hidden rounded-xl px-3 py-3',
!lighter ? 'bg-white/10 backdrop-blur-xl' : ''
)}
>
<User pubkey={event.pubkey} time={event.created_at} variant="repost" />
<div className="relative flex flex-col">
<User pubkey={embedEvent.pubkey} time={embedEvent.created_at} />
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="-mt-5 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div className="relative z-20 flex-1">
{renderKind(embedEvent)}
<NoteActions id={embedEvent.id} pubkey={embedEvent.pubkey} />
@ -89,7 +101,12 @@ export function Repost({ event }: { event: NDKEvent }) {
return (
<div className="h-min w-full px-3 pb-3">
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<div
className={twMerge(
'relative overflow-hidden rounded-xl px-3 py-3',
!lighter ? 'bg-white/10 backdrop-blur-xl' : ''
)}
>
<div className="relative flex flex-col">
<div className="relative z-10 flex items-start gap-3">
<div className="inline-flex h-11 w-11 items-end justify-center rounded-lg bg-black pb-1">
@ -122,12 +139,17 @@ export function Repost({ event }: { event: NDKEvent }) {
return (
<div className="h-min w-full px-3 pb-3">
<div className="relative flex flex-col gap-10 overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<div
className={twMerge(
'relative flex flex-col gap-1 overflow-hidden rounded-xl px-3 py-3',
!lighter ? 'bg-white/10 backdrop-blur-xl' : ''
)}
>
<User pubkey={event.pubkey} time={event.created_at} variant="repost" />
<div className="relative flex flex-col">
<User pubkey={data.pubkey} time={data.created_at} />
<div className="-mt-2 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="-mt-5 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div className="relative z-20 flex-1">
{renderKind(data)}
<NoteActions id={data.id} pubkey={data.pubkey} />

View File

@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import remarkGfm from 'remark-gfm';
import {
Boost,
Hashtag,
ImagePreview,
LinkPreview,
@ -13,8 +14,8 @@ import {
import { parser } from '@utils/parser';
export function TextNote({ content }: { content: string }) {
const richContent = parser(content) ?? null;
export function TextNote(props: { content?: string }) {
const richContent = parser(props.content) ?? null;
if (!richContent) {
return (
@ -26,7 +27,7 @@ export function TextNote({ content }: { content: string }) {
unwrapDisallowed={true}
linkTarget={'_blank'}
>
{content}
{props.content}
</ReactMarkdown>
</div>
);
@ -56,6 +57,9 @@ export function TextNote({ content }: { content: string }) {
if (key.startsWith('tag')) {
return <Hashtag tag={key.replace('tag-', '')} />;
}
if (key.startsWith('boost')) {
return <Boost boost={key.replace('boost-', '')} />;
}
},
}}
disallowedElements={['h1', 'h2', 'h3', 'h4', 'h5', 'h6']}

View File

@ -1,18 +1,18 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
export function UnknownNote({ event }: { event: NDKEvent }) {
export function UnknownNote(props: { event?: NDKEvent }) {
return (
<div className="flex w-full flex-col gap-2">
<div className="inline-flex flex-col gap-1 rounded-md bg-white/10 px-2 py-2 backdrop-blur-xl">
<span className="text-sm font-medium leading-none text-white/50">
Unknown kind: {event.kind}
Unknown kind: {props.event.kind}
</span>
<p className="text-sm leading-none text-white">
Lume isn&apos;t fully support this kind
</p>
</div>
<div className="select-text whitespace-pre-line break-all text-white">
<p>{event.content.toString()}</p>
<p>{props.event.content.toString()}</p>
</div>
</div>
);

View File

@ -0,0 +1,5 @@
export function Boost({ boost }: { boost: string }) {
return (
<span className="break-words text-fuchsia-400 hover:text-fuchsia-500">{boost}</span>
);
}

View File

@ -1,11 +1,12 @@
import { memo } from 'react';
import { useStorage } from '@libs/storage/provider';
import { WidgetKinds, useWidgets } from '@stores/widgets';
import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey';
export function MentionUser({ pubkey }: { pubkey: string }) {
export const MentionUser = memo(function MentionUser({ pubkey }: { pubkey: string }) {
const { db } = useStorage();
const { user } = useProfile(pubkey);
@ -18,20 +19,25 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
onClick={() =>
setWidget(db, {
kind: WidgetKinds.local.user,
title: user?.name || user?.display_name,
title: user?.name || user?.display_name || user?.displayName,
content: pubkey,
})
}
onKeyDown={() =>
setWidget(db, {
kind: WidgetKinds.local.user,
title: user?.name || user?.display_name,
title: user?.name || user?.display_name || user?.displayName,
content: pubkey,
})
}
className="break-words text-fuchsia-400 hover:text-fuchsia-500"
>
{user?.name || user?.display_name || user?.username || displayNpub(pubkey, 16)}
{'@' +
(user?.name ||
user?.display_name ||
user?.displayName ||
user?.username ||
'unknown')}
</span>
);
}
});

View File

@ -16,7 +16,7 @@ export function VideoPreview({ urls }: { urls: string[] }) {
<img
src={`https://thumbnail.video/api/get?url=${url}&seconds=1`}
alt={url}
className="h-auto w-full bg-white object-cover"
className="aspect-video h-full w-full bg-white object-cover"
/>
}
/>

View File

@ -10,7 +10,7 @@ export function SubReply({ event }: { event: NDKEvent }) {
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="flex-1">
<TextNote content={event.content} />
<TextNote />
<NoteActions id={event.id} pubkey={event.pubkey} />
</div>
</div>

View File

@ -1,5 +1,6 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { ReactNode } from 'react';
import { ReactElement, cloneElement } from 'react';
import { twMerge } from 'tailwind-merge';
import { ChildNote, NoteActions } from '@shared/notes';
import { User } from '@shared/user';
@ -9,24 +10,34 @@ export function NoteWrapper({
children,
root,
reply,
lighter = false,
}: {
event: NDKEvent;
children: ReactNode;
children: ReactElement;
repost?: boolean;
root?: string;
reply?: string;
lighter?: boolean;
}) {
return (
<div className="h-min w-full px-3 pb-3">
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<div
className={twMerge(
'relative overflow-hidden rounded-xl px-3 py-4',
!lighter ? 'bg-white/10 backdrop-blur-xl' : 'bg-transparent'
)}
>
<div className="relative">{root && <ChildNote id={root} />}</div>
<div className="relative">{reply && <ChildNote id={reply} root={root} />}</div>
<div className="relative flex flex-col">
<User pubkey={event.pubkey} time={event.created_at} />
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="-mt-5 flex items-start gap-3">
<div className="w-10 shrink-0" />
<div className="relative z-20 flex-1">
{children}
{cloneElement(
children,
event.kind === 1 ? { content: event.content } : { event: event }
)}
<NoteActions id={event.id} pubkey={event.pubkey} />
</div>
</div>

View File

@ -1,10 +1,10 @@
import * as Popover from '@radix-ui/react-popover';
import * as HoverCard from '@radix-ui/react-hover-card';
import { memo } from 'react';
import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom';
import remarkGfm from 'remark-gfm';
import { WorldIcon } from '@shared/icons';
import { RepostIcon, WorldIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { NIP05 } from '@shared/nip05';
@ -20,13 +20,27 @@ export const User = memo(function User({
}: {
pubkey: string;
time?: number;
variant?: 'default' | 'simple' | 'mention' | 'repost' | 'chat' | 'large' | 'thread';
variant?:
| 'default'
| 'simple'
| 'mention'
| 'repost'
| 'chat'
| 'large'
| 'thread'
| 'avatar';
embedProfile?: string;
}) {
const { status, user } = useProfile(pubkey, embedProfile);
const createdAt = time ? formatCreatedAt(time, variant === 'chat') : 0;
if (status === 'loading') {
if (variant === 'avatar') {
return (
<div className="h-12 w-12 animate-pulse overflow-hidden rounded-lg bg-white/10 backdrop-blur-xl" />
);
}
if (variant === 'mention') {
return (
<div className="relative flex items-center gap-3">
@ -38,7 +52,7 @@ export const User = memo(function User({
return (
<div className="relative flex items-start gap-3">
<div className="relative z-10 h-11 w-11 shrink-0 animate-pulse overflow-hidden rounded-lg bg-white/10 backdrop-blur-xl" />
<div className="relative z-10 h-10 w-10 shrink-0 animate-pulse overflow-hidden rounded-lg bg-white/10 backdrop-blur-xl" />
<div className="h-3.5 w-36 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
</div>
);
@ -56,7 +70,10 @@ export const User = memo(function User({
</button>
<div className="flex flex-1 items-baseline gap-2">
<h5 className="max-w-[10rem] truncate font-semibold leading-none text-white">
{user?.display_name || user?.name || displayNpub(pubkey, 16)}
{user?.name ||
user?.display_name ||
user?.displayName ||
displayNpub(pubkey, 16)}
</h5>
<span className="leading-none text-white/50">·</span>
<span className="leading-none text-white/50">{createdAt}</span>
@ -76,7 +93,7 @@ export const User = memo(function User({
<div className="flex h-full flex-col items-start justify-between">
<div className="flex flex-col items-start gap-1 text-start">
<p className="max-w-[15rem] truncate text-lg font-semibold leading-none text-white">
{user?.name || user?.display_name}
{user?.name || user?.display_name || user?.displayName}
</p>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
@ -96,7 +113,7 @@ export const User = memo(function User({
className="inline-flex items-center gap-2 text-sm text-white/70"
>
<WorldIcon className="h-4 w-4" />
<p className="max-w-[10rem] truncate">{user.website}</p>
<p className="max-w-[10rem] truncate">{user?.website}</p>
</Link>
) : null}
</div>
@ -113,11 +130,11 @@ export const User = memo(function User({
alt={pubkey}
className="h-12 w-12 shrink-0 rounded-lg object-cover"
/>
<div className="flex w-full flex-col gap-1">
<div className="flex w-full flex-col items-start gap-1">
<h3 className="max-w-[15rem] truncate font-medium leading-none text-white">
{user?.name || user?.display_name}
{user?.name || user?.display_name || user?.displayName}
</h3>
<p className="text-sm leading-none text-white/70">
<p className="max-w-[10rem] truncate text-sm leading-none text-white/70">
{user?.nip05 || user?.username || displayNpub(pubkey, 16)}
</p>
</div>
@ -125,26 +142,39 @@ export const User = memo(function User({
);
}
if (variant === 'avatar') {
return (
<Image
src={user?.picture || user?.image}
alt={pubkey}
className="h-12 w-12 shrink-0 rounded-lg object-cover"
/>
);
}
if (variant === 'repost') {
return (
<>
<div className="flex gap-3">
<div className="flex gap-3">
<div className="inline-flex h-10 w-10 items-center justify-center">
<RepostIcon className="h-6 w-6 text-blue-500" />
</div>
<div className="inline-flex items-center gap-2">
<Image
src={user?.picture || user?.image}
alt={pubkey}
className="relative z-20 inline-block h-11 w-11 rounded-lg"
className="relative z-20 inline-block h-6 w-6 rounded"
/>
<div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[15rem] truncate font-semibold leading-none text-white">
{user?.display_name || user?.name || displayNpub(pubkey, 16)}
<h5 className="max-w-[10rem] truncate font-medium leading-none text-white/80">
{user?.name ||
user?.display_name ||
user?.displayName ||
displayNpub(pubkey, 16)}
</h5>
<span className="font-semibold text-fuchsia-500">reposted</span>
<span className="leading-none text-white/50">·</span>
<span className="leading-none text-white/50">{createdAt}</span>
<span className="text-blue-500">reposted</span>
</div>
</div>
<div className="absolute left-[28px] top-16 h-6 w-0.5 bg-gradient-to-t from-white/20 to-white/10" />
</>
</div>
);
}
@ -154,11 +184,11 @@ export const User = memo(function User({
<Image
src={user?.picture || user?.image}
alt={pubkey}
className="relative z-20 inline-block h-11 w-11 rounded-lg"
className="relative z-20 inline-block h-10 w-10 rounded-lg"
/>
<div className="flex flex-1 flex-col gap-2">
<h5 className="max-w-[15rem] truncate font-semibold leading-none text-white">
{user?.display_name || user?.name}
{user?.name || user?.display_name || user?.displayName}
</h5>
<div className="inline-flex items-center gap-2">
<span className="leading-none text-white/50">{createdAt}</span>
@ -171,48 +201,54 @@ export const User = memo(function User({
}
return (
<Popover.Root>
<HoverCard.Root>
<div className="relative z-10 flex items-start gap-3">
<Popover.Trigger asChild>
<HoverCard.Trigger asChild>
<button
type="button"
className="relative z-40 h-11 w-11 shrink-0 overflow-hidden"
className="relative z-40 h-10 w-10 shrink-0 overflow-hidden"
>
<Image
src={user?.picture || user?.image}
alt={pubkey}
className="h-11 w-11 rounded-lg object-cover"
className="h-10 w-10 rounded-lg object-cover"
/>
</button>
</Popover.Trigger>
</HoverCard.Trigger>
<div className="flex flex-1 items-baseline gap-2">
<h5 className="max-w-[15rem] truncate font-semibold leading-none text-white">
{user?.display_name || user?.name || displayNpub(pubkey, 16)}
{user?.name ||
user?.display_name ||
user?.displayName ||
displayNpub(pubkey, 16)}
</h5>
<span className="leading-none text-white/50">·</span>
<span className="leading-none text-white/50">{createdAt}</span>
</div>
</div>
<Popover.Portal>
<Popover.Content
className="w-[300px] overflow-hidden rounded-xl border border-white/10 bg-white/10 backdrop-blur-3xl focus:outline-none"
<HoverCard.Portal>
<HoverCard.Content
className="w-[300px] overflow-hidden rounded-xl border border-white/10 bg-white/10 backdrop-blur-3xl focus:outline-none data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=top]:animate-slideDownAndFade data-[state=open]:transition-all"
sideOffset={5}
>
<div className="flex gap-2.5 border-b border-white/5 px-3 py-3">
<Image
src={user?.picture || user?.image}
alt={pubkey}
className="h-11 w-11 shrink-0 rounded-lg object-cover"
className="h-10 w-10 shrink-0 rounded-lg object-cover"
/>
<div className="flex flex-1 flex-col gap-2">
<div className="inline-flex flex-col gap-1">
<h5 className="text-sm font-semibold leading-none">
{user?.display_name || user?.name || user?.username}
{user?.name ||
user?.display_name ||
user?.displayName ||
user?.username}
</h5>
{user.nip05 ? (
{user?.nip05 ? (
<NIP05
pubkey={pubkey}
nip05={user.nip05}
nip05={user?.nip05}
className="max-w-[15rem] truncate text-sm leading-none text-white/50"
/>
) : (
@ -242,8 +278,8 @@ export const User = memo(function User({
Message
</Link>
</div>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
);
});

View File

@ -0,0 +1,74 @@
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useStorage } from '@libs/storage/provider';
import { useStronghold } from '@stores/stronghold';
import { useNostr } from '@utils/hooks/useNostr';
export function EventLoader({ firstTime }: { firstTime: boolean }) {
const { db } = useStorage();
const { getAllEventsSinceLastLogin } = useNostr();
const setIsFetched = useStronghold((state) => state.setIsFetched);
const queryClient = useQueryClient();
const [progress, setProgress] = useState(0);
useEffect(() => {
async function getEvents() {
const events = await getAllEventsSinceLastLogin();
const promises = await Promise.all(
events.data.map(async (event) => await db.createEvent(event))
);
if (promises) {
setProgress(100);
setIsFetched();
// invalidate queries
queryClient.invalidateQueries(['local-network-widget']);
}
}
// only start download if progress === 0
if (progress === 0) getEvents();
// auto increase progress after 2 secs
setInterval(() => setProgress((prev) => (prev += 5)), 2000);
}, []);
return (
<div className="mb-3 px-3">
<div className="h-max w-full rounded-lg border-t border-white/10 bg-white/20 p-3">
<div className="flex flex-col items-center gap-3">
{firstTime ? (
<div>
<span className="text-4xl">👋</span>
<h3 className="mt-2 font-semibold leading-tight">
Hello, this is the first time you&apos;re using Lume
</h3>
<p className="text-sm text-white/70">
Lume is downloading all events since the last 24 hours. It will auto
refresh when it done, please be patient
</p>
</div>
) : (
<div className="text-center">
<h3 className="font-semibold leading-tight">
Downloading all events from your last login...
</h3>
</div>
)}
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-white/20">
<div
className="flex flex-col justify-center overflow-hidden bg-fuchsia-500 transition-all duration-1000 ease-smooth"
role="progressbar"
style={{ width: `${progress}%` }}
/>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,12 +1,13 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
import { ArticleNote, NoteSkeleton, NoteWrapper } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { Widget } from '@utils/types';
@ -24,71 +25,49 @@ export function GlobalArticlesWidget({ params }: { params: Widget }) {
{ refetchOnWindowFocus: false }
);
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: data ? data.length : 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(event: NDKEvent) => {
return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
},
[data]
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm font-medium text-white">
There have been no new articles in the last 24 hours.
) : data.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex flex-col items-center gap-4">
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center">
<h3 className="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
<VList className="scrollbar-hide h-full">
{data.map((item) => renderItem(item))}
<div className="h-16" />
</VList>
)}
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -1,12 +1,13 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
import { FileNote, NoteSkeleton, NoteWrapper } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { Widget } from '@utils/types';
@ -25,71 +26,49 @@ export function GlobalFilesWidget({ params }: { params: Widget }) {
{ refetchOnWindowFocus: false }
);
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: data ? data.length : 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(event: NDKEvent) => {
return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
},
[data]
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm font-medium text-white">
There have been no new files in the last 24 hours.
) : data.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex flex-col items-center gap-4">
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center">
<h3 className="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
<VList className="scrollbar-hide h-full">
{data.map((item) => renderItem(item))}
<div className="h-16" />
</VList>
)}
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
@ -15,6 +15,7 @@ import {
UnknownNote,
} from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { nHoursAgo } from '@utils/date';
import { Widget } from '@utils/types';
@ -34,79 +35,35 @@ export function GlobalHashtagWidget({ params }: { params: Widget }) {
{ refetchOnWindowFocus: false }
);
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: data ? data.length : 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={event.id} event={event} />
</div>
);
return <Repost key={event.id} event={event} />;
case 1063:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@ -114,44 +71,37 @@ export function GlobalHashtagWidget({ params }: { params: Widget }) {
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title={params.title + ' in 24 hours ago'} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm font-medium text-white">
There have been no new posts with this hashtag in the last 24 hours.
) : data.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex flex-col items-center gap-4">
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center">
<h3 className="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
<VList className="scrollbar-hide h-full">
{data.map((item) => renderItem(item))}
<div className="h-16" />
</VList>
)}
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -1,3 +1,4 @@
export * from './wrapper';
export * from './local/feeds';
export * from './local/network';
export * from './local/user';
@ -13,3 +14,4 @@ export * from './nostrBand/trendingAccounts';
export * from './tmp/feeds';
export * from './tmp/hashtag';
export * from './other/learnNostr';
export * from './eventLoader';

View File

@ -1,13 +1,14 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo } from 'react';
import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { FileNote, NoteSkeleton, NoteWrapper } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { DBEvent, Widget } from '@utils/types';
@ -26,105 +27,77 @@ export function LocalArticlesWidget({ params }: { params: Widget }) {
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(dbEvent: DBEvent) => {
const event: NDKEvent = JSON.parse(dbEvent.event as string);
return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
},
[data]
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="bbg-white/10 rounded-xl px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-white">
There have been no new posts.
) : dbEvents.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex flex-col items-center gap-4">
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center">
<h3 className="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
<VList className="scrollbar-hide h-full">
{dbEvents.map((item) => renderItem(item))}
<div className="flex items-center justify-center px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
</>
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
</>
)}
</button>
) : null}
</div>
</div>
<div className="h-16" />
</VList>
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
</div>
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo } from 'react';
import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
@ -16,6 +16,7 @@ import {
} from '@shared/notes';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { DBEvent, Widget } from '@utils/types';
@ -35,80 +36,42 @@ export function LocalFeedsWidget({ params }: { params: Widget }) {
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const dbEvent: DBEvent = dbEvents[index];
if (!dbEvent) return;
(dbEvent: DBEvent) => {
const event: NDKEvent = JSON.parse(dbEvent.event as string);
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
<NoteWrapper
key={dbEvent.id + dbEvent.root_id + dbEvent.reply_id}
event={event}
root={dbEvent.root_id}
reply={dbEvent.reply_id}
>
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={dbEvent.id} event={event} />
</div>
);
return <Repost key={dbEvent.id} event={event} />;
case 1063:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@ -116,78 +79,62 @@ export function LocalFeedsWidget({ params }: { params: Widget }) {
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="bbg-white/10 rounded-xl px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-white">
There have been no new posts.
) : dbEvents.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex flex-col items-center gap-4">
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center">
<h3 className="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
<VList className="scrollbar-hide h-full">
{dbEvents.map((item) => renderItem(item))}
<div className="flex items-center justify-center px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
</>
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
</>
)}
</button>
) : null}
</div>
</div>
<div className="h-16" />
</VList>
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
</div>
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -1,13 +1,14 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo } from 'react';
import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { FileNote, NoteSkeleton, NoteWrapper } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { DBEvent, Widget } from '@utils/types';
@ -26,105 +27,77 @@ export function LocalFilesWidget({ params }: { params: Widget }) {
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(dbEvent: DBEvent) => {
const event: NDKEvent = JSON.parse(dbEvent.event as string);
return (
<div key={event.id} data-index={index} ref={virtualizer.measureElement}>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
},
[data]
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
<div className="px-3 py-1.5">
<div className="bbg-white/10 rounded-xl px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-white">
There have been no new posts.
) : dbEvents.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex flex-col items-center gap-4">
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center">
<h3 className="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
<VList className="scrollbar-hide h-full">
{dbEvents.map((item) => renderItem(item))}
<div className="flex items-center justify-center px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
</>
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
</>
)}
</button>
) : null}
</div>
</div>
<div className="h-16" />
</VList>
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
</div>
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo } from 'react';
import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
@ -16,6 +16,7 @@ import {
} from '@shared/notes';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { DBEvent, Widget } from '@utils/types';
@ -34,80 +35,42 @@ export function LocalFollowsWidget({ params }: { params: Widget }) {
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const dbEvent: DBEvent = dbEvents[index];
if (!dbEvent) return;
(dbEvent: DBEvent) => {
const event: NDKEvent = JSON.parse(dbEvent.event as string);
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
<NoteWrapper
key={dbEvent.id + dbEvent.root_id + dbEvent.reply_id}
event={event}
root={dbEvent.root_id}
reply={dbEvent.reply_id}
>
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={dbEvent.id} event={event} />
</div>
);
return <Repost key={dbEvent.id} event={event} />;
case 1063:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@ -115,9 +78,9 @@ export function LocalFollowsWidget({ params }: { params: Widget }) {
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title="Follows" />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
@ -139,60 +102,38 @@ export function LocalFollowsWidget({ params }: { params: Widget }) {
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
<VList className="scrollbar-hide h-full">
{dbEvents.map((item) => renderItem(item))}
<div className="flex items-center justify-center px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
</>
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
</>
)}
</button>
) : null}
</div>
</div>
<div className="h-16" />
</VList>
)}
{isFetchingNextPage && (
<div className="mb-20 px-3">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
) : null}
</div>
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -1,7 +1,7 @@
import { NDKEvent, NDKFilter, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { VList } from 'virtua';
import { useStorage } from '@libs/storage/provider';
@ -16,6 +16,9 @@ import {
} from '@shared/notes';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
import { EventLoader, WidgetWrapper } from '@shared/widgets';
import { useStronghold } from '@stores/stronghold';
import { useNostr } from '@utils/hooks/useNostr';
import { toRawEvent } from '@utils/rawEvent';
@ -33,84 +36,47 @@ export function LocalNetworkWidget() {
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
const isFetched = useStronghold((state) => state.isFetched);
const dbEvents = useMemo(
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const dbEvent: DBEvent = dbEvents[index];
if (!dbEvent) return;
(dbEvent: DBEvent) => {
const event: NDKEvent = JSON.parse(dbEvent.event as string);
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
<NoteWrapper
key={dbEvent.id + dbEvent.root_id + dbEvent.reply_id}
event={event}
root={dbEvent.root_id}
reply={dbEvent.reply_id}
>
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={dbEvent.id} event={event} />
</div>
);
return <Repost key={dbEvent.id} event={event} />;
case 1063:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={dbEvent.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@ -120,28 +86,24 @@ export function LocalNetworkWidget() {
// subscribe for new event
// sub will be managed by lru-cache
useEffect(() => {
if (db.account && db.account.network) {
if (db.account && db.account.network && dbEvents.length > 0) {
const filter: NDKFilter = {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: db.account.network,
since: db.account.last_login_at ?? Math.floor(Date.now() / 1000),
since: Math.floor(Date.now() / 1000),
};
sub(
filter,
async (event) => {
const rawEvent = toRawEvent(event);
await db.createEvent(rawEvent);
},
false // don't close sub on eose
);
sub(filter, async (event) => {
const rawEvent = toRawEvent(event);
await db.createEvent(rawEvent);
});
}
}, []);
}, [data]);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<TitleBar title="👋 Network" />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<WidgetWrapper>
<TitleBar title="Network" />
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
@ -149,74 +111,41 @@ export function LocalNetworkWidget() {
</div>
</div>
) : dbEvents.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex flex-col items-center gap-4">
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center">
<h3 className="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
<EventLoader firstTime={true} />
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
<VList className="scrollbar-hide h-full">
{!isFetched ? <EventLoader firstTime={false} /> : null}
{dbEvents.map((item) => renderItem(item))}
<div className="flex items-center justify-center px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Load more</span>
</>
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5 text-white" />
<span>Nothing more to load</span>
</>
)}
</button>
) : null}
</div>
</div>
<div className="h-16" />
</VList>
)}
{isFetchingNextPage && (
<div className="mb-20 px-3">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
) : null}
</div>
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -16,6 +16,7 @@ import { RepliesList } from '@shared/notes/replies/list';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
import { User } from '@shared/user';
import { WidgetWrapper } from '@shared/widgets';
import { useEvent } from '@utils/hooks/useEvent';
import { Widget } from '@utils/types';
@ -41,9 +42,9 @@ export function LocalThreadWidget({ params }: { params: Widget }) {
);
return (
<div className="scrollbar-hide relative shrink-0 grow-0 basis-[400px] overflow-y-auto bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div className="h-full">
<div className="scrollbar-hide h-full overflow-y-auto">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
@ -69,6 +70,6 @@ export function LocalThreadWidget({ params }: { params: Widget }) {
<RepliesList id={params.content} />
</div>
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -1,7 +1,7 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { VList } from 'virtua';
import { useNDK } from '@libs/ndk/provider';
@ -16,6 +16,7 @@ import {
} from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { UserProfile } from '@shared/userProfile';
import { WidgetWrapper } from '@shared/widgets';
import { nHoursAgo } from '@utils/date';
import { Widget } from '@utils/types';
@ -40,79 +41,35 @@ export function LocalUserWidget({ params }: { params: Widget }) {
}
);
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: data ? data.length : 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const event: NDKEvent = data[index];
if (!event) return;
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<TextNote />
</NoteWrapper>
);
case NDKKind.Repost:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={event.id} event={event} />
</div>
);
return <Repost key={event.id} event={event} />;
case 1063:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<FileNote />
</NoteWrapper>
);
case NDKKind.Article:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<ArticleNote />
</NoteWrapper>
);
default:
return (
<div
key={event.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
<NoteWrapper key={event.id} event={event}>
<UnknownNote />
</NoteWrapper>
);
}
},
@ -120,9 +77,9 @@ export function LocalUserWidget({ params }: { params: Widget }) {
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title={params.title} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="px-3 pt-1.5">
<UserProfile pubkey={params.content} />
</div>
@ -135,7 +92,7 @@ export function LocalUserWidget({ params }: { params: Widget }) {
<NoteSkeleton />
</div>
</div>
) : items.length === 0 ? (
) : data.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-6 backdrop-blur-xl">
<div className="flex flex-col items-center gap-4">
@ -146,26 +103,14 @@ export function LocalUserWidget({ params }: { params: Widget }) {
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
<VList className="scrollbar-hide h-full">
{data.map((item) => renderItem(item))}
<div className="h-16" />
</VList>
)}
</div>
</div>
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { NostrBandUserProfile, type Profile } from '@shared/widgets/nostrBandUserProfile';
import { Widget } from '@utils/types';
@ -31,7 +32,7 @@ export function TrendingAccountsWidget({ params }: { params: Widget }) {
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title="Trending Accounts" />
<div className="scrollbar-hide h-full max-w-full overflow-y-auto pb-20">
{status === 'loading' ? (
@ -56,6 +57,6 @@ export function TrendingAccountsWidget({ params }: { params: Widget }) {
</div>
)}
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { NoteSkeleton, NoteWrapper, TextNote } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { Widget } from '@utils/types';
@ -31,7 +32,7 @@ export function TrendingNotesWidget({ params }: { params: Widget }) {
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title="Trending Notes" />
<div className="scrollbar-hide h-full max-w-full overflow-y-auto pb-20">
{status === 'loading' ? (
@ -58,6 +59,6 @@ export function TrendingNotesWidget({ params }: { params: Widget }) {
</div>
)}
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -2,6 +2,7 @@ import { useNavigate } from 'react-router-dom';
import { ArrowRightIcon } from '@shared/icons';
import { TitleBar } from '@shared/titleBar';
import { WidgetWrapper } from '@shared/widgets';
import { useResources } from '@stores/resources';
@ -21,7 +22,7 @@ export function LearnNostrWidget({ params }: { params: Widget }) {
};
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<WidgetWrapper>
<TitleBar id={params.id} title="The Joy of Nostr" />
<div className="scrollbar-hide h-full overflow-y-auto px-3 pb-20">
{resources.map((resource, index) => (
@ -58,6 +59,6 @@ export function LearnNostrWidget({ params }: { params: Widget }) {
</div>
))}
</div>
</div>
</WidgetWrapper>
);
}

View File

@ -0,0 +1,32 @@
import { Resizable } from 're-resizable';
import { ReactNode, useState } from 'react';
import { twMerge } from 'tailwind-merge';
export function WidgetWrapper({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
const [width, setWidth] = useState(420);
return (
<Resizable
size={{ width: width, height: '100vh' }}
onResizeStart={(e) => e.preventDefault()}
onResizeStop={(_e, _direction, _ref, d) => {
setWidth((prevWidth) => prevWidth + d.width);
}}
minWidth={420}
minHeight={'100vh'}
className={twMerge(
'relative shrink-0 grow-0 bg-white/10 backdrop-blur-xl',
className
)}
enable={{ right: true }}
>
{children}
</Resizable>
);
}

22
src/stores/browse.ts Normal file
View File

@ -0,0 +1,22 @@
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
interface BrowseState {
data: Array<{ title: string; data: string[] }>;
setData: ({ title, data }: { title: string; data: string[] }) => void;
}
export const useBrowse = create<BrowseState>()(
persist(
(set) => ({
data: [],
setData: (data) => {
set((state) => ({ data: [...state.data, data] }));
},
}),
{
name: 'browseUsers',
storage: createJSONStorage(() => localStorage),
}
)
);

View File

@ -14,7 +14,7 @@ export const useSidebar = create<SidebarState>()(
persist(
(set) => ({
feeds: true,
chats: true,
chats: false,
integrations: true,
toggleFeeds: () => set((state) => ({ feeds: !state.feeds })),
toggleChats: () => set((state) => ({ chats: !state.chats })),

View File

@ -4,9 +4,11 @@ import { createJSONStorage, persist } from 'zustand/middleware';
interface StrongholdState {
privkey: null | string;
walletConnectURL: null | string;
isFetched: null | boolean;
setPrivkey: (privkey: string) => void;
setWalletConnectURL: (uri: string) => void;
clearPrivkey: () => void;
setIsFetched: () => void;
}
export const useStronghold = create<StrongholdState>()(
@ -14,6 +16,7 @@ export const useStronghold = create<StrongholdState>()(
(set) => ({
privkey: null,
walletConnectURL: null,
isFetched: false,
setPrivkey: (privkey: string) => {
set({ privkey: privkey });
},
@ -23,6 +26,9 @@ export const useStronghold = create<StrongholdState>()(
clearPrivkey: () => {
set({ privkey: null });
},
setIsFetched: () => {
set({ isFetched: true });
},
}),
{
name: 'stronghold',

View File

@ -10,6 +10,7 @@ interface WidgetState {
fetchWidgets: (db: LumeStorage) => void;
setWidget: (db: LumeStorage, { kind, title, content }: Widget) => void;
removeWidget: (db: LumeStorage, id: string) => void;
reorderWidget: (id: string, position: number) => void;
}
export const WidgetKinds = {
@ -141,6 +142,18 @@ export const useWidgets = create<WidgetState>()(
await db.removeWidget(id);
set((state) => ({ widgets: state.widgets.filter((widget) => widget.id !== id) }));
},
reorderWidget: (id: string, position: number) =>
set((state) => {
const widgets = [...state.widgets];
const widget = widgets.find((widget) => widget.id === id);
if (!widget) return { widgets };
const idx = widgets.indexOf(widget);
widgets.splice(idx, 1);
widgets.splice(position, 0, widget);
return { widgets };
}),
}),
{
name: 'widgets',

View File

@ -1,28 +0,0 @@
import Database from 'tauri-plugin-sql-api';
import { Account } from '@utils/types';
async function connect(): Promise<Database> {
let db: null | Database = null;
if (db) {
return db;
}
try {
db = await Database.load('sqlite:lume.db');
} catch (e) {
throw new Error('Failed to connect to database, error: ', e);
}
return db;
}
export async function checkActiveAccount() {
const db = await connect();
const result: Array<Account> = await db.select(
'SELECT * FROM accounts WHERE is_active = 1;'
);
return result.length > 0;
}

View File

@ -6,11 +6,10 @@ import {
NDKSubscription,
NDKUser,
} from '@nostr-dev-kit/ndk';
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
import { message, open } from '@tauri-apps/api/dialog';
import { Body, fetch } from '@tauri-apps/api/http';
import { LRUCache } from 'lru-cache';
import { NostrFetcher } from 'nostr-fetch';
import { NostrEventExt } from 'nostr-fetch';
import { nip19 } from 'nostr-tools';
import { useMemo } from 'react';
@ -21,11 +20,12 @@ import { useStronghold } from '@stores/stronghold';
import { createBlobFromFile } from '@utils/createBlobFromFile';
import { nHoursAgo } from '@utils/date';
import { getMultipleRandom } from '@utils/transform';
import { NDKEventWithReplies, NostrBuildResponse } from '@utils/types';
export function useNostr() {
const { ndk, relayUrls } = useNDK();
const { db } = useStorage();
const { ndk, relayUrls, fetcher } = useNDK();
const privkey = useStronghold((state) => state.privkey);
const subManager = useMemo(
@ -37,15 +37,24 @@ export function useNostr() {
[]
);
const sub = async (filter: NDKFilter, callback: (event: NDKEvent) => void) => {
const sub = async (
filter: NDKFilter,
callback: (event: NDKEvent) => void,
groupable?: boolean
) => {
if (!ndk) throw new Error('NDK instance not found');
const subEvent = ndk.subscribe(filter, { closeOnEose: false });
subManager.set(JSON.stringify(filter), subEvent);
const subEvent = ndk.subscribe(filter, {
closeOnEose: false,
groupable: groupable ?? true,
});
subEvent.addListener('event', (event: NDKEvent) => {
callback(event);
});
subManager.set(JSON.stringify(filter), subEvent);
console.log('current active sub: ', subManager.size);
};
const fetchUserData = async (preFollows?: string[]) => {
@ -135,45 +144,8 @@ export function useNostr() {
publish({ content: '', kind: NDKKind.Contacts, tags: tags });
};
const prefetchEvents = async () => {
try {
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
const dbEventsEmpty = await db.isEventsEmpty();
let since: number;
if (dbEventsEmpty || db.account.last_login_at === 0) {
since = db.account.network.length > 400 ? nHoursAgo(12) : nHoursAgo(24);
} else {
since = db.account.last_login_at;
}
console.log("prefetching events with user's network: ", db.account.network.length);
console.log('prefetching events since: ', since);
const events = (await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
authors: db.account.network,
},
{ since: since }
)) as unknown as NDKEvent[];
// save all events to database
const promises = await Promise.all(
events.map(async (event) => await db.createEvent(event))
);
if (promises) return { status: 'ok', message: 'prefetch completed' };
} catch (e) {
console.error('prefetch events failed, error: ', e);
return { status: 'failed', message: e };
}
};
const fetchActivities = async () => {
try {
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
const events = await fetcher.fetchAllEvents(
relayUrls,
{
@ -197,7 +169,6 @@ export function useNostr() {
};
const fetchNIP04Chats = async () => {
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
const events = await fetcher.fetchAllEvents(
relayUrls,
{
@ -215,17 +186,19 @@ export function useNostr() {
};
const fetchNIP04Messages = async (sender: string) => {
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
let senderMessages: NostrEventExt<false>[] = [];
const senderMessages = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.EncryptedDirectMessage],
authors: [sender],
'#p': [db.account.pubkey],
},
{ since: 0 }
);
if (sender !== db.account.pubkey) {
senderMessages = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.EncryptedDirectMessage],
authors: [sender],
'#p': [db.account.pubkey],
},
{ since: 0 }
);
}
const userMessages = await fetcher.fetchAllEvents(
relayUrls,
@ -246,7 +219,6 @@ export function useNostr() {
const fetchAllReplies = async (id: string, data?: NDKEventWithReplies[]) => {
let events = data || null;
const fetcher = NostrFetcher.withCustomPool(ndkAdapter(ndk));
if (!data) {
events = (await fetcher.fetchAllEvents(
@ -286,6 +258,53 @@ export function useNostr() {
return events;
};
const getAllEventsSinceLastLogin = async (customSince?: number) => {
try {
let since: number;
const dbEventsEmpty = await db.isEventsEmpty();
if (!customSince) {
if (dbEventsEmpty || db.account.last_login_at === 0) {
since = db.account.network.length > 400 ? nHoursAgo(12) : nHoursAgo(24);
} else {
since = db.account.last_login_at;
}
} else {
since = customSince;
}
const events = (await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [NDKKind.Text, NDKKind.Repost, 1063, NDKKind.Article],
authors: db.account.network,
},
{ since: since }
)) as unknown as NDKEvent[];
return { status: 'ok', message: 'fetch completed', data: events };
} catch (e) {
console.error('prefetch events failed, error: ', e);
return { status: 'failed', message: e };
}
};
const getContactsByPubkey = async (pubkey: string) => {
const user = ndk.getUser({ hexpubkey: pubkey });
const follows = [...(await user.follows())].map((user) => user.hexpubkey);
return getMultipleRandom([...follows], 10);
};
const getEventsByPubkey = async (pubkey: string) => {
const events = await fetcher.fetchAllEvents(
relayUrls,
{ authors: [pubkey], kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Article] },
{ since: nHoursAgo(24) },
{ sort: true }
);
return events as unknown as NDKEvent[];
};
const publish = async ({
content,
kind,
@ -421,7 +440,7 @@ export function useNostr() {
fetchUserData,
addContact,
removeContact,
prefetchEvents,
getAllEventsSinceLastLogin,
fetchActivities,
fetchNIP04Chats,
fetchNIP04Messages,
@ -429,5 +448,7 @@ export function useNostr() {
publish,
createZap,
upload,
getContactsByPubkey,
getEventsByPubkey,
};
}

View File

@ -2,8 +2,10 @@ import { NDKUserProfile } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
export function useProfile(pubkey: string, embed?: string) {
const { db } = useStorage();
const { ndk } = useNDK();
const {
status,
@ -12,20 +14,20 @@ export function useProfile(pubkey: string, embed?: string) {
} = useQuery(
['user', pubkey],
async () => {
if (!embed) {
const cleanPubkey = pubkey.replace('-', '');
const user = ndk.getUser({ hexpubkey: cleanPubkey });
await user.fetchProfile();
if (user.profile) {
user.profile.display_name = user.profile.displayName;
return user.profile;
} else {
throw new Error(`User not found: ${pubkey}`);
}
} else {
if (embed) {
const profile: NDKUserProfile = JSON.parse(embed);
return profile;
}
const cleanPubkey = pubkey.replace('-', '');
const user = ndk.getUser({ hexpubkey: cleanPubkey });
const profile = await user.fetchProfile({ closeOnEose: true });
if (!user.profile) return Promise.reject(new Error('profile not found'));
await db.createProfile(cleanPubkey, profile);
return user.profile;
},
{
enabled: !!ndk,

View File

@ -57,6 +57,11 @@ export function parser(eventContent: string) {
return word.replace(word, `~tag-${word}~`);
}
// boost
if (word.startsWith('$') && word.length > 1) {
return word.replace(word, `~boost-${word}~`);
}
// nostr account references
if (word.startsWith('nostr:npub1') || word.startsWith('npub1')) {
const npub = word.replace('nostr:', '').replace(/[^a-zA-Z0-9 ]/g, '');

View File

@ -24,3 +24,9 @@ export function getRepostID(tags: NDKTag[]) {
return quoteID;
}
// get random n elements from array
export function getMultipleRandom(arr: string[], num: number) {
const shuffled = [...arr].sort(() => 0.5 - Math.random());
return shuffled.slice(0, num);
}