Merge pull request #14 from reyamir/feat/v0.2.0
Release first public version - v0.2.0
This commit is contained in:
@@ -12,7 +12,9 @@
|
|||||||
"^@layouts/(.*)$",
|
"^@layouts/(.*)$",
|
||||||
"^@pages/(.*)$",
|
"^@pages/(.*)$",
|
||||||
"^@components/(.*)$",
|
"^@components/(.*)$",
|
||||||
|
"^@stores/(.*)$",
|
||||||
"^@utils/(.*)$",
|
"^@utils/(.*)$",
|
||||||
|
"^@assets/(.*)$",
|
||||||
"<THIRD_PARTY_MODULES>",
|
"<THIRD_PARTY_MODULES>",
|
||||||
"^[./]"
|
"^[./]"
|
||||||
],
|
],
|
||||||
|
|||||||
54
package.json
54
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "lume",
|
"name": "lume",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 1420",
|
"dev": "next dev -p 1420",
|
||||||
"build": "next build && next export -o dist",
|
"build": "next build && next export -o dist",
|
||||||
@@ -12,53 +12,61 @@
|
|||||||
"**/*": "prettier --write --ignore-unknown"
|
"**/*": "prettier --write --ignore-unknown"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emoji-mart/data": "^1.1.2",
|
||||||
|
"@emoji-mart/react": "^1.1.1",
|
||||||
|
"@radix-ui/react-collapsible": "^1.0.2",
|
||||||
"@radix-ui/react-dialog": "^1.0.3",
|
"@radix-ui/react-dialog": "^1.0.3",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||||
"@radix-ui/react-icons": "^1.2.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@rehooks/local-storage": "^2.4.4",
|
"@radix-ui/react-popover": "^1.0.5",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.3",
|
||||||
|
"@supabase/supabase-js": "^2.12.1",
|
||||||
|
"@tanstack/query-core": "^4.27.0",
|
||||||
|
"@tanstack/react-query": "^4.28.0",
|
||||||
|
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||||
"@tauri-apps/api": "^1.2.0",
|
"@tauri-apps/api": "^1.2.0",
|
||||||
"@uiw/react-markdown-preview": "^4.1.10",
|
|
||||||
"@uiw/react-md-editor": "^3.20.5",
|
|
||||||
"bitcoin-address-validation": "^2.2.1",
|
|
||||||
"boring-avatars": "^1.7.0",
|
"boring-avatars": "^1.7.0",
|
||||||
|
"dayjs": "^1.11.7",
|
||||||
|
"destr": "^1.2.2",
|
||||||
|
"emoji-mart": "^5.5.2",
|
||||||
"framer-motion": "^9.1.7",
|
"framer-motion": "^9.1.7",
|
||||||
"moment": "^2.29.4",
|
"jotai": "^2.0.3",
|
||||||
|
"jotai-cache": "^0.3.0",
|
||||||
|
"jotai-tanstack-query": "^0.6.0",
|
||||||
"next": "^13.2.4",
|
"next": "^13.2.4",
|
||||||
"next-remove-imports": "^1.0.10",
|
"next-remove-imports": "^1.0.10",
|
||||||
"nostr-relaypool": "^0.5.12",
|
"nostr-relaypool": "^0.5.18",
|
||||||
"nostr-tools": "^1.7.4",
|
"nostr-tools": "^1.8.0",
|
||||||
"qrcode.react": "^3.1.0",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.43.5",
|
"react-hook-form": "^7.43.8",
|
||||||
"react-moment": "^1.1.3",
|
|
||||||
"react-player": "^2.12.0",
|
"react-player": "^2.12.0",
|
||||||
"react-virtuoso": "^4.1.0",
|
"react-string-replace": "^1.1.0",
|
||||||
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql",
|
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"ws": "^8.12.1"
|
"ws": "^8.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@tauri-apps/cli": "^1.2.3",
|
"@tauri-apps/cli": "^1.2.3",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||||
"@types/node": "^18.15.0",
|
"@types/node": "^18.15.10",
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.29",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.54.1",
|
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||||
"@typescript-eslint/parser": "^5.54.1",
|
"@typescript-eslint/parser": "^5.56.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"csstype": "^3.1.1",
|
"csstype": "^3.1.1",
|
||||||
"eslint": "^8.35.0",
|
"eslint": "^8.36.0",
|
||||||
"eslint-config-next": "^13.2.4",
|
"eslint-config-next": "^13.2.4",
|
||||||
"eslint-config-prettier": "^8.7.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.1.2",
|
"lint-staged": "^13.2.0",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.7",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.4",
|
"prettier-plugin-tailwindcss": "^0.2.5",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5"
|
||||||
|
|||||||
2330
pnpm-lock.yaml
generated
2330
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
335
src-tauri/Cargo.lock
generated
335
src-tauri/Cargo.lock
generated
@@ -82,6 +82,22 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attohttpc"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7"
|
||||||
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
|
"http",
|
||||||
|
"log",
|
||||||
|
"native-tls",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -500,17 +516,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dbus"
|
|
||||||
version = "0.9.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"libdbus-sys",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "0.99.17"
|
version = "0.99.17"
|
||||||
@@ -899,7 +904,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"windows 0.39.0",
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1393,15 +1398,6 @@ version = "0.2.139"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libdbus-sys"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9f8d7ae751e1cb825c840ae5e682f59b098cdfd213c350ac268b61449a5f58a0"
|
|
||||||
dependencies = [
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
@@ -1468,7 +1464,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lume"
|
name = "lume"
|
||||||
version = "0.1.1"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"objc",
|
"objc",
|
||||||
@@ -1485,19 +1481,6 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mac-notification-sys"
|
|
||||||
version = "0.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3e72d50edb17756489e79d52eb146927bec8eba9dd48faadf9ef08bca3791ad5"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"dirs-next",
|
|
||||||
"objc-foundation",
|
|
||||||
"objc_id",
|
|
||||||
"time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "malloc_buf"
|
name = "malloc_buf"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
@@ -1578,6 +1561,24 @@ dependencies = [
|
|||||||
"windows-sys 0.42.0",
|
"windows-sys 0.42.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native-tls"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"openssl",
|
||||||
|
"openssl-probe",
|
||||||
|
"openssl-sys",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
"security-framework-sys",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -1637,17 +1638,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "notify-rust"
|
|
||||||
version = "4.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3ce656bb6d22a93ae276a23de52d1aec5ba4db3ece3c0eb79dfd5add7384db6a"
|
|
||||||
dependencies = [
|
|
||||||
"dbus",
|
|
||||||
"mac-notification-sys",
|
|
||||||
"tauri-winrt-notification",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
@@ -1729,17 +1719,6 @@ dependencies = [
|
|||||||
"objc_exception",
|
"objc_exception",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc-foundation"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
|
||||||
dependencies = [
|
|
||||||
"block",
|
|
||||||
"objc",
|
|
||||||
"objc_id",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc_exception"
|
name = "objc_exception"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -1774,6 +1753,62 @@ dependencies = [
|
|||||||
"windows-sys 0.42.0",
|
"windows-sys 0.42.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl"
|
||||||
|
version = "0.10.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"foreign-types",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"openssl-macros",
|
||||||
|
"openssl-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-probe"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.80"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os_info"
|
||||||
|
version = "3.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c424bc68d15e0778838ac013b5b3449544d8133633d8016319e7e05a820b8c0"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2026,7 +2061,7 @@ dependencies = [
|
|||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"line-wrap",
|
"line-wrap",
|
||||||
"quick-xml 0.26.0",
|
"quick-xml",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
@@ -2104,15 +2139,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quick-xml"
|
|
||||||
version = "0.23.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.26.0"
|
version = "0.26.0"
|
||||||
@@ -2276,30 +2302,6 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rfd"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea"
|
|
||||||
dependencies = [
|
|
||||||
"block",
|
|
||||||
"dispatch",
|
|
||||||
"glib-sys",
|
|
||||||
"gobject-sys",
|
|
||||||
"gtk-sys",
|
|
||||||
"js-sys",
|
|
||||||
"lazy_static",
|
|
||||||
"log",
|
|
||||||
"objc",
|
|
||||||
"objc-foundation",
|
|
||||||
"objc_id",
|
|
||||||
"raw-window-handle",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
"windows 0.37.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.16.20"
|
version = "0.16.20"
|
||||||
@@ -2381,6 +2383,15 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schannel"
|
||||||
|
version = "0.1.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.42.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scoped-tls"
|
name = "scoped-tls"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -2403,6 +2414,29 @@ dependencies = [
|
|||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework"
|
||||||
|
version = "2.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"core-foundation",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
"security-framework-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework-sys"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "selectors"
|
name = "selectors"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@@ -2492,6 +2526,18 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa 1.0.5",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "1.14.0"
|
version = "1.14.0"
|
||||||
@@ -2797,27 +2843,6 @@ version = "0.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strum"
|
|
||||||
version = "0.22.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e"
|
|
||||||
dependencies = [
|
|
||||||
"strum_macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strum_macros"
|
|
||||||
version = "0.22.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb"
|
|
||||||
dependencies = [
|
|
||||||
"heck 0.3.3",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
@@ -2898,7 +2923,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"uuid 1.3.0",
|
"uuid 1.3.0",
|
||||||
"windows 0.39.0",
|
"windows",
|
||||||
"windows-implement",
|
"windows-implement",
|
||||||
"x11-dl",
|
"x11-dl",
|
||||||
]
|
]
|
||||||
@@ -2921,6 +2946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fe7e0f1d535e7cbbbab43c82be4fc992b84f9156c16c160955617e0260ebc449"
|
checksum = "fe7e0f1d535e7cbbbab43c82be4fc992b84f9156c16c160955617e0260ebc449"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"attohttpc",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
"embed_plist",
|
"embed_plist",
|
||||||
@@ -2933,15 +2959,14 @@ dependencies = [
|
|||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"http",
|
"http",
|
||||||
"ignore",
|
"ignore",
|
||||||
"notify-rust",
|
|
||||||
"objc",
|
"objc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"open",
|
"open",
|
||||||
|
"os_info",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"regex",
|
"regex",
|
||||||
"rfd",
|
|
||||||
"semver 1.0.16",
|
"semver 1.0.16",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -2960,7 +2985,7 @@ dependencies = [
|
|||||||
"uuid 1.3.0",
|
"uuid 1.3.0",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows 0.39.0",
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3051,7 +3076,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid 1.3.0",
|
"uuid 1.3.0",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows 0.39.0",
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3070,7 +3095,7 @@ dependencies = [
|
|||||||
"uuid 1.3.0",
|
"uuid 1.3.0",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows 0.39.0",
|
"windows",
|
||||||
"wry",
|
"wry",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3099,18 +3124,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"url",
|
"url",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"windows 0.39.0",
|
"windows",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tauri-winrt-notification"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c58de036c4d2e20717024de2a3c4bf56c301f07b21bc8ef9b57189fce06f1f3b"
|
|
||||||
dependencies = [
|
|
||||||
"quick-xml 0.23.1",
|
|
||||||
"strum",
|
|
||||||
"windows 0.39.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3513,18 +3527,6 @@ dependencies = [
|
|||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-futures"
|
|
||||||
version = "0.4.34"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.84"
|
version = "0.2.84"
|
||||||
@@ -3638,7 +3640,7 @@ checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"webview2-com-macros",
|
"webview2-com-macros",
|
||||||
"webview2-com-sys",
|
"webview2-com-sys",
|
||||||
"windows 0.39.0",
|
"windows",
|
||||||
"windows-implement",
|
"windows-implement",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3663,7 +3665,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"windows 0.39.0",
|
"windows",
|
||||||
"windows-bindgen",
|
"windows-bindgen",
|
||||||
"windows-metadata",
|
"windows-metadata",
|
||||||
]
|
]
|
||||||
@@ -3699,19 +3701,6 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows"
|
|
||||||
version = "0.37.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_msvc 0.37.0",
|
|
||||||
"windows_i686_gnu 0.37.0",
|
|
||||||
"windows_i686_msvc 0.37.0",
|
|
||||||
"windows_x86_64_gnu 0.37.0",
|
|
||||||
"windows_x86_64_msvc 0.37.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
@@ -3803,12 +3792,6 @@ version = "0.42.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
|
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.37.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
@@ -3821,12 +3804,6 @@ version = "0.42.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
|
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.37.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
@@ -3839,12 +3816,6 @@ version = "0.42.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
|
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.37.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
@@ -3857,12 +3828,6 @@ version = "0.42.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
|
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.37.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
@@ -3881,12 +3846,6 @@ version = "0.42.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
|
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.37.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
@@ -3942,7 +3901,7 @@ dependencies = [
|
|||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webkit2gtk-sys",
|
"webkit2gtk-sys",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows 0.39.0",
|
"windows",
|
||||||
"windows-implement",
|
"windows-implement",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lume"
|
name = "lume"
|
||||||
version = "0.1.1"
|
version = "0.2.0"
|
||||||
description = "nostr client"
|
description = "nostr client"
|
||||||
authors = ["Ren Amamiya"]
|
authors = ["Ren Amamiya"]
|
||||||
license = ""
|
license = ""
|
||||||
@@ -16,7 +16,7 @@ tauri-build = { version = "1.2", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "1.2", features = ["clipboard-all", "notification-all", "shell-open", "system-tray", "window-start-dragging"] }
|
tauri = { version = "1.2", features = ["clipboard-read-text", "clipboard-write-text", "http-request", "os-all", "shell-open", "system-tray", "window-close", "window-start-dragging"] }
|
||||||
|
|
||||||
[dependencies.tauri-plugin-sql]
|
[dependencies.tauri-plugin-sql]
|
||||||
git = "https://github.com/tauri-apps/plugins-workspace"
|
git = "https://github.com/tauri-apps/plugins-workspace"
|
||||||
|
|||||||
@@ -9,21 +9,38 @@ CREATE TABLE
|
|||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- add default relays
|
||||||
|
-- relay status:
|
||||||
|
-- 0: off
|
||||||
|
-- 1: on
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
relays (relay_url, relay_status)
|
relays (relay_url, relay_status)
|
||||||
VALUES
|
VALUES
|
||||||
("wss://relay.damus.io", "1"),
|
("wss://relay.damus.io", "1"),
|
||||||
("wss://relay.uselume.xyz", "0"),
|
("wss://eden.nostr.land", "0"),
|
||||||
("wss://nostr-pub.wellorder.net", "1"),
|
("wss://nostr-pub.wellorder.net", "1"),
|
||||||
("wss://nostr.bongbong.com", "1"),
|
("wss://nostr.bongbong.com", "1"),
|
||||||
("wss://nostr.zebedee.cloud", "1"),
|
("wss://nostr.zebedee.cloud", "1"),
|
||||||
("wss://nostr.fmt.wiz.biz", "1"),
|
("wss://nostr.fmt.wiz.biz", "1"),
|
||||||
("wss://nostr.walletofsatoshi.com", "1"),
|
("wss://nostr.walletofsatoshi.com", "0"),
|
||||||
("wss://relay.snort.social", "1"),
|
("wss://relay.snort.social", "1"),
|
||||||
("wss://offchain.pub", "1"),
|
("wss://offchain.pub", "1"),
|
||||||
|
("wss://brb.io", "0"),
|
||||||
|
("wss://relay.current.fyi", "1"),
|
||||||
|
("wss://nostr.relayer.se", "0"),
|
||||||
|
("wss://nostr.bitcoiner.social", "1"),
|
||||||
|
("wss://relay.nostr.info", "1"),
|
||||||
|
("wss://relay.zeh.app", "0"),
|
||||||
|
("wss://nostr-01.dorafactory.org", "1"),
|
||||||
|
("wss://nostr.zhongwen.world", "1"),
|
||||||
|
("wss://nostro.cc", "1"),
|
||||||
|
("wss://relay.nostr.net.in", "1"),
|
||||||
("wss://nos.lol", "1");
|
("wss://nos.lol", "1");
|
||||||
|
|
||||||
-- create accounts
|
-- create accounts
|
||||||
|
-- is_active (part of multi-account feature):
|
||||||
|
-- 0: false
|
||||||
|
-- 1: true
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
accounts (
|
accounts (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -31,27 +48,30 @@ CREATE TABLE
|
|||||||
npub TEXT NOT NULL,
|
npub TEXT NOT NULL,
|
||||||
nsec TEXT NOT NULL,
|
nsec TEXT NOT NULL,
|
||||||
is_active INTEGER NOT NULL DEFAULT 0,
|
is_active INTEGER NOT NULL DEFAULT 0,
|
||||||
metadata JSON
|
metadata TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
-- create follows
|
-- create follows
|
||||||
|
-- kind (part of multi-newsfeed feature):
|
||||||
|
-- 0: direct
|
||||||
|
-- 1: follow of follow
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
follows (
|
follows (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
pubkey TEXT NOT NULL,
|
pubkey TEXT NOT NULL,
|
||||||
account TEXT NOT NULL,
|
account TEXT NOT NULL,
|
||||||
kind INTEGER NOT NULL DEFAULT 0,
|
kind INTEGER NOT NULL DEFAULT 0,
|
||||||
metadata JSON
|
metadata TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
-- create index for pubkey in follows
|
-- create index for pubkey in follows
|
||||||
CREATE UNIQUE INDEX index_pubkey ON follows (pubkey);
|
CREATE UNIQUE INDEX index_pubkey_on_follows ON follows (pubkey);
|
||||||
|
|
||||||
-- create cache profiles
|
-- create cache profiles
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
cache_profiles (
|
cache_profiles (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
metadata JSON,
|
metadata TEXT,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -65,6 +85,20 @@ CREATE TABLE
|
|||||||
kind INTEGER NOT NULL DEFAULT 1,
|
kind INTEGER NOT NULL DEFAULT 1,
|
||||||
tags TEXT NOT NULL,
|
tags TEXT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
relay TEXT,
|
parent_id TEXT,
|
||||||
is_multi BOOLEAN DEFAULT 0
|
parent_comment_id TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- create settings
|
||||||
|
CREATE TABLE
|
||||||
|
settings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
setting_key TEXT NOT NULL,
|
||||||
|
setting_value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- add default setting
|
||||||
|
INSERT INTO
|
||||||
|
settings (setting_key, setting_value)
|
||||||
|
VALUES
|
||||||
|
("last_login", "0");
|
||||||
@@ -7,24 +7,21 @@
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate objc;
|
extern crate objc;
|
||||||
|
|
||||||
use tauri::{Manager, SystemTray, WindowEvent};
|
use tauri::{Manager, WindowEvent};
|
||||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||||
use window_ext::WindowExt;
|
use window_ext::WindowExt;
|
||||||
|
|
||||||
mod window_ext;
|
mod window_ext;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let tray = SystemTray::new();
|
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let main_window = app.get_window("main").unwrap();
|
let main_window = app.get_window("main").unwrap();
|
||||||
// set inset for traffic lights
|
// set inset for traffic lights
|
||||||
main_window.position_traffic_lights(8.0, 16.0);
|
main_window.position_traffic_lights(8.0, 20.0);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.system_tray(tray)
|
|
||||||
.plugin(
|
.plugin(
|
||||||
tauri_plugin_sql::Builder::default()
|
tauri_plugin_sql::Builder::default()
|
||||||
.add_migrations(
|
.add_migrations(
|
||||||
@@ -41,7 +38,7 @@ fn main() {
|
|||||||
.on_window_event(|e| {
|
.on_window_event(|e| {
|
||||||
let apply_offset = || {
|
let apply_offset = || {
|
||||||
let win = e.window();
|
let win = e.window();
|
||||||
win.position_traffic_lights(8.0, 16.0);
|
win.position_traffic_lights(8.0, 20.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
match e.event() {
|
match e.event() {
|
||||||
|
|||||||
@@ -8,25 +8,37 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Lume",
|
"productName": "Lume",
|
||||||
"version": "0.1.1"
|
"version": "0.2.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
"all": false,
|
"all": false,
|
||||||
|
"app": {
|
||||||
|
"all": false
|
||||||
|
},
|
||||||
|
"os": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"all": false,
|
||||||
|
"request": true,
|
||||||
|
"scope": ["https://rbr.bio/*", "https://metadata.uselume.xyz/*"]
|
||||||
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"all": false,
|
"all": false,
|
||||||
"open": true
|
"open": true
|
||||||
},
|
},
|
||||||
"clipboard": {
|
"clipboard": {
|
||||||
"all": true,
|
"all": false,
|
||||||
"writeText": true,
|
"writeText": true,
|
||||||
"readText": true
|
"readText": true
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"all": true
|
"all": false
|
||||||
},
|
},
|
||||||
"window": {
|
"window": {
|
||||||
"startDragging": true
|
"startDragging": true,
|
||||||
|
"close": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|||||||
16
src/App.css
16
src/App.css
@@ -4,6 +4,13 @@
|
|||||||
|
|
||||||
@import './assets/editor.css';
|
@import './assets/editor.css';
|
||||||
|
|
||||||
|
/* Fixed next/image bug, source: https://nextjs.org/docs/api-reference/next/image */
|
||||||
|
@supports (font: -apple-system-body) and (-webkit-appearance: none) {
|
||||||
|
img[loading='lazy'] {
|
||||||
|
clip-path: inset(0.6px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* For Webkit-based browsers (Chrome, Safari and Opera) */
|
/* For Webkit-based browsers (Chrome, Safari and Opera) */
|
||||||
.scrollbar-hide::-webkit-scrollbar {
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -23,3 +30,12 @@
|
|||||||
.border {
|
.border {
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes loop {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name": "jb55",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1362882895669436423/Jzsp1Ikr.jpg",
|
|
||||||
"npub": "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "jack",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1115644092329758721/AFjOr-K8.jpg",
|
|
||||||
"npub": "npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "derekmoss",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1609534946435076096/Gl1xeTPP.jpg",
|
|
||||||
"npub": "npub18ams6ewn5aj2n3wt2qawzglx9mr4nzksxhvrdc4gzrecw7n5tvjqctp424"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ODELL",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1421584695746338819/Z_7ZfAeP.jpg",
|
|
||||||
"npub": "npub1qny3tkh0acurzla8x3zy4nhrjz5zd8l9sy9jys09umwng00manysew95gx"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "yeg0rpetrov",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1593772940126035968/D_LQYRd9.jpg",
|
|
||||||
"npub": "npub1z4m7gkva6yxgvdyclc7zp0vz4ta0s2d9jh8g83w03tp5vdf3kzdsxana6p"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "PrestonPysh",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1408783276081299462/f4Ye5n7-.jpg",
|
|
||||||
"npub": "npub1s5yq6wadwrxde4lhfs56gn64hwzuhnfa6r9mj476r5s4hkunzgzqrs6q7z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "fiatjaf",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/539211568035004416/sBMjPR9q.jpeg",
|
|
||||||
"npub": "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dergigi",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1566370176446119943/UeuACt-4.jpg",
|
|
||||||
"npub": "npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "hodlonaut",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1570910274755911682/z8DJsufc.jpg",
|
|
||||||
"npub": "npub1cjw49ftnxene9wdxujz3tp7zspp0kf862cjud4nm3j2usag6eg2smwj2rh"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "DylanLeClair_",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1599858581611941922/XxvPPWAt.jpg",
|
|
||||||
"npub": "npub1pyp9fqq60689ppds9ec3vghsm7s6s4grfya0y342g2hs3a0y6t0segc0qq"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ShadowOfNakadai",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1620811984374464514/V7GJo1ak.jpg",
|
|
||||||
"npub": "npub1sqaxzwvh5fhgw9q3d7v658ucapvfeds3dcd2587fcwyesn7dnwuqt2r45v"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "jackmallers",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1599778945699909632/O0qc9ykA.jpg",
|
|
||||||
"npub": "npub1cn4t4cd78nm900qc2hhqte5aa8c9njm6qkfzw95tszufwcwtcnsq7g3vle"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "remroya",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1616979727515881478/5ABZzBYO.jpg",
|
|
||||||
"npub": "npub1csamkk8zu67zl9z4wkp90a462v53q775aqn5q6xzjdkxnkvcpd7srtz4x9"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "TakumiHisoka",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1623286991944302594/cXSJ04BF.jpg",
|
|
||||||
"npub": "npub1yc8jxnzkzm2esndrqdae6lza6qlwzxpcz9drpy699j9k7xetrpkqgvkwe9"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "EvelinSchallert",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1448008447983763457/7k07LJxQ.jpg",
|
|
||||||
"npub": "npub1l2gvp9wxajsl6wqnh6eulvz5sdk05gtajjwjn2yn45s9yvfru2kqf3r0gm"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "peer",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1623291991709700097/aBL_VpMC.jpg",
|
|
||||||
"npub": "npub18zx8lw3947pghsgzqv2t0x8pe767sscag5djgj5afr755xkqd97qt530pr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "francispouliot_",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1524789480439283719/5Q_XBKGb.jpg",
|
|
||||||
"npub": "npub1t289s8ck5qfwynf2vsq49t2kypvvkpj7rhegayrur0ag9s2sezaqgunkzs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "lanyihou",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1603653816175689729/Ctj5GXPt.jpg",
|
|
||||||
"npub": "npub18hywyhcnn5rqhlgu80yxeyf57fyhghlrc54dzaqyd9vtts949u9s24rtva"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "marttimalmi",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/1125299725828272129/n8NDo1LN.png",
|
|
||||||
"npub": "npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Snowden",
|
|
||||||
"avatar": "https://pbs.twimg.com/profile_images/648888480974508032/66_cUYfj.jpg",
|
|
||||||
"npub": "npub1sn0wdenkukak0d9dfczzeacvhkrgz92ak56egt7vdgzn8pv2wfqqhrjdv9"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
18
src/assets/icons/comment.tsx
Normal file
18
src/assets/icons/comment.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export default function CommentIcon({ className }: { className: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/assets/icons/emoji.tsx
Normal file
18
src/assets/icons/emoji.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export default function EmojiIcon({ className }: { className: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M15.182 15.182a4.5 4.5 0 01-6.364 0M21 12a9 9 0 11-18 0 9 9 0 0118 0zM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75zm-.375 0h.008v.015h-.008V9.75zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75zm-.375 0h.008v.015h-.008V9.75z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/assets/icons/image.tsx
Normal file
18
src/assets/icons/image.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export default function ImageIcon({ className }: { className: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/assets/icons/like.tsx
Normal file
18
src/assets/icons/like.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export default function LikeIcon({ className }: { className: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/assets/icons/liked.tsx
Normal file
7
src/assets/icons/liked.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function LikedIcon({ className }: { className: string }) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
|
||||||
|
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/components/appHeader/actions.tsx
Normal file
53
src/components/appHeader/actions.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { ArrowLeftIcon, ArrowRightIcon, ReloadIcon } from '@radix-ui/react-icons';
|
||||||
|
import { platform } from '@tauri-apps/api/os';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function AppActions() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [os, setOS] = useState('');
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
const goForward = () => {
|
||||||
|
window.history.forward();
|
||||||
|
};
|
||||||
|
|
||||||
|
const reload = () => {
|
||||||
|
router.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const getPlatform = async () => {
|
||||||
|
const result = await platform();
|
||||||
|
setOS(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
getPlatform().catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex h-full items-center gap-2 ${os === 'darwin' ? 'pl-[68px]' : ''}`}>
|
||||||
|
<button
|
||||||
|
onClick={() => goBack()}
|
||||||
|
className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon className="h-4 w-4 text-zinc-500 group-hover:text-zinc-300" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => goForward()}
|
||||||
|
className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
|
||||||
|
>
|
||||||
|
<ArrowRightIcon className="h-4 w-4 text-zinc-500 group-hover:text-zinc-300" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => reload()}
|
||||||
|
className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
|
||||||
|
>
|
||||||
|
<ReloadIcon className="h-[14px] w-[14px] text-zinc-500 group-hover:text-zinc-300" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/components/appHeader/index.tsx
Normal file
24
src/components/appHeader/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { PlusIcon } from '@radix-ui/react-icons';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const AppActions = dynamic(() => import('@components/appHeader/actions'), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const NoteConnector = dynamic(() => import('@components/note/connector'), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function AppHeader() {
|
||||||
|
return (
|
||||||
|
<div data-tauri-drag-region className="flex h-full w-full flex-1 items-center px-2">
|
||||||
|
<AppActions />
|
||||||
|
<div data-tauri-drag-region className="flex h-full w-full items-center justify-between">
|
||||||
|
<div className="flex h-full items-center divide-x divide-zinc-900 px-4 pt-px"></div>
|
||||||
|
<div>
|
||||||
|
<NoteConnector />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import Image from 'next/image';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
export const Account = memo(function Account({ user, current }: { user: any; current: string }) {
|
|
||||||
const userData = JSON.parse(user.metadata);
|
|
||||||
|
|
||||||
const setCurrentUser = () => {
|
|
||||||
console.log('clicked');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentUser()}
|
|
||||||
className={`relative h-11 w-11 shrink overflow-hidden rounded-full ${
|
|
||||||
current === user.pubkey ? 'ring-1 ring-fuchsia-500 ring-offset-4 ring-offset-black' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{userData?.picture !== undefined ? (
|
|
||||||
<Image src={userData.picture} alt="user's avatar" fill={true} className="rounded-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<div className="h-11 w-11 animate-pulse rounded-full bg-zinc-700" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
120
src/components/columns/account/active.tsx
Normal file
120
src/components/columns/account/active.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
import { createFollows } from '@utils/storage';
|
||||||
|
import { tagsToArray } from '@utils/transform';
|
||||||
|
|
||||||
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
|
import { AvatarIcon, ExitIcon, GearIcon } from '@radix-ui/react-icons';
|
||||||
|
import { writeText } from '@tauri-apps/api/clipboard';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
import { memo, useContext, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export const ActiveAccount = memo(function ActiveAccount({ user }: { user: any }) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const userData = destr(user.metadata);
|
||||||
|
|
||||||
|
const now = useRef(new Date());
|
||||||
|
|
||||||
|
const openProfilePage = () => {
|
||||||
|
router.push(`/users/${user.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyPublicKey = async () => {
|
||||||
|
await writeText(nip19.npubEncode(user.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = pool.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
kinds: [3],
|
||||||
|
authors: [user.id],
|
||||||
|
since: dateToUnix(now.current),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relays,
|
||||||
|
(event: any) => {
|
||||||
|
if (event.tags.length > 0) {
|
||||||
|
createFollows(tagsToArray(event.tags), user.id, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
unsubscribeOnEose: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe;
|
||||||
|
};
|
||||||
|
}, [pool, relays, user.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<button className="relative h-11 w-11 rounded-md">
|
||||||
|
<Image
|
||||||
|
src={userData.picture}
|
||||||
|
alt="user's avatar"
|
||||||
|
fill={true}
|
||||||
|
className="rounded-md object-cover"
|
||||||
|
placeholder="blur"
|
||||||
|
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkqAcAAIUAgUW0RjgAAAAASUVORK5CYII="
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
className="min-w-[220px] rounded-md bg-zinc-900/80 p-1.5 shadow-input shadow-black/50 ring-1 ring-zinc-800 backdrop-blur-xl will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade"
|
||||||
|
side="right"
|
||||||
|
sideOffset={5}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onClick={() => openProfilePage()}
|
||||||
|
className="group relative flex h-7 select-none items-center rounded-sm px-1 pl-7 text-sm leading-none text-zinc-400 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-800 data-[highlighted]:text-fuchsia-500"
|
||||||
|
>
|
||||||
|
<div className="absolute left-0 inline-flex w-6 items-center justify-center">
|
||||||
|
<AvatarIcon />
|
||||||
|
</div>
|
||||||
|
Open profile
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item className="group relative flex h-7 select-none items-center rounded px-1 pl-7 text-sm leading-none text-zinc-400 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-800 data-[highlighted]:text-fuchsia-500">
|
||||||
|
Update profile
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onClick={() => copyPublicKey()}
|
||||||
|
className="group relative flex h-7 select-none items-center rounded px-1 pl-7 text-sm leading-none text-zinc-400 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-800 data-[highlighted]:text-fuchsia-500"
|
||||||
|
>
|
||||||
|
Copy public key
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator className="m-1 h-px bg-zinc-700/50" />
|
||||||
|
<DropdownMenu.Item className="group relative flex h-7 select-none items-center rounded px-1 pl-7 text-sm leading-none text-zinc-400 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-800 data-[highlighted]:text-fuchsia-500">
|
||||||
|
<div className="absolute left-0 inline-flex w-6 items-center justify-center">
|
||||||
|
<GearIcon />
|
||||||
|
</div>
|
||||||
|
Settings
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item className="group relative flex h-7 select-none items-center rounded px-1 pl-7 text-sm leading-none text-zinc-400 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-800 data-[highlighted]:text-fuchsia-500">
|
||||||
|
<div className="absolute left-0 inline-flex w-6 items-center justify-center">
|
||||||
|
<ExitIcon />
|
||||||
|
</div>
|
||||||
|
Logout
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
);
|
||||||
|
});
|
||||||
17
src/components/columns/account/inactive.tsx
Normal file
17
src/components/columns/account/inactive.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import destr from 'destr';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const InactiveAccount = memo(function InactiveAccount({ user }: { user: any }) {
|
||||||
|
const userData = destr(user.metadata);
|
||||||
|
|
||||||
|
const setCurrentUser = () => {
|
||||||
|
console.log('clicked');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={() => setCurrentUser()} className="relative h-11 w-11 shrink rounded-md">
|
||||||
|
<Image src={userData.picture} alt="user's avatar" fill={true} className="rounded-md object-cover" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,42 +1,46 @@
|
|||||||
import { Account } from '@components/columns/account/account';
|
import AccountList from '@components/columns/account/list';
|
||||||
|
|
||||||
import LumeSymbol from '@assets/icons/Lume';
|
import LumeSymbol from '@assets/icons/Lume';
|
||||||
|
|
||||||
import { PlusIcon } from '@radix-ui/react-icons';
|
import { PlusIcon } from '@radix-ui/react-icons';
|
||||||
import { useLocalStorage } from '@rehooks/local-storage';
|
import { getVersion } from '@tauri-apps/api/app';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import Database from 'tauri-plugin-sql-api';
|
|
||||||
|
|
||||||
export default function AccountColumn() {
|
export default function AccountColumn() {
|
||||||
const [users, setUsers] = useState([]);
|
const [version, setVersion] = useState(null);
|
||||||
const [currentUser]: any = useLocalStorage('current-user');
|
|
||||||
|
|
||||||
const getAccounts = useCallback(async () => {
|
const getAppVersion = useCallback(async () => {
|
||||||
const db = await Database.load('sqlite:lume.db');
|
const appVersion = await getVersion();
|
||||||
const result: any = await db.select('SELECT * FROM accounts');
|
setVersion(appVersion);
|
||||||
|
|
||||||
setUsers(result);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAccounts().catch(console.error);
|
getAppVersion().catch(console.error);
|
||||||
}, [getAccounts]);
|
}, [getAppVersion]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-between px-2 pt-12 pb-4">
|
<div className="flex h-full flex-col items-center justify-between px-2 pt-4 pb-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{users.map((user, index) => (
|
<Link
|
||||||
<Account key={index} user={user} current={currentUser.id} />
|
href="/explore"
|
||||||
))}
|
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-md bg-zinc-900 hover:bg-zinc-800"
|
||||||
|
>
|
||||||
|
<LumeSymbol className="h-6 w-auto text-zinc-400 group-hover:text-zinc-200" />
|
||||||
|
</Link>
|
||||||
|
<AccountList />
|
||||||
<Link
|
<Link
|
||||||
href="/onboarding"
|
href="/onboarding"
|
||||||
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center overflow-hidden rounded-full border-2 border-dashed border-zinc-600 hover:border-zinc-400"
|
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-md border-2 border-dashed border-zinc-600 hover:border-zinc-400"
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4 text-zinc-400 group-hover:text-zinc-200" />
|
<PlusIcon className="h-4 w-4 text-zinc-400 group-hover:text-zinc-200" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-0.5 text-center">
|
||||||
<LumeSymbol className="h-8 w-auto text-zinc-700" />
|
<span className="animate-moveBg from-fuchsia-300 via-orange-100 to-amber-300 text-sm font-black uppercase leading-tight text-zinc-600 hover:bg-gradient-to-r hover:bg-clip-text hover:text-transparent">
|
||||||
|
Lume
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-medium text-zinc-700">v{version}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
36
src/components/columns/account/list.tsx
Normal file
36
src/components/columns/account/list.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { ActiveAccount } from '@components/columns/account/active';
|
||||||
|
import { InactiveAccount } from '@components/columns/account/inactive';
|
||||||
|
|
||||||
|
import { activeAccountAtom } from '@stores/account';
|
||||||
|
|
||||||
|
import { getAccounts } from '@utils/storage';
|
||||||
|
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function AccountList() {
|
||||||
|
const [activeAccount] = useAtom(activeAccountAtom);
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
|
||||||
|
const renderAccount = useCallback(
|
||||||
|
(user: { id: string }) => {
|
||||||
|
if (user.id === activeAccount.id) {
|
||||||
|
return <ActiveAccount key={user.id} user={user} />;
|
||||||
|
} else {
|
||||||
|
return <InactiveAccount key={user.id} user={user} />;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[activeAccount.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAccount = async () => {
|
||||||
|
const result: any = await getAccounts();
|
||||||
|
setUsers(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAccount().catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <>{users.map((user) => renderAccount(user))}</>;
|
||||||
|
}
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import { RelayContext } from '@components/contexts/relay';
|
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/getDate';
|
|
||||||
|
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
|
||||||
import { useLocalStorage } from '@rehooks/local-storage';
|
|
||||||
import * as commands from '@uiw/react-md-editor/lib/commands';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { getEventHash, signEvent } from 'nostr-tools';
|
|
||||||
import { useContext, useState } from 'react';
|
|
||||||
|
|
||||||
const MDEditor = dynamic(() => import('@uiw/react-md-editor').then((mod) => mod.default), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function CreatePost() {
|
|
||||||
const relayPool: any = useContext(RelayContext);
|
|
||||||
const [relays]: any = useLocalStorage('relays');
|
|
||||||
|
|
||||||
const [value, setValue] = useState('');
|
|
||||||
|
|
||||||
const [currentUser]: any = useLocalStorage('current-user');
|
|
||||||
const pubkey = currentUser.id;
|
|
||||||
const privkey = currentUser.privkey;
|
|
||||||
|
|
||||||
const postButton = {
|
|
||||||
name: 'post',
|
|
||||||
keyCommand: 'post',
|
|
||||||
buttonProps: { className: 'cta-btn', 'aria-label': 'Post a message' },
|
|
||||||
icon: (
|
|
||||||
<div className="relative inline-flex h-10 w-16 transform cursor-pointer overflow-hidden rounded bg-zinc-900 px-2.5 ring-zinc-500/50 ring-offset-zinc-900 will-change-transform focus:outline-none focus:ring-1 focus:ring-offset-2 active:translate-y-1">
|
|
||||||
<span className="absolute inset-px z-10 inline-flex items-center justify-center rounded bg-zinc-900 text-zinc-200">
|
|
||||||
Post
|
|
||||||
</span>
|
|
||||||
<span className="absolute inset-0 z-0 scale-x-[2.0] blur before:absolute before:inset-0 before:top-1/2 before:aspect-square before:animate-disco before:bg-gradient-conic before:from-gray-300 before:via-fuchsia-600 before:to-orange-600"></span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
execute: (state: { text: any }) => {
|
|
||||||
const message = state.text;
|
|
||||||
|
|
||||||
if (message.length > 0) {
|
|
||||||
const event: any = {
|
|
||||||
content: message,
|
|
||||||
created_at: dateToUnix(),
|
|
||||||
kind: 1,
|
|
||||||
pubkey: pubkey,
|
|
||||||
tags: [],
|
|
||||||
};
|
|
||||||
event.id = getEventHash(event);
|
|
||||||
event.sig = signEvent(event, privkey);
|
|
||||||
|
|
||||||
relayPool.publish(event, relays);
|
|
||||||
setValue('');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog.Root>
|
|
||||||
<Dialog.Trigger asChild>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<div className="relative h-16 shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
|
||||||
<textarea
|
|
||||||
readOnly
|
|
||||||
placeholder="What's your thought?"
|
|
||||||
className="relative h-16 w-full resize-none rounded-lg border border-black/5 px-3.5 py-3 text-sm shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button className="inline-flex h-9 w-full items-center justify-center rounded-lg border border-white/10 bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 text-sm font-semibold shadow-input">
|
|
||||||
<span className="drop-shadow-lg">Post</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Dialog.Trigger>
|
|
||||||
<Dialog.Portal>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm data-[state=open]:animate-overlayShow" />
|
|
||||||
<Dialog.Content className="fixed inset-0 overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
|
||||||
<div className="relative w-full max-w-2xl transform overflow-hidden rounded-lg text-zinc-100 shadow-modal transition-all">
|
|
||||||
<div className="absolute top-0 left-0 h-full w-full bg-black bg-opacity-20 backdrop-blur-lg"></div>
|
|
||||||
<div className="absolute bottom-0 left-0 h-24 w-full border-t border-white/10 bg-zinc-900"></div>
|
|
||||||
<div className="relative z-10 px-4 pt-4 pb-2">
|
|
||||||
<MDEditor
|
|
||||||
value={value}
|
|
||||||
preview={'edit'}
|
|
||||||
height={200}
|
|
||||||
minHeight={200}
|
|
||||||
visibleDragbar={false}
|
|
||||||
highlightEnable={false}
|
|
||||||
defaultTabEnable={true}
|
|
||||||
autoFocus={true}
|
|
||||||
commands={[
|
|
||||||
commands.bold,
|
|
||||||
commands.italic,
|
|
||||||
commands.strikethrough,
|
|
||||||
commands.divider,
|
|
||||||
commands.checkedListCommand,
|
|
||||||
commands.unorderedListCommand,
|
|
||||||
commands.orderedListCommand,
|
|
||||||
commands.divider,
|
|
||||||
commands.link,
|
|
||||||
commands.image,
|
|
||||||
]}
|
|
||||||
extraCommands={[postButton]}
|
|
||||||
textareaProps={{
|
|
||||||
placeholder: "What's your thought?",
|
|
||||||
}}
|
|
||||||
onChange={(val) => setValue(val)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,74 +1,13 @@
|
|||||||
import ActiveLink from '@components/activeLink';
|
import Messages from '@components/columns/navigator/messages';
|
||||||
import CreatePost from '@components/columns/navigator/createPost';
|
import Newsfeed from '@components/columns/navigator/newsfeed';
|
||||||
import { UserDropdownMenu } from '@components/columns/navigator/userDropdownMenu';
|
|
||||||
|
|
||||||
import { PlusIcon } from '@radix-ui/react-icons';
|
|
||||||
import { useLocalStorage } from '@rehooks/local-storage';
|
|
||||||
|
|
||||||
export default function NavigatorColumn() {
|
export default function NavigatorColumn() {
|
||||||
const [currentUser]: any = useLocalStorage('current-user');
|
|
||||||
const profile = JSON.parse(currentUser.metadata);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col flex-wrap justify-between overflow-hidden px-2 pt-3 pb-4">
|
<div className="relative flex h-full flex-col gap-1 overflow-hidden pt-4">
|
||||||
<div className="flex flex-col gap-4">
|
{/* Newsfeed */}
|
||||||
{/* Create post */}
|
<Newsfeed />
|
||||||
<div className="flex flex-col rounded-lg bg-zinc-900 ring-1 ring-white/10">
|
{/* Messages */}
|
||||||
<div className="flex flex-col p-2">
|
<Messages />
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h5 className="font-semibold leading-tight text-zinc-100">{profile.display_name || ''}</h5>
|
|
||||||
<UserDropdownMenu pubkey={currentUser.id} />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm leading-tight text-zinc-500">@{profile.username || ''}</span>
|
|
||||||
</div>
|
|
||||||
<div className="p-2">
|
|
||||||
<CreatePost />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Newsfeed */}
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between px-2">
|
|
||||||
<h3 className="text-sm font-bold text-zinc-400">Newsfeed</h3>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="group flex h-6 w-6 items-center justify-center rounded-full hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-3 w-3 text-zinc-400 group-hover:text-zinc-100" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1 text-zinc-500">
|
|
||||||
<ActiveLink
|
|
||||||
href={`/newsfeed/following`}
|
|
||||||
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
|
|
||||||
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<span>#</span>
|
|
||||||
<span>following</span>
|
|
||||||
</ActiveLink>
|
|
||||||
<ActiveLink
|
|
||||||
href={`/newsfeed/global`}
|
|
||||||
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
|
|
||||||
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<span>#</span>
|
|
||||||
<span>global</span>
|
|
||||||
</ActiveLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Messages */}
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between px-2">
|
|
||||||
<h3 className="text-sm font-bold text-zinc-400">Direct Messages</h3>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="group flex h-6 w-6 items-center justify-center rounded-full hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-3 w-3 text-zinc-400 group-hover:text-zinc-100" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/components/columns/navigator/messages/index.tsx
Normal file
44
src/components/columns/navigator/messages/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { MessageList } from '@components/columns/navigator/messages/list';
|
||||||
|
|
||||||
|
import { activeAccountAtom } from '@stores/account';
|
||||||
|
|
||||||
|
import { getAllFollowsByID } from '@utils/storage';
|
||||||
|
|
||||||
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
|
import { TriangleUpIcon } from '@radix-ui/react-icons';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function Messages() {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
const [follows, setFollows] = useState([]);
|
||||||
|
const [activeAccount] = useAtom(activeAccountAtom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAllFollowsByID(activeAccount.id)
|
||||||
|
.then((res: any) => setFollows(res))
|
||||||
|
.catch(console.error);
|
||||||
|
}, [activeAccount.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible.Root open={open} onOpenChange={setOpen} className="h-full shrink-0">
|
||||||
|
<div className="flex h-full flex-col gap-1 px-2 pb-8">
|
||||||
|
<Collapsible.Trigger className="flex cursor-pointer items-center gap-2 py-1 px-2">
|
||||||
|
<div
|
||||||
|
className={`inline-flex h-6 w-6 transform items-center justify-center transition-transform duration-150 ease-in-out ${
|
||||||
|
open ? 'rotate-180' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<TriangleUpIcon className="h-4 w-4 text-zinc-500" />
|
||||||
|
</div>
|
||||||
|
<h3 className="bg-gradient-to-r from-red-300 via-pink-100 to-blue-300 bg-clip-text text-xs font-bold uppercase tracking-wide text-transparent">
|
||||||
|
Chats
|
||||||
|
</h3>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content className="h-full">
|
||||||
|
<MessageList data={follows} />
|
||||||
|
</Collapsible.Content>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
src/components/columns/navigator/messages/list.tsx
Normal file
33
src/components/columns/navigator/messages/list.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { UserMini } from '@components/user/mini';
|
||||||
|
|
||||||
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
|
import { Suspense, memo, useRef } from 'react';
|
||||||
|
|
||||||
|
export const MessageList = memo(function MessageList({ data }: { data: any }) {
|
||||||
|
const parentRef = useRef(null);
|
||||||
|
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: data.length,
|
||||||
|
estimateSize: () => 32,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
});
|
||||||
|
const items = virtualizer.getVirtualItems();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={parentRef} className="scrollbar-hide h-full w-full overflow-y-auto" style={{ contain: 'strict' }}>
|
||||||
|
<Suspense fallback={<p className="text-sm text-zinc-400">Loading...</p>}>
|
||||||
|
{items.length > 0 && (
|
||||||
|
<div className="relative mb-24 w-full" style={{ height: virtualizer.getTotalSize() }}>
|
||||||
|
<div className="absolute top-0 left-0 w-full" style={{ transform: `translateY(${items[0].start}px)` }}>
|
||||||
|
{items.map((virtualRow) => (
|
||||||
|
<div key={virtualRow.key} data-index={virtualRow.index} ref={virtualizer.measureElement}>
|
||||||
|
<UserMini pubkey={data[virtualRow.index].pubkey} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
50
src/components/columns/navigator/newsfeed/index.tsx
Normal file
50
src/components/columns/navigator/newsfeed/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import ActiveLink from '@components/activeLink';
|
||||||
|
|
||||||
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
|
import { TriangleUpIcon } from '@radix-ui/react-icons';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export default function Newsfeed() {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||||
|
<div className="flex flex-col gap-1 px-2">
|
||||||
|
<Collapsible.Trigger className="flex cursor-pointer items-center gap-2 py-1 px-2">
|
||||||
|
<div
|
||||||
|
className={`inline-flex h-6 w-6 transform items-center justify-center transition-transform duration-150 ease-in-out ${
|
||||||
|
open ? 'rotate-180' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<TriangleUpIcon className="h-4 w-4 text-zinc-500" />
|
||||||
|
</div>
|
||||||
|
<h3 className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-xs font-bold uppercase tracking-wide text-transparent">
|
||||||
|
Newsfeed
|
||||||
|
</h3>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content className="flex flex-col gap-1 text-zinc-400">
|
||||||
|
<ActiveLink
|
||||||
|
href={`/newsfeed/following`}
|
||||||
|
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white hover:dark:bg-zinc-800"
|
||||||
|
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:bg-zinc-900"
|
||||||
|
>
|
||||||
|
<div className="inline-flex h-5 w-5 items-center justify-center">
|
||||||
|
<span className="h-4 w-3 rounded-sm bg-gradient-to-br from-fuchsia-500 via-purple-300 to-pink-300"></span>
|
||||||
|
</div>
|
||||||
|
<span>Following</span>
|
||||||
|
</ActiveLink>
|
||||||
|
<ActiveLink
|
||||||
|
href={`/newsfeed/circle`}
|
||||||
|
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white hover:dark:bg-zinc-800"
|
||||||
|
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:bg-zinc-900"
|
||||||
|
>
|
||||||
|
<div className="inline-flex h-5 w-5 items-center justify-center">
|
||||||
|
<span className="h-4 w-3 rounded-sm bg-gradient-to-br from-amber-500 via-orange-200 to-yellow-300"></span>
|
||||||
|
</div>
|
||||||
|
<span>Circle</span>
|
||||||
|
</ActiveLink>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
|
||||||
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
|
|
||||||
import { writeText } from '@tauri-apps/api/clipboard';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { nip19 } from 'nostr-tools';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
export const UserDropdownMenu = memo(function ProfileMenu({ pubkey }: { pubkey: string }) {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const viewProfile = () => {
|
|
||||||
router.push(`/profile/${pubkey}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateProfile = () => {
|
|
||||||
router.push('/profile/update');
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyPubkey = async () => {
|
|
||||||
const npub = nip19.npubEncode(pubkey);
|
|
||||||
await writeText(npub);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu.Root>
|
|
||||||
<DropdownMenu.Trigger asChild>
|
|
||||||
<button className="rounded-lg p-1 hover:bg-zinc-800">
|
|
||||||
<DotsHorizontalIcon className="h-4 w-4 text-zinc-300" />
|
|
||||||
</button>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Portal>
|
|
||||||
<DropdownMenu.Content
|
|
||||||
className="min-w-[220px] rounded-md border border-white/20 bg-zinc-800 p-1 shadow-lg shadow-black/30 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade"
|
|
||||||
sideOffset={2}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onClick={() => viewProfile()}
|
|
||||||
className="group relative flex h-[30px] select-none items-center rounded px-1 pl-6 text-sm leading-none text-zinc-100 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-700 data-[highlighted]:text-fuchsia-400 data-[disabled]:text-zinc-400"
|
|
||||||
>
|
|
||||||
View profile
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onClick={() => updateProfile()}
|
|
||||||
className="group relative flex h-[30px] select-none items-center rounded px-1 pl-6 text-sm leading-none text-zinc-100 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-700 data-[highlighted]:text-fuchsia-400 data-[disabled]:text-zinc-400"
|
|
||||||
>
|
|
||||||
Update profile
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onClick={() => copyPubkey()}
|
|
||||||
className="group relative flex h-[30px] select-none items-center rounded px-1 pl-6 text-sm leading-none text-zinc-100 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-700 data-[highlighted]:text-fuchsia-400 data-[disabled]:text-zinc-400"
|
|
||||||
>
|
|
||||||
Copy public key
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item className="group relative flex h-[30px] select-none items-center rounded px-1 pl-6 text-sm leading-none text-zinc-100 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-700 data-[highlighted]:text-fuchsia-400 data-[disabled]:text-zinc-400">
|
|
||||||
Log out
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { deleteFromStorage, writeStorage } from '@rehooks/local-storage';
|
|
||||||
import { createContext, useCallback, useEffect, useState } from 'react';
|
|
||||||
import Database from 'tauri-plugin-sql-api';
|
|
||||||
|
|
||||||
export const DatabaseContext = createContext({});
|
|
||||||
|
|
||||||
const db = typeof window !== 'undefined' ? await Database.load('sqlite:lume.db') : null;
|
|
||||||
|
|
||||||
export default function DatabaseProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
const [done, setDone] = useState(false);
|
|
||||||
|
|
||||||
const getRelays = useCallback(async () => {
|
|
||||||
const result: any[] = await db.select('SELECT relay_url FROM relays WHERE relay_status = "1"');
|
|
||||||
const arr = [];
|
|
||||||
result.forEach((item: { relay_url: string }) => {
|
|
||||||
arr.push(item.relay_url);
|
|
||||||
});
|
|
||||||
// delete old item then save new item to local storage
|
|
||||||
deleteFromStorage('relays');
|
|
||||||
writeStorage('relays', arr);
|
|
||||||
// return
|
|
||||||
return;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getAccount = useCallback(async () => {
|
|
||||||
const result = await db.select(`SELECT * FROM accounts LIMIT 1`);
|
|
||||||
// delete old item then save new item to local storage
|
|
||||||
deleteFromStorage('current-user');
|
|
||||||
if (result[0]) {
|
|
||||||
writeStorage('current-user', result[0]);
|
|
||||||
} else {
|
|
||||||
writeStorage('current-user', null);
|
|
||||||
}
|
|
||||||
// return first record
|
|
||||||
return result[0];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getFollows = useCallback(async (id: string) => {
|
|
||||||
const result: any[] = await db.select(`SELECT pubkey FROM follows WHERE account = "${id}"`);
|
|
||||||
const arr = [];
|
|
||||||
result.forEach((item: { pubkey: string }) => {
|
|
||||||
arr.push(item.pubkey);
|
|
||||||
});
|
|
||||||
// delete old item then save new item to local storage
|
|
||||||
deleteFromStorage('follows');
|
|
||||||
writeStorage('follows', arr);
|
|
||||||
// return
|
|
||||||
return;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getRelays().catch(console.error);
|
|
||||||
getAccount()
|
|
||||||
.then((res) => {
|
|
||||||
if (res) {
|
|
||||||
getFollows(res.id).catch(console.error);
|
|
||||||
}
|
|
||||||
setDone(true);
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
}, [getAccount, getFollows, getRelays]);
|
|
||||||
|
|
||||||
if (!done) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <DatabaseContext.Provider value={{ db }}>{children}</DatabaseContext.Provider>;
|
|
||||||
}
|
|
||||||
81
src/components/form/base.tsx
Normal file
81
src/components/form/base.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import EmojiPicker from '@components/form/emojiPicker';
|
||||||
|
import ImagePicker from '@components/form/imagePicker';
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { activeAccountAtom } from '@stores/account';
|
||||||
|
import { noteContentAtom } from '@stores/note';
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
|
import { PersonIcon } from '@radix-ui/react-icons';
|
||||||
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
|
import { useResetAtom } from 'jotai/utils';
|
||||||
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
export default function FormBase() {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
const [activeAccount] = useAtom(activeAccountAtom);
|
||||||
|
const [value, setValue] = useAtom(noteContentAtom);
|
||||||
|
const resetValue = useResetAtom(noteContentAtom);
|
||||||
|
|
||||||
|
const pubkey = activeAccount.id;
|
||||||
|
const privkey = activeAccount.privkey;
|
||||||
|
|
||||||
|
const submitEvent = () => {
|
||||||
|
const event: any = {
|
||||||
|
content: value,
|
||||||
|
created_at: dateToUnix(),
|
||||||
|
kind: 1,
|
||||||
|
pubkey: pubkey,
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = signEvent(event, privkey);
|
||||||
|
|
||||||
|
// publish note
|
||||||
|
pool.publish(event, relays);
|
||||||
|
// reset form
|
||||||
|
resetValue();
|
||||||
|
// send notification
|
||||||
|
// sendNotification('Note has been published successfully');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="relative h-32 w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
spellCheck={false}
|
||||||
|
placeholder="What's your thought?"
|
||||||
|
className="relative h-32 w-full resize-none rounded-lg border border-black/5 px-3.5 py-3 text-sm shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-2 w-full px-2">
|
||||||
|
<div className="flex w-full items-center justify-between bg-zinc-800">
|
||||||
|
<div className="flex items-center gap-2 divide-x divide-zinc-700">
|
||||||
|
<ImagePicker />
|
||||||
|
<div className="flex items-center gap-2 pl-2">
|
||||||
|
<EmojiPicker />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => submitEvent()}
|
||||||
|
disabled={value.length === 0 ? true : false}
|
||||||
|
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
src/components/form/comment.tsx
Normal file
81
src/components/form/comment.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { activeAccountAtom } from '@stores/account';
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
|
import destr from 'destr';
|
||||||
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
|
import { useContext, useState } from 'react';
|
||||||
|
|
||||||
|
export default function FormComment({ eventID }: { eventID: any }) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
const [activeAccount] = useAtom(activeAccountAtom);
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
|
const profile = destr(activeAccount.metadata);
|
||||||
|
|
||||||
|
const submitEvent = () => {
|
||||||
|
const event: any = {
|
||||||
|
content: value,
|
||||||
|
created_at: dateToUnix(),
|
||||||
|
kind: 1,
|
||||||
|
pubkey: activeAccount.id,
|
||||||
|
tags: [['e', eventID]],
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = signEvent(event, activeAccount.privkey);
|
||||||
|
|
||||||
|
// publish note
|
||||||
|
pool.publish(event, relays);
|
||||||
|
// send notification
|
||||||
|
// sendNotification('Comment has been published successfully');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<div>
|
||||||
|
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
||||||
|
<ImageWithFallback
|
||||||
|
src={profile?.picture}
|
||||||
|
alt={activeAccount.id}
|
||||||
|
fill={true}
|
||||||
|
className="rounded-md object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative h-24 w-full flex-1 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
name="content"
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
placeholder="Send your comment"
|
||||||
|
className="relative h-24 w-full resize-none rounded-md border border-black/5 px-3.5 py-3 text-sm shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-2 w-full px-2">
|
||||||
|
<div className="flex w-full items-center justify-between bg-zinc-800">
|
||||||
|
<div className="flex items-center gap-2 divide-x divide-zinc-700"></div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => submitEvent()}
|
||||||
|
disabled={value.length === 0 ? true : false}
|
||||||
|
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-button hover:bg-fuchsia-600 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
src/components/form/emojiPicker.tsx
Normal file
37
src/components/form/emojiPicker.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { noteContentAtom } from '@stores/note';
|
||||||
|
|
||||||
|
import EmojiIcon from '@assets/icons/emoji';
|
||||||
|
|
||||||
|
import data from '@emoji-mart/data';
|
||||||
|
import Picker from '@emoji-mart/react';
|
||||||
|
import * as Popover from '@radix-ui/react-popover';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
|
||||||
|
export default function EmojiPicker() {
|
||||||
|
const [value, setValue] = useAtom(noteContentAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<button className="inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-zinc-700">
|
||||||
|
<EmojiIcon className="h-4 w-4 text-zinc-400" />
|
||||||
|
</button>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Portal>
|
||||||
|
<Popover.Content
|
||||||
|
className="rounded-md will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
|
||||||
|
sideOffset={5}
|
||||||
|
>
|
||||||
|
<Picker
|
||||||
|
data={data}
|
||||||
|
emojiSize={16}
|
||||||
|
navPosition={'none'}
|
||||||
|
skinTonePosition={'none'}
|
||||||
|
onEmojiSelect={(res) => setValue(value + ' ' + res.native)}
|
||||||
|
/>
|
||||||
|
<Popover.Arrow className="fill-[#141516]" />
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Portal>
|
||||||
|
</Popover.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/components/form/imagePicker.tsx
Normal file
51
src/components/form/imagePicker.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { noteContentAtom } from '@stores/note';
|
||||||
|
|
||||||
|
import { PlusIcon } from '@radix-ui/react-icons';
|
||||||
|
import * as Popover from '@radix-ui/react-popover';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export default function ImagePicker() {
|
||||||
|
const [value, setValue] = useAtom(noteContentAtom);
|
||||||
|
const [url, setURL] = useState('');
|
||||||
|
|
||||||
|
const handleEnter = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
setValue(value + ' ' + url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<button className="inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-zinc-700">
|
||||||
|
<PlusIcon className="h-4 w-4 text-zinc-400" />
|
||||||
|
</button>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Portal>
|
||||||
|
<Popover.Content
|
||||||
|
className="w-80 rounded-md bg-zinc-900/80 p-3 shadow-input shadow-black/50 ring-1 ring-zinc-800 backdrop-blur-xl will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
|
||||||
|
sideOffset={3}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-semibold text-zinc-200">Image URL</label>
|
||||||
|
<div className="relative mb-1 shrink-0 before:pointer-events-none before:absolute before:-inset-px before:rounded-[8px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-1 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
|
||||||
|
<input
|
||||||
|
placeholder="https://..."
|
||||||
|
onKeyDown={handleEnter}
|
||||||
|
onChange={(e) => setURL(e.target.value)}
|
||||||
|
className="relative w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm leading-none text-zinc-500">
|
||||||
|
Press <span className="rounded bg-zinc-800 px-1 py-0.5">Enter</span> to insert image
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Portal>
|
||||||
|
</Popover.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,10 +26,20 @@ export const ImageWithFallback = memo(function ImageWithFallback({
|
|||||||
size={44}
|
size={44}
|
||||||
name={alt}
|
name={alt}
|
||||||
variant="beam"
|
variant="beam"
|
||||||
|
square={true}
|
||||||
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
|
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Image alt={alt} onError={setError} src={src} fill={fill} className={className} />
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
fill={fill}
|
||||||
|
className={className}
|
||||||
|
onError={setError}
|
||||||
|
placeholder="blur"
|
||||||
|
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkqAcAAIUAgUW0RjgAAAAASUVORK5CYII="
|
||||||
|
priority
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import { RelayContext } from '@components/contexts/relay';
|
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/getDate';
|
|
||||||
|
|
||||||
import { HeartFilledIcon, HeartIcon } from '@radix-ui/react-icons';
|
|
||||||
import { useLocalStorage } from '@rehooks/local-storage';
|
|
||||||
import { getEventHash, signEvent } from 'nostr-tools';
|
|
||||||
import { useContext, useState } from 'react';
|
|
||||||
|
|
||||||
export default function Reaction({ eventID, eventPubkey }: { eventID: string; eventPubkey: string }) {
|
|
||||||
const relayPool: any = useContext(RelayContext);
|
|
||||||
const [relays]: any = useLocalStorage('relays');
|
|
||||||
|
|
||||||
const [reaction, setReaction] = useState(0);
|
|
||||||
const [isReact, setIsReact] = useState(false);
|
|
||||||
|
|
||||||
const [currentUser]: any = useLocalStorage('current-user');
|
|
||||||
const pubkey = currentUser.id;
|
|
||||||
const privkey = currentUser.privkey;
|
|
||||||
|
|
||||||
/*
|
|
||||||
relayPool.subscribe(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
'#e': [eventID],
|
|
||||||
since: 0,
|
|
||||||
kinds: [7],
|
|
||||||
limit: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
relays,
|
|
||||||
(event: any) => {
|
|
||||||
if (event.content === '🤙' || event.content === '+') {
|
|
||||||
setReaction(reaction + 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
(events: any, relayURL: any) => {
|
|
||||||
console.log(events, relayURL);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
const handleReaction = (e: any) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const event: any = {
|
|
||||||
content: '+',
|
|
||||||
kind: 7,
|
|
||||||
tags: [
|
|
||||||
['e', eventID],
|
|
||||||
['p', eventPubkey],
|
|
||||||
],
|
|
||||||
created_at: dateToUnix(),
|
|
||||||
pubkey: pubkey,
|
|
||||||
};
|
|
||||||
event.id = getEventHash(event);
|
|
||||||
event.sig = signEvent(event, privkey);
|
|
||||||
|
|
||||||
relayPool.publish(event, relays);
|
|
||||||
|
|
||||||
setIsReact(true);
|
|
||||||
setReaction(reaction + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button onClick={(e) => handleReaction(e)} className="group flex w-16 items-center gap-1.5 text-sm text-zinc-500">
|
|
||||||
<div className="rounded-lg p-1 group-hover:bg-zinc-600">
|
|
||||||
{isReact ? (
|
|
||||||
<HeartFilledIcon className="h-4 w-4 group-hover:text-red-400" />
|
|
||||||
) : (
|
|
||||||
<HeartIcon className="h-4 w-4 text-zinc-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span>{reaction}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { ChatBubbleIcon } from '@radix-ui/react-icons';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
export default function Reply() {
|
|
||||||
const [count] = useState(0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button className="group flex w-16 items-center gap-1.5 text-sm text-zinc-500">
|
|
||||||
<div className="rounded-lg p-1 group-hover:bg-zinc-600">
|
|
||||||
<ChatBubbleIcon className="h-4 w-4 group-hover:text-orange-400" />
|
|
||||||
</div>
|
|
||||||
<span>{count}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { DatabaseContext } from '@components/contexts/database';
|
|
||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { truncate } from '@utils/truncate';
|
|
||||||
|
|
||||||
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
|
|
||||||
import Avatar from 'boring-avatars';
|
|
||||||
import { memo, useCallback, useContext, useEffect, useState } from 'react';
|
|
||||||
import Moment from 'react-moment';
|
|
||||||
|
|
||||||
export const User = memo(function User({ pubkey, time }: { pubkey: string; time: any }) {
|
|
||||||
const { db }: any = useContext(DatabaseContext);
|
|
||||||
const [profile, setProfile] = useState({ picture: null, name: null, username: null });
|
|
||||||
|
|
||||||
const insertCacheProfile = useCallback(
|
|
||||||
async (event) => {
|
|
||||||
const metadata: any = JSON.parse(event.content);
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
`INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES ("${pubkey}", '${JSON.stringify(metadata)}')`
|
|
||||||
);
|
|
||||||
setProfile(metadata);
|
|
||||||
},
|
|
||||||
[db, pubkey]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getCacheProfile = useCallback(async () => {
|
|
||||||
const result: any = await db.select(`SELECT metadata FROM cache_profiles WHERE id = "${pubkey}"`);
|
|
||||||
return result;
|
|
||||||
}, [db, pubkey]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getCacheProfile()
|
|
||||||
.then((res) => {
|
|
||||||
if (res[0] !== undefined) {
|
|
||||||
setProfile(JSON.parse(res[0].metadata));
|
|
||||||
} else {
|
|
||||||
fetch(`https://rbr.bio/${pubkey}/metadata.json`).then((res) =>
|
|
||||||
res.json().then((res) => {
|
|
||||||
// update state
|
|
||||||
setProfile(JSON.parse(res.content));
|
|
||||||
// save profile to database
|
|
||||||
insertCacheProfile(res);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
}, [getCacheProfile, insertCacheProfile, pubkey]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-start gap-4">
|
|
||||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
|
||||||
{profile.picture ? (
|
|
||||||
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<Avatar
|
|
||||||
size={44}
|
|
||||||
name={pubkey}
|
|
||||||
variant="beam"
|
|
||||||
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-1 items-start justify-between">
|
|
||||||
<div className="flex w-full justify-between">
|
|
||||||
<div className="flex items-baseline gap-2 text-sm">
|
|
||||||
<span className="font-bold leading-tight">
|
|
||||||
{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}
|
|
||||||
</span>
|
|
||||||
<span className="leading-tight text-zinc-500">·</span>
|
|
||||||
<Moment fromNow unix className="text-zinc-500">
|
|
||||||
{time}
|
|
||||||
</Moment>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<DotsHorizontalIcon className="h-4 w-4 text-zinc-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { truncate } from '@utils/truncate';
|
|
||||||
|
|
||||||
import { memo, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export const UserRepost = memo(function UserRepost({ pubkey }: { pubkey: string }) {
|
|
||||||
const [profile, setProfile] = useState({ picture: null, name: null });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch(`https://rbr.bio/${pubkey}/metadata.json`).then((res) =>
|
|
||||||
res.json().then((res) => {
|
|
||||||
// update state
|
|
||||||
setProfile(JSON.parse(res.content));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [pubkey]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="text-zinc-400">
|
|
||||||
<p>{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')} repost</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
|
||||||
|
|
||||||
import { truncate } from '@utils/truncate';
|
|
||||||
|
|
||||||
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
|
|
||||||
import Avatar from 'boring-avatars';
|
|
||||||
import { memo, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export const UserWithUsername = memo(function UserWithUsername({ pubkey }: { pubkey: string }) {
|
|
||||||
const [profile, setProfile] = useState({ picture: null, name: null, username: null });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch(`https://rbr.bio/${pubkey}/metadata.json`).then((res) =>
|
|
||||||
res.json().then((res) => {
|
|
||||||
// update state
|
|
||||||
setProfile(JSON.parse(res.content));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [pubkey]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-start gap-2">
|
|
||||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
|
||||||
{profile.picture ? (
|
|
||||||
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<Avatar
|
|
||||||
size={44}
|
|
||||||
name={pubkey}
|
|
||||||
variant="beam"
|
|
||||||
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-1 items-start justify-between">
|
|
||||||
<div className="flex w-full justify-between">
|
|
||||||
<div className="flex flex-col gap-1 text-sm">
|
|
||||||
<span className="font-bold leading-tight">
|
|
||||||
{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}
|
|
||||||
</span>
|
|
||||||
<span className="text-zinc-500">
|
|
||||||
{profile.username ? profile.username : truncate(pubkey, 16, ' .... ')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<DotsHorizontalIcon className="h-4 w-4 text-zinc-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
106
src/components/note/base.tsx
Normal file
106
src/components/note/base.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import NoteMetadata from '@components/note/metadata';
|
||||||
|
import { NoteParent } from '@components/note/parent';
|
||||||
|
import { ImagePreview } from '@components/note/preview/image';
|
||||||
|
import { VideoPreview } from '@components/note/preview/video';
|
||||||
|
import { NoteRepost } from '@components/note/repost';
|
||||||
|
import { UserExtend } from '@components/user/extend';
|
||||||
|
import { UserMention } from '@components/user/mention';
|
||||||
|
|
||||||
|
import destr from 'destr';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import ReactPlayer from 'react-player/lazy';
|
||||||
|
import reactStringReplace from 'react-string-replace';
|
||||||
|
|
||||||
|
export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
let parsedContent = event.content;
|
||||||
|
// get data tags
|
||||||
|
const tags = destr(event.tags);
|
||||||
|
// handle urls
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => {
|
||||||
|
if (match.toLowerCase().match(/\.(jpg|jpeg|gif|png|webp)$/)) {
|
||||||
|
// image url
|
||||||
|
return <ImagePreview key={match + i} url={match} />;
|
||||||
|
} else if (ReactPlayer.canPlay(match)) {
|
||||||
|
return <VideoPreview key={match + i} url={match} />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<a key={match + i} href={match} target="_blank" rel="noreferrer">
|
||||||
|
{match}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// handle #-hashtags
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /#(\w+)/g, (match, i) => (
|
||||||
|
<span key={match + i} className="cursor-pointer text-fuchsia-500">
|
||||||
|
#{match}
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
// handle mentions
|
||||||
|
if (tags.length > 0) {
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => {
|
||||||
|
if (tags[match][0] === 'p') {
|
||||||
|
// @-mentions
|
||||||
|
return <UserMention key={match + i} pubkey={tags[match][1]} />;
|
||||||
|
} else if (tags[match][0] === 'e') {
|
||||||
|
// note-mentions
|
||||||
|
return <NoteRepost key={match + i} id={tags[match][1]} />;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedContent;
|
||||||
|
}, [event.content, event.tags]);
|
||||||
|
|
||||||
|
const getParent = useMemo(() => {
|
||||||
|
if (event.parent_id) {
|
||||||
|
if (event.parent_id !== event.id && !event.content.includes('#[0]')) {
|
||||||
|
return <NoteParent id={event.parent_id} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}, [event.content, event.id, event.parent_id]);
|
||||||
|
|
||||||
|
const openThread = (e) => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection.toString().length === 0) {
|
||||||
|
router.push(`/newsfeed/${event.parent_id}`);
|
||||||
|
} else {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={(e) => openThread(e)}
|
||||||
|
className="relative z-10 flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 py-5 px-3 hover:bg-black/20"
|
||||||
|
>
|
||||||
|
<>{getParent}</>
|
||||||
|
<div className="relative z-10 flex flex-col">
|
||||||
|
<UserExtend pubkey={event.pubkey} time={event.created_at} />
|
||||||
|
<div className="-mt-5 pl-[52px]">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||||
|
<NoteMetadata
|
||||||
|
eventID={event.id}
|
||||||
|
eventPubkey={event.pubkey}
|
||||||
|
eventContent={event.content}
|
||||||
|
eventTime={event.created_at}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
76
src/components/note/comment.tsx
Normal file
76
src/components/note/comment.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import NoteMetadata from '@components/note/metadata';
|
||||||
|
import { ImagePreview } from '@components/note/preview/image';
|
||||||
|
import { VideoPreview } from '@components/note/preview/video';
|
||||||
|
import { NoteRepost } from '@components/note/repost';
|
||||||
|
import { UserExtend } from '@components/user/extend';
|
||||||
|
import { UserMention } from '@components/user/mention';
|
||||||
|
|
||||||
|
import destr from 'destr';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import ReactPlayer from 'react-player/lazy';
|
||||||
|
import reactStringReplace from 'react-string-replace';
|
||||||
|
|
||||||
|
export const NoteComment = memo(function NoteComment({ event }: { event: any }) {
|
||||||
|
const content = useMemo(() => {
|
||||||
|
let parsedContent = event.content;
|
||||||
|
// get data tags
|
||||||
|
const tags = destr(event.tags);
|
||||||
|
// handle urls
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => {
|
||||||
|
if (match.toLowerCase().match(/\.(jpg|jpeg|gif|png|webp)$/)) {
|
||||||
|
// image url
|
||||||
|
return <ImagePreview key={match + i} url={match} />;
|
||||||
|
} else if (ReactPlayer.canPlay(match)) {
|
||||||
|
return <VideoPreview key={match + i} url={match} />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<a key={match + i} href={match} target="_blank" rel="noreferrer">
|
||||||
|
{match}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// handle #-hashtags
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /#(\w+)/g, (match, i) => (
|
||||||
|
<span key={match + i} className="cursor-pointer text-fuchsia-500">
|
||||||
|
#{match}
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
// handle mentions
|
||||||
|
if (tags.length > 0) {
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => {
|
||||||
|
if (tags[match][0] === 'p') {
|
||||||
|
// @-mentions
|
||||||
|
return <UserMention key={match + i} pubkey={tags[match][1]} />;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedContent;
|
||||||
|
}, [event.content, event.tags]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative z-10 flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 py-5 px-3 hover:bg-black/20">
|
||||||
|
<div className="relative z-10 flex flex-col">
|
||||||
|
<UserExtend pubkey={event.pubkey} time={event.created_at} />
|
||||||
|
<div className="-mt-5 pl-[52px]">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||||
|
<NoteMetadata
|
||||||
|
eventID={event.id}
|
||||||
|
eventPubkey={event.pubkey}
|
||||||
|
eventContent={event.content}
|
||||||
|
eventTime={event.created_at}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,111 +1,71 @@
|
|||||||
import { DatabaseContext } from '@components/contexts/database';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
import { RelayContext } from '@components/contexts/relay';
|
|
||||||
|
|
||||||
import { dateToUnix, hoursAgo } from '@utils/getDate';
|
import { activeAccountAtom } from '@stores/account';
|
||||||
|
import { hasNewerNoteAtom } from '@stores/note';
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
import { ReloadIcon } from '@radix-ui/react-icons';
|
import { dateToUnix } from '@utils/getDate';
|
||||||
import { useLocalStorage } from '@rehooks/local-storage';
|
import { createCacheNote, getAllFollowsByID, updateLastLoginTime } from '@utils/storage';
|
||||||
import { memo, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
import { pubkeyArray } from '@utils/transform';
|
||||||
|
|
||||||
export const NoteConnector = memo(function NoteConnector({
|
import { TauriEvent } from '@tauri-apps/api/event';
|
||||||
setParentReload,
|
import { appWindow, getCurrent } from '@tauri-apps/api/window';
|
||||||
setHasNewNote,
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
currentDate,
|
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||||
}: {
|
|
||||||
setParentReload: any;
|
|
||||||
setHasNewNote: any;
|
|
||||||
currentDate: any;
|
|
||||||
}) {
|
|
||||||
const { db }: any = useContext(DatabaseContext);
|
|
||||||
const relayPool: any = useContext(RelayContext);
|
|
||||||
|
|
||||||
const [follows]: any = useLocalStorage('follows');
|
export default function NoteConnector() {
|
||||||
const [relays]: any = useLocalStorage('relays');
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
const [reload, setReload] = useState(false);
|
const setHasNewerNote = useSetAtom(hasNewerNoteAtom);
|
||||||
const timeout = useRef(null);
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
const [activeAccount] = useAtom(activeAccountAtom);
|
||||||
|
|
||||||
const reloadNewsfeed = () => {
|
const [isOnline] = useState(true);
|
||||||
setParentReload(true);
|
const now = useRef(new Date());
|
||||||
setReload(true);
|
|
||||||
timeout.current = setTimeout(() => {
|
|
||||||
setReload(false);
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const insertDB = useCallback(
|
const subscribe = useCallback(() => {
|
||||||
async (event: any) => {
|
getAllFollowsByID(activeAccount.id).then((follows) => {
|
||||||
await db.execute(
|
pool.subscribe(
|
||||||
`INSERT OR IGNORE INTO
|
[
|
||||||
cache_notes
|
{
|
||||||
(id, pubkey, created_at, kind, tags, content) VALUES
|
kinds: [1],
|
||||||
(
|
authors: pubkeyArray(follows),
|
||||||
"${event.id}",
|
since: dateToUnix(now.current),
|
||||||
"${event.pubkey}",
|
},
|
||||||
"${event.created_at}",
|
],
|
||||||
"${event.kind}",
|
relays,
|
||||||
'${JSON.stringify(event.tags)}',
|
(event: any) => {
|
||||||
"${event.content}"
|
// insert event to local database
|
||||||
);`
|
createCacheNote(event);
|
||||||
);
|
setHasNewerNote(true);
|
||||||
},
|
|
||||||
[db]
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchEvent = useCallback(() => {
|
|
||||||
relayPool.subscribe(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
kinds: [1],
|
|
||||||
authors: follows,
|
|
||||||
since: dateToUnix(hoursAgo(12, currentDate)),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
relays,
|
|
||||||
(event: any) => {
|
|
||||||
// show trigger update newer event
|
|
||||||
if (event.created_at > dateToUnix(currentDate)) {
|
|
||||||
setHasNewNote(true);
|
|
||||||
}
|
}
|
||||||
// insert event to local database
|
);
|
||||||
insertDB(event).catch(console.error);
|
});
|
||||||
},
|
}, [activeAccount.id, pool, relays, setHasNewerNote]);
|
||||||
undefined,
|
|
||||||
(events: any, relayURL: any) => {
|
|
||||||
console.log(events, relayURL);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [relayPool, follows, currentDate, relays, insertDB, setHasNewNote]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchEvent();
|
subscribe();
|
||||||
|
getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => {
|
||||||
return () => {
|
updateLastLoginTime(now.current);
|
||||||
clearTimeout(timeout.current);
|
appWindow.close();
|
||||||
};
|
});
|
||||||
}, [fetchEvent]);
|
}, [activeAccount.id, pool, relays, setHasNewerNote, subscribe]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-12 items-center justify-between border-b border-zinc-800 px-6 shadow-input">
|
<>
|
||||||
<div>
|
<div className="inline-flex items-center gap-1 rounded-md py-1 px-1.5 hover:bg-zinc-900">
|
||||||
<h3 className="text-sm font-semibold text-zinc-500"># following</h3>
|
<span className="relative flex h-1.5 w-1.5">
|
||||||
|
<span
|
||||||
|
className={`absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 ${
|
||||||
|
isOnline ? 'bg-green-400' : 'bg-red-400'
|
||||||
|
}`}
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
className={`relative inline-flex h-1.5 w-1.5 rounded-full ${isOnline ? 'bg-green-400' : 'bg-amber-400'}`}
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
<p className="text-xs font-medium text-zinc-500">{isOnline ? 'Online' : 'Offline'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
</>
|
||||||
<button
|
|
||||||
onClick={() => reloadNewsfeed()}
|
|
||||||
className={`${reload ? 'animate-spin' : ''} rounded-full p-1 hover:bg-zinc-800`}
|
|
||||||
>
|
|
||||||
<ReloadIcon className="h-3.5 w-3.5 text-zinc-500" />
|
|
||||||
</button>
|
|
||||||
<div className="inline-flex items-center gap-1 rounded-full border border-zinc-700 bg-zinc-800 px-2.5 py-1">
|
|
||||||
{/* #TODO: get user network status */}
|
|
||||||
<span className="relative flex h-1.5 w-1.5">
|
|
||||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-green-500"></span>
|
|
||||||
</span>
|
|
||||||
<p className="text-xs font-medium text-zinc-500">Online</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import Reaction from '@components/note/atoms/reaction';
|
|
||||||
import Reply from '@components/note/atoms/reply';
|
|
||||||
import { User } from '@components/note/atoms/user';
|
|
||||||
import { ImageCard } from '@components/note/content/preview/imageCard';
|
|
||||||
import { Video } from '@components/note/content/preview/video';
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import ReactPlayer from 'react-player';
|
|
||||||
|
|
||||||
const MarkdownPreview = dynamic(() => import('@uiw/react-markdown-preview'), {
|
|
||||||
ssr: false,
|
|
||||||
loading: () => <div className="h-4 w-36 animate-pulse rounded bg-zinc-700" />,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Content = memo(function Content({ data }: { data: any }) {
|
|
||||||
const [preview, setPreview] = useState({});
|
|
||||||
|
|
||||||
const content = useRef(data.content);
|
|
||||||
const urls = useMemo(
|
|
||||||
() =>
|
|
||||||
content.current.match(
|
|
||||||
/((http|ftp|https):\/\/)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (urls !== null && urls.length > 0) {
|
|
||||||
// #TODO: support multiple url
|
|
||||||
let url = urls[0];
|
|
||||||
// make sure url alway have http://
|
|
||||||
if (!/^https?:\/\//i.test(url)) {
|
|
||||||
url = 'http://' + url;
|
|
||||||
}
|
|
||||||
// parse url with new URL();
|
|
||||||
const parseURL = new URL(url, 'https://uselume.xyz');
|
|
||||||
// #TODO performance test
|
|
||||||
if (parseURL.pathname.toLowerCase().match(/\.(jpg|jpeg|gif|png|webp)$/)) {
|
|
||||||
// add image to preview
|
|
||||||
setPreview({ image: parseURL.href, type: 'image' });
|
|
||||||
content.current = content.current.replace(parseURL.href, '');
|
|
||||||
} else if (ReactPlayer.canPlay(parseURL.href)) {
|
|
||||||
// add video to preview
|
|
||||||
setPreview({ url: parseURL.href, type: 'video' });
|
|
||||||
content.current = content.current.replace(parseURL.href, '');
|
|
||||||
} // #TODO: support multiple previ3ew
|
|
||||||
}
|
|
||||||
}, [urls]);
|
|
||||||
|
|
||||||
const previewAttachment = useCallback(() => {
|
|
||||||
if (Object.keys(preview).length > 0) {
|
|
||||||
switch (preview['type']) {
|
|
||||||
case 'image':
|
|
||||||
return <ImageCard data={preview} />;
|
|
||||||
case 'video':
|
|
||||||
return <Video data={preview} />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [preview]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<User pubkey={data.pubkey} time={data.created_at} />
|
|
||||||
<div className="-mt-4 pl-[60px]">
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div>
|
|
||||||
<MarkdownPreview
|
|
||||||
source={content.current}
|
|
||||||
className={
|
|
||||||
'prose prose-zinc max-w-none break-words dark:prose-invert prose-headings:mt-3 prose-headings:mb-2 prose-p:m-0 prose-p:leading-normal prose-ul:mt-2 prose-li:my-1'
|
|
||||||
}
|
|
||||||
linkTarget="_blank"
|
|
||||||
disallowedElements={[
|
|
||||||
'Table',
|
|
||||||
'Heading ID',
|
|
||||||
'Highlight',
|
|
||||||
'Fenced Code Block',
|
|
||||||
'Footnote',
|
|
||||||
'Definition List',
|
|
||||||
'Task List',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<>{previewAttachment()}</>
|
|
||||||
</div>
|
|
||||||
<div className="relative z-10 -ml-1 flex items-center gap-8">
|
|
||||||
<Reply eventID={data.id} />
|
|
||||||
<Reaction eventID={data.id} eventPubkey={data.pubkey} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import Image from 'next/image';
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const ImageCard = memo(function ImageCard({ data }: { data: object }) {
|
|
||||||
return (
|
|
||||||
<div className={`relative mt-2 flex flex-col overflow-hidden`}>
|
|
||||||
<div className="relative h-full w-full rounded-lg border border-zinc-800">
|
|
||||||
<Image
|
|
||||||
placeholder="blur"
|
|
||||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkqAcAAIUAgUW0RjgAAAAASUVORK5CYII="
|
|
||||||
src={data['image']}
|
|
||||||
alt={data['image']}
|
|
||||||
width="0"
|
|
||||||
height="0"
|
|
||||||
sizes="100vw"
|
|
||||||
className=" h-auto w-full rounded-lg object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import ReactPlayer from 'react-player/lazy';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const Video = memo(function Video({ data }: { data: object }) {
|
|
||||||
return (
|
|
||||||
<div className="relative mt-2 flex flex-col overflow-hidden rounded-xl border border-zinc-800">
|
|
||||||
<ReactPlayer
|
|
||||||
url={data['url']}
|
|
||||||
controls={true}
|
|
||||||
volume={0}
|
|
||||||
className="aspect-video w-full"
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
79
src/components/note/extend.tsx
Normal file
79
src/components/note/extend.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import NoteMetadata from '@components/note/metadata';
|
||||||
|
import { ImagePreview } from '@components/note/preview/image';
|
||||||
|
import { VideoPreview } from '@components/note/preview/video';
|
||||||
|
import { NoteRepost } from '@components/note/repost';
|
||||||
|
import { UserLarge } from '@components/user/large';
|
||||||
|
import { UserMention } from '@components/user/mention';
|
||||||
|
|
||||||
|
import destr from 'destr';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import ReactPlayer from 'react-player/lazy';
|
||||||
|
import reactStringReplace from 'react-string-replace';
|
||||||
|
|
||||||
|
export const NoteExtend = memo(function NoteExtend({ event }: { event: any }) {
|
||||||
|
const content = useMemo(() => {
|
||||||
|
let parsedContent = event.content;
|
||||||
|
// get data tags
|
||||||
|
const tags = destr(event.tags);
|
||||||
|
// handle urls
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => {
|
||||||
|
if (match.toLowerCase().match(/\.(jpg|jpeg|gif|png|webp)$/)) {
|
||||||
|
// image url
|
||||||
|
return <ImagePreview key={match + i} url={match} />;
|
||||||
|
} else if (ReactPlayer.canPlay(match)) {
|
||||||
|
return <VideoPreview key={match + i} url={match} />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<a key={match + i} href={match} target="_blank" rel="noreferrer">
|
||||||
|
{match}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// handle #-hashtags
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /#(\w+)/g, (match, i) => (
|
||||||
|
<span key={match + i} className="cursor-pointer text-fuchsia-500">
|
||||||
|
#{match}
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
// handle mentions
|
||||||
|
if (tags.length > 0) {
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => {
|
||||||
|
if (tags[match][0] === 'p') {
|
||||||
|
// @-mentions
|
||||||
|
return <UserMention key={match + i} pubkey={tags[match][1]} />;
|
||||||
|
} else if (tags[match][0] === 'e') {
|
||||||
|
// note-mentions
|
||||||
|
return <NoteRepost key={match + i} id={tags[match][1]} />;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedContent;
|
||||||
|
}, [event.content, event.tags]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative z-10 flex h-min min-h-min w-full select-text flex-col">
|
||||||
|
<div className="relative z-10 flex flex-col">
|
||||||
|
<UserLarge pubkey={event.pubkey} time={event.created_at} />
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex items-center border-t border-b border-zinc-800 py-2">
|
||||||
|
<NoteMetadata
|
||||||
|
eventID={event.id}
|
||||||
|
eventPubkey={event.pubkey}
|
||||||
|
eventContent={event.content}
|
||||||
|
eventTime={event.created_at}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
143
src/components/note/meta/comment.tsx
Normal file
143
src/components/note/meta/comment.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
import { UserExtend } from '@components/user/extend';
|
||||||
|
|
||||||
|
import { activeAccountAtom } from '@stores/account';
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
|
import CommentIcon from '@assets/icons/comment';
|
||||||
|
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { SizeIcon } from '@radix-ui/react-icons';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
|
import { memo, useContext, useState } from 'react';
|
||||||
|
|
||||||
|
export const NoteComment = memo(function NoteComment({
|
||||||
|
count,
|
||||||
|
eventID,
|
||||||
|
eventPubkey,
|
||||||
|
eventContent,
|
||||||
|
eventTime,
|
||||||
|
}: {
|
||||||
|
count: number;
|
||||||
|
eventID: string;
|
||||||
|
eventPubkey: string;
|
||||||
|
eventTime: string;
|
||||||
|
eventContent: any;
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
const [activeAccount] = useAtom(activeAccountAtom);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
|
const profile = destr(activeAccount.metadata);
|
||||||
|
|
||||||
|
const openThread = () => {
|
||||||
|
router.push(`/newsfeed/${eventID}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitEvent = () => {
|
||||||
|
const event: any = {
|
||||||
|
content: value,
|
||||||
|
created_at: dateToUnix(),
|
||||||
|
kind: 1,
|
||||||
|
pubkey: activeAccount.id,
|
||||||
|
tags: [['e', eventID]],
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = signEvent(event, activeAccount.privkey);
|
||||||
|
|
||||||
|
pool.publish(event, relays);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||||
|
<Dialog.Trigger asChild>
|
||||||
|
<button className="group flex w-16 items-center gap-1 text-sm text-zinc-500">
|
||||||
|
<div className="rounded-md p-1 group-hover:bg-zinc-800">
|
||||||
|
<CommentIcon className="h-5 w-5 text-zinc-500" />
|
||||||
|
</div>
|
||||||
|
<span>{count}</span>
|
||||||
|
</button>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm data-[state=open]:animate-overlayShow" />
|
||||||
|
<Dialog.Content className="fixed inset-0 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-center justify-center">
|
||||||
|
<div className="relative w-full max-w-2xl rounded-lg bg-zinc-900 p-4 text-zinc-100 ring-1 ring-zinc-800">
|
||||||
|
{/* root note */}
|
||||||
|
<div className="relative z-10 flex flex-col pb-6">
|
||||||
|
<div className="relative z-10">
|
||||||
|
<UserExtend pubkey={eventPubkey} time={eventTime} />
|
||||||
|
</div>
|
||||||
|
<div className="-mt-5 pl-[52px]">
|
||||||
|
<div className="prose prose-zinc max-w-none break-words leading-tight dark:prose-invert prose-headings:mt-3 prose-headings:mb-2 prose-p:m-0 prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-ul:mt-2 prose-li:my-1">
|
||||||
|
{eventContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* divider */}
|
||||||
|
<div className="absolute top-0 left-[21px] h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
|
||||||
|
</div>
|
||||||
|
{/* comment form */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div>
|
||||||
|
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
||||||
|
<ImageWithFallback
|
||||||
|
src={profile.picture}
|
||||||
|
alt="user's avatar"
|
||||||
|
fill={true}
|
||||||
|
className="rounded-md object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative h-36 w-full flex-1 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
name="content"
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
placeholder="Send your comment"
|
||||||
|
className="relative h-36 w-full resize-none rounded-md border border-black/5 px-3.5 py-3 text-sm shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-2 w-full px-2">
|
||||||
|
<div className="flex w-full items-center justify-between bg-zinc-800">
|
||||||
|
<div className="flex items-center gap-2 divide-x divide-zinc-700">
|
||||||
|
<button
|
||||||
|
onClick={() => openThread()}
|
||||||
|
className="inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-zinc-700"
|
||||||
|
>
|
||||||
|
<SizeIcon className="h-4 w-4 text-zinc-400" />
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center gap-2 pl-2"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => submitEvent()}
|
||||||
|
disabled={value.length === 0 ? true : false}
|
||||||
|
className="inline-flex h-8 w-16 items-center justify-center rounded-md bg-fuchsia-500 px-4 text-sm font-medium shadow-md shadow-fuchsia-900/50 hover:bg-fuchsia-600"
|
||||||
|
>
|
||||||
|
<span className="text-white drop-shadow">Send</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
});
|
||||||
67
src/components/note/meta/reaction.tsx
Normal file
67
src/components/note/meta/reaction.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { activeAccountAtom } from '@stores/account';
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { dateToUnix } from '@utils/getDate';
|
||||||
|
|
||||||
|
import LikeIcon from '@assets/icons/like';
|
||||||
|
import LikedIcon from '@assets/icons/liked';
|
||||||
|
|
||||||
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
|
import { memo, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const NoteReaction = memo(function NoteReaction({
|
||||||
|
count,
|
||||||
|
eventID,
|
||||||
|
eventPubkey,
|
||||||
|
}: {
|
||||||
|
count: number;
|
||||||
|
eventID: string;
|
||||||
|
eventPubkey: string;
|
||||||
|
}) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
const [activeAccount] = useAtom(activeAccountAtom);
|
||||||
|
|
||||||
|
const [isReact, setIsReact] = useState(false);
|
||||||
|
const [like, setLike] = useState(0);
|
||||||
|
|
||||||
|
const handleLike = (e: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const event: any = {
|
||||||
|
content: '+',
|
||||||
|
kind: 7,
|
||||||
|
tags: [
|
||||||
|
['e', eventID],
|
||||||
|
['p', eventPubkey],
|
||||||
|
],
|
||||||
|
created_at: dateToUnix(),
|
||||||
|
pubkey: activeAccount.id,
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = signEvent(event, activeAccount.privkey);
|
||||||
|
// publish event to all relays
|
||||||
|
pool.publish(event, relays);
|
||||||
|
// update state to change icon to filled heart
|
||||||
|
setIsReact(true);
|
||||||
|
// update counter
|
||||||
|
setLike(like + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLike(count);
|
||||||
|
}, [count]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={(e) => handleLike(e)} className="group flex w-16 items-center gap-1 text-sm text-zinc-500">
|
||||||
|
<div className="rounded-md p-1 group-hover:bg-zinc-800">
|
||||||
|
{isReact ? <LikedIcon className="h-5 w-5 text-red-500" /> : <LikeIcon className="h-5 w-5 text-zinc-500" />}
|
||||||
|
</div>
|
||||||
|
<span>{like}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
81
src/components/note/metadata.tsx
Normal file
81
src/components/note/metadata.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { NoteComment } from '@components/note/meta/comment';
|
||||||
|
import { NoteReaction } from '@components/note/meta/reaction';
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { createCacheCommentNote } from '@utils/storage';
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function NoteMetadata({
|
||||||
|
eventID,
|
||||||
|
eventPubkey,
|
||||||
|
eventContent,
|
||||||
|
eventTime,
|
||||||
|
}: {
|
||||||
|
eventID: string;
|
||||||
|
eventPubkey: string;
|
||||||
|
eventTime: any;
|
||||||
|
eventContent: any;
|
||||||
|
}) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
|
||||||
|
const [likes, setLikes] = useState(0);
|
||||||
|
const [comments, setComments] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = pool.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'#e': [eventID],
|
||||||
|
since: parseInt(eventTime),
|
||||||
|
kinds: [1, 7],
|
||||||
|
limit: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relays,
|
||||||
|
(event: any) => {
|
||||||
|
switch (event.kind) {
|
||||||
|
case 1:
|
||||||
|
// update state
|
||||||
|
setComments((comments) => (comments += 1));
|
||||||
|
// save comment to database
|
||||||
|
createCacheCommentNote(event, eventID);
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
if (event.content === '🤙' || event.content === '+') {
|
||||||
|
setLikes((likes) => (likes += 1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
unsubscribeOnEose: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe;
|
||||||
|
};
|
||||||
|
}, [eventID, eventTime, pool, relays]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative z-10 -ml-1 flex items-center gap-8">
|
||||||
|
<NoteComment
|
||||||
|
count={comments}
|
||||||
|
eventID={eventID}
|
||||||
|
eventPubkey={eventPubkey}
|
||||||
|
eventContent={eventContent}
|
||||||
|
eventTime={eventTime}
|
||||||
|
/>
|
||||||
|
<NoteReaction count={likes} eventID={eventID} eventPubkey={eventPubkey} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
161
src/components/note/parent.tsx
Normal file
161
src/components/note/parent.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import NoteMetadata from '@components/note/metadata';
|
||||||
|
import { ImagePreview } from '@components/note/preview/image';
|
||||||
|
import { VideoPreview } from '@components/note/preview/video';
|
||||||
|
import { NoteRepost } from '@components/note/repost';
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
import { UserExtend } from '@components/user/extend';
|
||||||
|
import { UserMention } from '@components/user/mention';
|
||||||
|
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { createCacheNote, getNoteByID } from '@utils/storage';
|
||||||
|
|
||||||
|
import destr from 'destr';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import ReactPlayer from 'react-player';
|
||||||
|
import reactStringReplace from 'react-string-replace';
|
||||||
|
|
||||||
|
export const NoteParent = memo(function NoteParent({ id }: { id: string }) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
const [event, setEvent] = useState(null);
|
||||||
|
const unsubscribe = useRef(null);
|
||||||
|
|
||||||
|
const fetchEvent = useCallback(() => {
|
||||||
|
unsubscribe.current = pool.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ids: [id],
|
||||||
|
kinds: [1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relays,
|
||||||
|
(event: any) => {
|
||||||
|
// update state
|
||||||
|
setEvent(event);
|
||||||
|
// insert to database
|
||||||
|
createCacheNote(event);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
unsubscribeOnEose: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [id, pool, relays]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getNoteByID(id).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
setEvent(res);
|
||||||
|
} else {
|
||||||
|
fetchEvent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe.current;
|
||||||
|
};
|
||||||
|
}, [fetchEvent, id]);
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
let parsedContent = event ? event.content : null;
|
||||||
|
|
||||||
|
if (parsedContent !== null) {
|
||||||
|
// get data tags
|
||||||
|
const tags = destr(event.tags);
|
||||||
|
// handle urls
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => {
|
||||||
|
if (match.toLowerCase().match(/\.(jpg|jpeg|gif|png|webp)$/)) {
|
||||||
|
// image url
|
||||||
|
return <ImagePreview key={match + i} url={match} />;
|
||||||
|
} else if (ReactPlayer.canPlay(match)) {
|
||||||
|
return <VideoPreview key={match + i} url={match} />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<a key={match + i} href={match} target="_blank" rel="noreferrer">
|
||||||
|
{match}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// handle #-hashtags
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /#(\w+)/g, (match, i) => (
|
||||||
|
<span key={match + i} className="cursor-pointer text-fuchsia-500">
|
||||||
|
#{match}
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
// handle mentions
|
||||||
|
if (tags.length > 0) {
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => {
|
||||||
|
if (tags[match][0] === 'p') {
|
||||||
|
// @-mentions
|
||||||
|
return <UserMention key={match + i} pubkey={tags[match][1]} />;
|
||||||
|
} else if (tags[match][0] === 'e') {
|
||||||
|
// note-mentions
|
||||||
|
return <NoteRepost key={match + i} id={tags[match][1]} />;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedContent;
|
||||||
|
}, [event]);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
return (
|
||||||
|
<div className="relative pb-5">
|
||||||
|
<div className="absolute top-0 left-[21px] h-full w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600"></div>
|
||||||
|
<div className="relative z-10 flex flex-col">
|
||||||
|
<UserExtend pubkey={event.pubkey} time={event.created_at} />
|
||||||
|
<div className="-mt-5 pl-[52px]">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div onClick={(e) => e.stopPropagation()} className="mt-5 pl-[52px]">
|
||||||
|
<NoteMetadata
|
||||||
|
eventID={event.id}
|
||||||
|
eventPubkey={event.pubkey}
|
||||||
|
eventContent={event.content}
|
||||||
|
eventTime={event.created_at}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="relative z-10 flex h-min animate-pulse select-text flex-col pb-5">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
|
||||||
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<div className="h-4 w-16 rounded bg-zinc-700" />
|
||||||
|
<span className="text-zinc-500">·</span>
|
||||||
|
<div className="h-4 w-12 rounded bg-zinc-700" />
|
||||||
|
</div>
|
||||||
|
<div className="h-3 w-3 rounded-full bg-zinc-700" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="-mt-5 pl-[52px]">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="h-16 w-full rounded bg-zinc-700" />
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<div className="h-4 w-12 rounded bg-zinc-700" />
|
||||||
|
<div className="h-4 w-12 rounded bg-zinc-700" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -2,9 +2,9 @@ import { memo } from 'react';
|
|||||||
|
|
||||||
export const Placeholder = memo(function Placeholder() {
|
export const Placeholder = memo(function Placeholder() {
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-min animate-pulse select-text flex-col py-4 px-6">
|
<div className="relative z-10 flex h-min animate-pulse select-text flex-col py-5 px-3">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-2">
|
||||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full bg-zinc-700" />
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-700" />
|
||||||
<div className="flex w-full flex-1 items-start justify-between">
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
@@ -16,7 +16,7 @@ export const Placeholder = memo(function Placeholder() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="-mt-4 pl-[60px]">
|
<div className="-mt-5 pl-[52px]">
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="h-16 w-full rounded bg-zinc-700" />
|
<div className="h-16 w-full rounded bg-zinc-700" />
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
|
|||||||
20
src/components/note/preview/image.tsx
Normal file
20
src/components/note/preview/image.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import Image from 'next/image';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const ImagePreview = memo(function ImagePreview({ url }: { url: string }) {
|
||||||
|
return (
|
||||||
|
<div className="relative mt-3 mb-2 h-full w-full rounded-lg xl:w-2/3">
|
||||||
|
<Image
|
||||||
|
src={url}
|
||||||
|
alt={url}
|
||||||
|
width="0"
|
||||||
|
height="0"
|
||||||
|
sizes="100vw"
|
||||||
|
className="h-auto w-full rounded-lg border border-zinc-800 object-cover"
|
||||||
|
placeholder="blur"
|
||||||
|
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkqAcAAIUAgUW0RjgAAAAASUVORK5CYII="
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
export default function LinkCard({ data }: { data: any }) {
|
||||||
export default function LinkCard({ data }: { data: object }) {
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={data['url']}
|
href={data['url']}
|
||||||
target={'_blank'}
|
target={'_blank'}
|
||||||
className="relative mt-2 flex flex-col overflow-hidden rounded-xl border border-zinc-700"
|
className="relative mt-2 flex flex-col overflow-hidden rounded-lg border border-zinc-700"
|
||||||
>
|
>
|
||||||
<div className="relative aspect-video h-auto w-full">
|
<div className="relative aspect-video h-auto w-full">
|
||||||
<Image src={data['image']} alt="image preview" fill={true} className="object-cover" />
|
<Image src={data['image']} alt="image preview" fill={true} className="object-cover" />
|
||||||
17
src/components/note/preview/video.tsx
Normal file
17
src/components/note/preview/video.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import ReactPlayer from 'react-player/lazy';
|
||||||
|
|
||||||
|
export const VideoPreview = memo(function VideoPreview({ url }: { url: string }) {
|
||||||
|
return (
|
||||||
|
<div onClick={(e) => e.stopPropagation()} className="relative mt-3 flex flex-col overflow-hidden rounded-lg">
|
||||||
|
<ReactPlayer
|
||||||
|
url={url}
|
||||||
|
controls={true}
|
||||||
|
volume={0}
|
||||||
|
className="aspect-video w-full xl:w-2/3"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,52 +1,110 @@
|
|||||||
import { RelayContext } from '@components/contexts/relay';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
import { UserRepost } from '@components/note/atoms/userRepost';
|
import { UserExtend } from '@components/user/extend';
|
||||||
import { Content } from '@components/note/content';
|
import { UserMention } from '@components/user/mention';
|
||||||
import { Placeholder } from '@components/note/placeholder';
|
|
||||||
|
|
||||||
import { LoopIcon } from '@radix-ui/react-icons';
|
import { relaysAtom } from '@stores/relays';
|
||||||
import useLocalStorage from '@rehooks/local-storage';
|
|
||||||
import { memo, useContext, useState } from 'react';
|
|
||||||
|
|
||||||
export const Repost = memo(function Repost({ root, user }: { root: any; user: string }) {
|
import { createCacheNote, getNoteByID } from '@utils/storage';
|
||||||
const relayPool: any = useContext(RelayContext);
|
|
||||||
const [relays]: any = useLocalStorage('relays');
|
|
||||||
const [events, setEvents] = useState([]);
|
|
||||||
|
|
||||||
relayPool.subscribe(
|
import destr from 'destr';
|
||||||
[
|
import { useAtomValue } from 'jotai';
|
||||||
{
|
import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
ids: [root[0][1]],
|
import reactStringReplace from 'react-string-replace';
|
||||||
since: 0,
|
|
||||||
kinds: [1],
|
export const NoteRepost = memo(function NoteRepost({ id }: { id: string }) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
const [event, setEvent] = useState(null);
|
||||||
|
const unsubscribe = useRef(null);
|
||||||
|
|
||||||
|
const fetchEvent = useCallback(() => {
|
||||||
|
unsubscribe.current = pool.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ids: [id],
|
||||||
|
kinds: [1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relays,
|
||||||
|
(event: any) => {
|
||||||
|
// update state
|
||||||
|
setEvent(event);
|
||||||
|
// insert to database
|
||||||
|
createCacheNote(event);
|
||||||
},
|
},
|
||||||
],
|
undefined,
|
||||||
relays,
|
undefined,
|
||||||
(event: any) => {
|
{
|
||||||
setEvents((events) => [event, ...events]);
|
unsubscribeOnEose: true,
|
||||||
},
|
}
|
||||||
undefined,
|
);
|
||||||
(events: any, relayURL: any) => {
|
}, [id, pool, relays]);
|
||||||
console.log(events, relayURL);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (events !== null && Object.keys(events).length > 0) {
|
useEffect(() => {
|
||||||
|
getNoteByID(id).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
setEvent(res);
|
||||||
|
} else {
|
||||||
|
fetchEvent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe.current;
|
||||||
|
};
|
||||||
|
}, [fetchEvent, id]);
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
let parsedContent = event ? event.content : null;
|
||||||
|
|
||||||
|
if (parsedContent !== null) {
|
||||||
|
// get data tags
|
||||||
|
const tags = destr(event.tags);
|
||||||
|
// handle urls
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /(https?:\/\/\S+)/g, (match, i) => (
|
||||||
|
<a key={match + i} href={match} target="_blank" rel="noreferrer">
|
||||||
|
{match}
|
||||||
|
</a>
|
||||||
|
));
|
||||||
|
// handle #-hashtags
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /#(\w+)/g, (match, i) => (
|
||||||
|
<span key={match + i} className="cursor-pointer text-fuchsia-500">
|
||||||
|
#{match}
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
// handle mentions
|
||||||
|
if (tags.length > 0) {
|
||||||
|
parsedContent = reactStringReplace(parsedContent, /\#\[(\d+)\]/gm, (match, i) => {
|
||||||
|
if (tags[match][0] === 'p') {
|
||||||
|
// @-mentions
|
||||||
|
return <UserMention key={match + i} pubkey={tags[match][1]} />;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedContent;
|
||||||
|
}, [event]);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 py-6 px-6">
|
<div className="relative mt-3 mb-2 rounded-lg border border-zinc-700 bg-zinc-800 p-2 py-3">
|
||||||
<div className="flex items-center gap-1 pl-8 text-sm">
|
<div className="relative z-10 flex flex-col">
|
||||||
<LoopIcon className="h-4 w-4 text-zinc-400" />
|
<UserExtend pubkey={event.pubkey} time={event.created_at} />
|
||||||
<div className="ml-2">
|
<div className="-mt-5 pl-[52px]">
|
||||||
<UserRepost pubkey={user} />
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="prose prose-zinc max-w-none break-words text-[15px] leading-tight dark:prose-invert prose-p:m-0 prose-p:text-[15px] prose-p:leading-tight prose-a:font-normal prose-a:text-fuchsia-500 prose-a:no-underline prose-img:m-0 prose-video:m-0">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{events[0].content && <Content data={events[0]} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return <div className="mt-2 h-6 animate-pulse select-text flex-col rounded bg-zinc-700 pb-5"></div>;
|
||||||
<div className="border-b border-zinc-800">
|
|
||||||
<Placeholder />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Content } from '@components/note/content';
|
|
||||||
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
export const Single = memo(function Single({ event }: { event: any }) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-min min-h-min w-full cursor-pointer select-text flex-col border-b border-zinc-800 py-4 px-6 hover:bg-zinc-800">
|
|
||||||
<Content data={event} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
27
src/components/profile/followers.tsx
Normal file
27
src/components/profile/followers.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
import { UserFollow } from '@components/user/follow';
|
||||||
|
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import destr from 'destr';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { Author } from 'nostr-relaypool';
|
||||||
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function ProfileFollowers({ id }: { id: string }) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
const relays: any = useAtomValue(relaysAtom);
|
||||||
|
|
||||||
|
const [followers, setFollowers] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const user = new Author(pool, relays, id);
|
||||||
|
user.followers((res) => setFollowers(destr(res.tags)), 0, 100);
|
||||||
|
}, [id, pool, relays]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3 px-3 py-5">
|
||||||
|
{followers && followers.map((follower) => <UserFollow key={follower[1]} pubkey={follower[1]} />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/components/profile/follows.tsx
Normal file
26
src/components/profile/follows.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
import { UserFollow } from '@components/user/follow';
|
||||||
|
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { Author } from 'nostr-relaypool';
|
||||||
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function ProfileFollows({ id }: { id: string }) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
const relays: any = useAtomValue(relaysAtom);
|
||||||
|
|
||||||
|
const [follows, setFollows] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const user = new Author(pool, relays, id);
|
||||||
|
user.follows((res) => setFollows(res), 0);
|
||||||
|
}, [id, pool, relays]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3 px-3 py-5">
|
||||||
|
{follows && follows.map((follow) => <UserFollow key={follow.pubkey} pubkey={follow.pubkey} />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
src/components/profile/metadata.tsx
Normal file
70
src/components/profile/metadata.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { truncate } from '@utils/truncate';
|
||||||
|
|
||||||
|
import Avatar from 'boring-avatars';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { Author } from 'nostr-relaypool';
|
||||||
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const DEFAULT_BANNER = 'https://bafybeiacwit7hjmdefqggxqtgh6ht5dhth7ndptwn2msl5kpkodudsr7py.ipfs.w3s.link/banner-1.jpg';
|
||||||
|
|
||||||
|
export default function ProfileMetadata({ id }: { id: string }) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
const relays: any = useAtomValue(relaysAtom);
|
||||||
|
|
||||||
|
const [profile, setProfile] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const user = new Author(pool, relays, id);
|
||||||
|
user.metaData((res) => setProfile(destr(res.content)), 0);
|
||||||
|
}, [id, pool, relays]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="relative h-56 w-full rounded-t-lg bg-zinc-800">
|
||||||
|
<Image
|
||||||
|
src={profile?.banner || DEFAULT_BANNER}
|
||||||
|
alt="user's banner"
|
||||||
|
fill={true}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative -top-8 z-10 px-4">
|
||||||
|
<div className="relative h-16 w-16 rounded-lg bg-zinc-900 ring-2 ring-zinc-900">
|
||||||
|
{profile?.picture ? (
|
||||||
|
<ImageWithFallback src={profile.picture} alt={id} fill={true} className="rounded-lg object-cover" />
|
||||||
|
) : (
|
||||||
|
<Avatar
|
||||||
|
size={64}
|
||||||
|
name={id}
|
||||||
|
variant="beam"
|
||||||
|
square={true}
|
||||||
|
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="-mt-4 mb-8 px-4">
|
||||||
|
<div>
|
||||||
|
<div className="mb-3 flex flex-col">
|
||||||
|
<h3 className="text-lg font-semibold leading-tight text-zinc-100">
|
||||||
|
{profile?.display_name || profile?.name}
|
||||||
|
</h3>
|
||||||
|
<span className="text-sm leading-tight text-zinc-500">
|
||||||
|
{profile?.username || (id && truncate(id, 16, ' .... '))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="prose-sm prose-zinc leading-tight dark:prose-invert">{profile?.about}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/components/profile/notes.tsx
Normal file
30
src/components/profile/notes.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { NoteBase } from '@components/note/base';
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { Author } from 'nostr-relaypool';
|
||||||
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function ProfileNotes({ id }: { id: string }) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
const relays: any = useAtomValue(relaysAtom);
|
||||||
|
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const user = new Author(pool, relays, id);
|
||||||
|
user.text((res) => setData((data) => [...data, res]), 100, 0);
|
||||||
|
}, [id, pool, relays]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{data.map((item) => (
|
||||||
|
<div key={item.id}>
|
||||||
|
<NoteBase event={item} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,8 +3,7 @@ import { createContext, useMemo } from 'react';
|
|||||||
|
|
||||||
export const RelayContext = createContext({});
|
export const RelayContext = createContext({});
|
||||||
|
|
||||||
export default function RelayProvider({ relays, children }: { relays: any; children: React.ReactNode }) {
|
export default function RelayProvider({ relays, children }: { relays: Array<string>; children: React.ReactNode }) {
|
||||||
const value = useMemo(() => new RelayPool(relays, { useEventCache: true }), [relays]);
|
const value = useMemo(() => new RelayPool(relays, { useEventCache: false, logSubscriptions: false }), [relays]);
|
||||||
|
|
||||||
return <RelayContext.Provider value={value}>{children}</RelayContext.Provider>;
|
return <RelayContext.Provider value={value}>{children}</RelayContext.Provider>;
|
||||||
}
|
}
|
||||||
45
src/components/user/base.tsx
Normal file
45
src/components/user/base.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
|
|
||||||
|
import { createCacheProfile } from '@utils/storage';
|
||||||
|
import { truncate } from '@utils/truncate';
|
||||||
|
|
||||||
|
import { fetch } from '@tauri-apps/api/http';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
|
||||||
|
const [profile, setProfile] = useState(null);
|
||||||
|
|
||||||
|
const fetchProfile = useCallback(async (id: string) => {
|
||||||
|
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 30,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProfile(pubkey)
|
||||||
|
.then((res: any) => {
|
||||||
|
setProfile(destr(res.content));
|
||||||
|
createCacheProfile(res.pubkey, res.content);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, [fetchProfile, pubkey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
||||||
|
{profile?.picture && (
|
||||||
|
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||||
|
<span className="truncate font-medium leading-tight text-zinc-200">
|
||||||
|
{profile?.display_name || profile?.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm leading-tight text-zinc-400">{truncate(pubkey, 16, ' .... ')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
90
src/components/user/extend.tsx
Normal file
90
src/components/user/extend.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
|
|
||||||
|
import { createCacheProfile, getCacheProfile } from '@utils/storage';
|
||||||
|
import { truncate } from '@utils/truncate';
|
||||||
|
|
||||||
|
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
|
||||||
|
import { fetch } from '@tauri-apps/api/http';
|
||||||
|
import Avatar from 'boring-avatars';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
export const UserExtend = memo(function UserExtend({ pubkey, time }: { pubkey: string; time: any }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [profile, setProfile] = useState(null);
|
||||||
|
|
||||||
|
const openUserPage = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
router.push(`/users/${pubkey}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchProfile = useCallback(async (id: string) => {
|
||||||
|
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 30,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCacheProfile(pubkey).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
setProfile(destr(res.metadata));
|
||||||
|
} else {
|
||||||
|
fetchProfile(pubkey)
|
||||||
|
.then((res: any) => {
|
||||||
|
setProfile(destr(res.content));
|
||||||
|
createCacheProfile(pubkey, res.content);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [fetchProfile, pubkey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group flex items-start gap-2">
|
||||||
|
<div
|
||||||
|
onClick={(e) => openUserPage(e)}
|
||||||
|
className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-900 ring-fuchsia-500 ring-offset-1 ring-offset-zinc-900 group-hover:ring-1"
|
||||||
|
>
|
||||||
|
{profile?.picture ? (
|
||||||
|
<ImageWithFallback
|
||||||
|
src={profile.picture}
|
||||||
|
alt={pubkey}
|
||||||
|
fill={true}
|
||||||
|
className="rounded-md border border-white/10 object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Avatar
|
||||||
|
size={44}
|
||||||
|
name={pubkey}
|
||||||
|
variant="beam"
|
||||||
|
square={true}
|
||||||
|
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-1 items-start justify-between">
|
||||||
|
<div className="flex w-full justify-between">
|
||||||
|
<div className="flex items-baseline gap-2 text-sm">
|
||||||
|
<span onClick={(e) => openUserPage(e)} className="font-bold leading-tight group-hover:underline">
|
||||||
|
{profile?.display_name || profile?.name || truncate(pubkey, 16, ' .... ')}
|
||||||
|
</span>
|
||||||
|
<span className="leading-tight text-zinc-500">·</span>
|
||||||
|
<span className="text-zinc-500">{dayjs().to(dayjs.unix(time))}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800">
|
||||||
|
<DotsHorizontalIcon className="h-3 w-3 text-zinc-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
45
src/components/user/follow.tsx
Normal file
45
src/components/user/follow.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
|
|
||||||
|
import { createCacheProfile } from '@utils/storage';
|
||||||
|
import { truncate } from '@utils/truncate';
|
||||||
|
|
||||||
|
import { fetch } from '@tauri-apps/api/http';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const UserFollow = memo(function UserFollow({ pubkey }: { pubkey: string }) {
|
||||||
|
const [profile, setProfile] = useState(null);
|
||||||
|
|
||||||
|
const fetchProfile = useCallback(async (id: string) => {
|
||||||
|
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 30,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProfile(pubkey)
|
||||||
|
.then((res: any) => {
|
||||||
|
setProfile(destr(res.content));
|
||||||
|
createCacheProfile(res.pubkey, res.content);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, [fetchProfile, pubkey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
||||||
|
{profile?.picture && (
|
||||||
|
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||||
|
<span className="truncate font-medium leading-tight text-zinc-200">
|
||||||
|
{profile?.display_name || profile?.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm leading-tight text-zinc-400">{truncate(pubkey, 16, ' .... ')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
81
src/components/user/large.tsx
Normal file
81
src/components/user/large.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
|
|
||||||
|
import { createCacheProfile, getCacheProfile } from '@utils/storage';
|
||||||
|
import { truncate } from '@utils/truncate';
|
||||||
|
|
||||||
|
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
|
||||||
|
import { fetch } from '@tauri-apps/api/http';
|
||||||
|
import Avatar from 'boring-avatars';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
export const UserLarge = memo(function UserLarge({ pubkey, time }: { pubkey: string; time: any }) {
|
||||||
|
const [profile, setProfile] = useState(null);
|
||||||
|
|
||||||
|
const fetchProfile = useCallback(async (id: string) => {
|
||||||
|
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 30,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCacheProfile(pubkey).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
setProfile(destr(res.metadata));
|
||||||
|
} else {
|
||||||
|
fetchProfile(pubkey)
|
||||||
|
.then((res: any) => {
|
||||||
|
setProfile(destr(res.content));
|
||||||
|
createCacheProfile(pubkey, res.content);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [fetchProfile, pubkey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-zinc-900">
|
||||||
|
{profile?.picture ? (
|
||||||
|
<ImageWithFallback
|
||||||
|
src={profile.picture}
|
||||||
|
alt={pubkey}
|
||||||
|
fill={true}
|
||||||
|
className="rounded-md border border-white/10 object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Avatar
|
||||||
|
size={44}
|
||||||
|
name={pubkey}
|
||||||
|
variant="beam"
|
||||||
|
square={true}
|
||||||
|
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex-1">
|
||||||
|
<div className="flex w-full justify-between">
|
||||||
|
<div className="flex flex-col gap-1 text-sm">
|
||||||
|
<span className="font-bold leading-tight text-zinc-100">
|
||||||
|
{profile?.display_name || profile?.name || truncate(pubkey, 16, ' .... ')}
|
||||||
|
</span>
|
||||||
|
<span className="leading-tight text-zinc-400">
|
||||||
|
{profile?.username || truncate(pubkey, 16, ' .... ')} · {dayjs().to(dayjs.unix(time))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800">
|
||||||
|
<DotsHorizontalIcon className="h-3 w-3 text-zinc-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
35
src/components/user/mention.tsx
Normal file
35
src/components/user/mention.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { createCacheProfile, getCacheProfile } from '@utils/storage';
|
||||||
|
import { truncate } from '@utils/truncate';
|
||||||
|
|
||||||
|
import { fetch } from '@tauri-apps/api/http';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const UserMention = memo(function UserMention({ pubkey }: { pubkey: string }) {
|
||||||
|
const [profile, setProfile] = useState(null);
|
||||||
|
|
||||||
|
const fetchProfile = useCallback(async (id: string) => {
|
||||||
|
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 30,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCacheProfile(pubkey).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
setProfile(destr(res.metadata));
|
||||||
|
} else {
|
||||||
|
fetchProfile(pubkey)
|
||||||
|
.then((res: any) => {
|
||||||
|
setProfile(destr(res.content));
|
||||||
|
createCacheProfile(pubkey, res.content);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [fetchProfile, pubkey]);
|
||||||
|
|
||||||
|
return <span className="cursor-pointer text-fuchsia-500">@{profile?.name || truncate(pubkey, 16, ' .... ')}</span>;
|
||||||
|
});
|
||||||
59
src/components/user/mini.tsx
Normal file
59
src/components/user/mini.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||||
|
|
||||||
|
import { createCacheProfile, getCacheProfile } from '@utils/storage';
|
||||||
|
import { truncate } from '@utils/truncate';
|
||||||
|
|
||||||
|
import { fetch } from '@tauri-apps/api/http';
|
||||||
|
import Avatar from 'boring-avatars';
|
||||||
|
import destr from 'destr';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const UserMini = memo(function UserMini({ pubkey }: { pubkey: string }) {
|
||||||
|
const [profile, setProfile] = useState(null);
|
||||||
|
|
||||||
|
const fetchProfile = useCallback(async (id: string) => {
|
||||||
|
const res = await fetch(`https://rbr.bio/${id}/metadata.json`, {
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 30,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCacheProfile(pubkey).then((res) => {
|
||||||
|
if (res) {
|
||||||
|
setProfile(destr(res.metadata));
|
||||||
|
} else {
|
||||||
|
fetchProfile(pubkey)
|
||||||
|
.then((res: any) => {
|
||||||
|
setProfile(destr(res.content));
|
||||||
|
createCacheProfile(pubkey, res.content);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [fetchProfile, pubkey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm font-medium hover:bg-zinc-900">
|
||||||
|
<div className="relative h-5 w-5 shrink-0 overflow-hidden rounded">
|
||||||
|
{profile?.picture ? (
|
||||||
|
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded object-cover" />
|
||||||
|
) : (
|
||||||
|
<Avatar
|
||||||
|
size={20}
|
||||||
|
name={pubkey}
|
||||||
|
variant="beam"
|
||||||
|
square={true}
|
||||||
|
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex w-full flex-1 flex-col overflow-hidden">
|
||||||
|
<p className="truncate leading-tight text-zinc-300">
|
||||||
|
{profile?.display_name || profile?.name || truncate(pubkey, 16, ' .... ')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export default function BaseLayout({ children }: { children: React.ReactNode }) {
|
export default function BaseLayout({ children }: { children: React.ReactNode }) {
|
||||||
return <div className="h-screen w-screen bg-white text-zinc-900 dark:bg-near-black dark:text-white">{children}</div>;
|
return <div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">{children}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
export default function FullscreenLayout({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="bg-gradient-radial-page relative h-full overflow-hidden">
|
|
||||||
{/* dragging area */}
|
|
||||||
<div data-tauri-drag-region className="absolute top-0 left-0 z-20 h-16 w-full bg-transparent" />
|
|
||||||
{/* end dragging area */}
|
|
||||||
{/* content */}
|
|
||||||
<div className="relative z-10 h-full">{children}</div>
|
|
||||||
{/* end content */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import AccountColumn from '@components/columns/account';
|
|
||||||
import NavigatorColumn from '@components/columns/navigator';
|
|
||||||
|
|
||||||
export default function NewsFeedLayout({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full flex-row">
|
|
||||||
<div className="relative h-full w-[70px] shrink-0 border-r border-zinc-900">
|
|
||||||
<div data-tauri-drag-region className="absolute top-0 left-0 h-12 w-full" />
|
|
||||||
<AccountColumn />
|
|
||||||
</div>
|
|
||||||
<div className="grid grow grid-cols-4">
|
|
||||||
<div className="col-span-1">
|
|
||||||
<NavigatorColumn />
|
|
||||||
</div>
|
|
||||||
<div className="col-span-3 m-3 ml-0 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20">
|
|
||||||
<div className="h-full w-full rounded-lg">{children}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
export default function OnboardingLayout({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full flex-row">
|
|
||||||
<div className="relative h-full w-[70px] shrink-0 border-r border-zinc-900">
|
|
||||||
<div data-tauri-drag-region className="absolute top-0 left-0 h-12 w-full" />
|
|
||||||
</div>
|
|
||||||
<div className="grid grow grid-cols-4">
|
|
||||||
<div className="col-span-1"></div>
|
|
||||||
<div className="col-span-3 m-3 ml-0 overflow-hidden rounded-lg bg-zinc-900 shadow-md ring-1 ring-inset dark:shadow-black/10 dark:ring-white/10">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
38
src/layouts/withSidebar.tsx
Normal file
38
src/layouts/withSidebar.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import AppHeader from '@components/appHeader';
|
||||||
|
import AccountColumn from '@components/columns/account';
|
||||||
|
import NavigatorColumn from '@components/columns/navigator';
|
||||||
|
|
||||||
|
export default function WithSidebarLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-full flex-col">
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="relative h-11 shrink-0 border-b border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
|
||||||
|
>
|
||||||
|
<AppHeader />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex min-h-0 w-full flex-1">
|
||||||
|
<div className="relative w-[68px] shrink-0 border-r border-zinc-900">
|
||||||
|
<div className="absolute top-0 left-0 h-12 w-full" />
|
||||||
|
<AccountColumn />
|
||||||
|
</div>
|
||||||
|
<div className="grid w-full grid-cols-4 xl:grid-cols-5">
|
||||||
|
<div className="scrollbar-hide col-span-1 overflow-y-auto overflow-x-hidden border-r border-zinc-900">
|
||||||
|
<NavigatorColumn />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 m-3 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20 xl:col-span-2 xl:mr-1.5">
|
||||||
|
<div className="h-full w-full rounded-lg">{children}</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 m-3 hidden overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20 xl:col-span-2 xl:ml-1.5 xl:flex">
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<p className="select-text p-8 text-center text-zinc-400">
|
||||||
|
This feature hasn't implemented yet, so resize Lume to the initial size for a better experience.
|
||||||
|
I'm sorry for this inconvenience, and I swear I will add it soon 😁
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import DatabaseProvider from '@components/contexts/database';
|
import RelayProvider from '@components/relaysProvider';
|
||||||
import RelayProvider from '@components/contexts/relay';
|
|
||||||
|
|
||||||
import { useLocalStorage } from '@rehooks/local-storage';
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { Provider, useAtomValue } from 'jotai';
|
||||||
|
import { queryClientAtom } from 'jotai-tanstack-query';
|
||||||
|
import { useHydrateAtoms } from 'jotai/react/utils';
|
||||||
import type { NextPage } from 'next';
|
import type { NextPage } from 'next';
|
||||||
import type { AppProps } from 'next/app';
|
import type { AppProps } from 'next/app';
|
||||||
import { ReactElement, ReactNode } from 'react';
|
import { ReactElement, ReactNode } from 'react';
|
||||||
@@ -17,15 +21,25 @@ type AppPropsWithLayout = AppProps & {
|
|||||||
Component: NextPageWithLayout;
|
Component: NextPageWithLayout;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
const HydrateAtoms = ({ children }) => {
|
||||||
|
useHydrateAtoms([[queryClientAtom, queryClient]]);
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||||
// Use the layout defined at the page level, if available
|
// Use the layout defined at the page level, if available
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
// Get relays from localstorage
|
const relays = useAtomValue(relaysAtom);
|
||||||
const [relays] = useLocalStorage('relays');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DatabaseProvider>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RelayProvider relays={relays}>{getLayout(<Component {...pageProps} />)}</RelayProvider>
|
<Provider>
|
||||||
</DatabaseProvider>
|
<HydrateAtoms>
|
||||||
|
<RelayProvider relays={relays}>{getLayout(<Component {...pageProps} />)}</RelayProvider>
|
||||||
|
</HydrateAtoms>
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
import BaseLayout from '@layouts/base';
|
||||||
import NewsFeedLayout from '@layouts/newsfeed';
|
import WithSidebarLayout from '@layouts/withSidebar';
|
||||||
|
|
||||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return <></>;
|
||||||
<div className="h-full w-full">
|
|
||||||
<p>Global</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Page.getLayout = function getLayout(
|
Page.getLayout = function getLayout(
|
||||||
@@ -22,7 +18,7 @@ Page.getLayout = function getLayout(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<NewsFeedLayout>{page}</NewsFeedLayout>
|
<WithSidebarLayout>{page}</WithSidebarLayout>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,102 +1,58 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
import BaseLayout from '@layouts/base';
|
||||||
import FullscreenLayout from '@layouts/fullscreen';
|
|
||||||
|
import { getAccounts } from '@utils/storage';
|
||||||
|
|
||||||
import LumeSymbol from '@assets/icons/Lume';
|
import LumeSymbol from '@assets/icons/Lume';
|
||||||
import { useLocalStorage } from '@rehooks/local-storage';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useEffect, useRef, useState } from 'react';
|
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useEffect } from 'react';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [currentUser]: any = useLocalStorage('current-user');
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const timer = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser) {
|
getAccounts()
|
||||||
timer.current = setTimeout(() => {
|
.then((res: any) => {
|
||||||
setLoading(false);
|
if (res.length > 0) {
|
||||||
router.push('/newsfeed/following');
|
router.push('/init');
|
||||||
}, 1000);
|
} else {
|
||||||
} else {
|
router.push('/onboarding');
|
||||||
timer.current = setTimeout(() => {
|
}
|
||||||
setLoading(false);
|
})
|
||||||
router.push('/onboarding');
|
.catch(console.error);
|
||||||
}, 1000);
|
}, [router]);
|
||||||
}
|
|
||||||
|
|
||||||
// clean up
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timer.current);
|
|
||||||
};
|
|
||||||
}, [currentUser, router]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full flex-col items-center justify-between">
|
<div className="relative h-full overflow-hidden">
|
||||||
<div>{/* spacer */}</div>
|
{/* dragging area */}
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div data-tauri-drag-region className="absolute top-0 left-0 z-20 h-16 w-full bg-transparent" />
|
||||||
<motion.div layoutId="logo" className="relative">
|
{/* end dragging area */}
|
||||||
<LumeSymbol className="h-16 w-16 text-white" />
|
<div className="relative flex h-full flex-col items-center justify-center">
|
||||||
</motion.div>
|
<div className="flex flex-col items-center gap-2">
|
||||||
<div className="flex flex-col items-center gap-0.5">
|
<LumeSymbol className="h-16 w-16 text-black dark:text-white" />
|
||||||
<motion.h2
|
<div className="text-center">
|
||||||
layoutId="subtitle"
|
<h3 className="text-lg font-semibold leading-tight text-zinc-900 dark:text-zinc-100">Did you know?</h3>
|
||||||
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-4xl font-medium text-transparent"
|
<p className="font-medium text-zinc-300 dark:text-zinc-600">
|
||||||
>
|
No one can't stop you use bitcoin and nostr
|
||||||
A censorship-resistant social network
|
</p>
|
||||||
</motion.h2>
|
</div>
|
||||||
<motion.h1
|
|
||||||
layoutId="title"
|
|
||||||
className="bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 bg-clip-text text-5xl font-bold text-transparent"
|
|
||||||
>
|
|
||||||
built on nostr
|
|
||||||
</motion.h1>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="absolute bottom-16 left-1/2 -translate-x-1/2 transform">
|
||||||
<div className="flex items-center gap-2 pb-16">
|
<svg
|
||||||
<div className="h-10">
|
className="h-5 w-5 animate-spin text-black dark:text-white"
|
||||||
{loading ? (
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg
|
fill="none"
|
||||||
className="h-5 w-5 animate-spin text-white"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
>
|
||||||
fill="none"
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
viewBox="0 0 24 24"
|
<path
|
||||||
>
|
className="opacity-75"
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
fill="currentColor"
|
||||||
<path
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
className="opacity-75"
|
></path>
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* background */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-fuchsia-400/10 to-orange-100/10 opacity-100 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)]">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
className="dark:fill-white/2.5 absolute inset-x-0 inset-y-[-50%] h-[200%] w-full skew-y-[-18deg] fill-black/40 stroke-black/50 mix-blend-overlay dark:stroke-white/5"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<pattern id=":R11d6:" width="72" height="56" patternUnits="userSpaceOnUse" x="-12" y="4">
|
|
||||||
<path d="M.5 56V.5H72" fill="none"></path>
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect width="100%" height="100%" strokeWidth="0" fill="url(#:R11d6:)"></rect>
|
|
||||||
<svg x="-12" y="4" className="overflow-visible">
|
|
||||||
<rect strokeWidth="0" width="73" height="57" x="288" y="168"></rect>
|
|
||||||
<rect strokeWidth="0" width="73" height="57" x="144" y="56"></rect>
|
|
||||||
<rect strokeWidth="0" width="73" height="57" x="504" y="168"></rect>
|
|
||||||
<rect strokeWidth="0" width="73" height="57" x="720" y="336"></rect>
|
|
||||||
</svg>
|
</svg>
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* end background */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -110,9 +66,5 @@ Page.getLayout = function getLayout(
|
|||||||
| ReactFragment
|
| ReactFragment
|
||||||
| ReactPortal
|
| ReactPortal
|
||||||
) {
|
) {
|
||||||
return (
|
return <BaseLayout>{page}</BaseLayout>;
|
||||||
<BaseLayout>
|
|
||||||
<FullscreenLayout>{page}</FullscreenLayout>
|
|
||||||
</BaseLayout>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
135
src/pages/init.tsx
Normal file
135
src/pages/init.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import BaseLayout from '@layouts/base';
|
||||||
|
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { activeAccountAtom } from '@stores/account';
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { dateToUnix, hoursAgo } from '@utils/getDate';
|
||||||
|
import { countTotalNotes, createCacheNote, getAllFollowsByID, getLastLoginTime } from '@utils/storage';
|
||||||
|
import { pubkeyArray } from '@utils/transform';
|
||||||
|
|
||||||
|
import LumeSymbol from '@assets/icons/Lume';
|
||||||
|
|
||||||
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import {
|
||||||
|
JSXElementConstructor,
|
||||||
|
ReactElement,
|
||||||
|
ReactFragment,
|
||||||
|
ReactPortal,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const router = useRouter();
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
const [activeAccount] = useAtom(activeAccountAtom);
|
||||||
|
|
||||||
|
const [done, setDone] = useState(false);
|
||||||
|
const now = useRef(new Date());
|
||||||
|
const unsubscribe = useRef(null);
|
||||||
|
|
||||||
|
const fetchData = useCallback(
|
||||||
|
(since) => {
|
||||||
|
getAllFollowsByID(activeAccount.id).then((follows) => {
|
||||||
|
unsubscribe.current = pool.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
kinds: [1],
|
||||||
|
authors: pubkeyArray(follows),
|
||||||
|
since: dateToUnix(since),
|
||||||
|
until: dateToUnix(now.current),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relays,
|
||||||
|
(event) => {
|
||||||
|
// insert event to local database
|
||||||
|
createCacheNote(event);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
() => {
|
||||||
|
setDone(true);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unsubscribeOnEose: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[activeAccount.id, pool, relays]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!done) {
|
||||||
|
countTotalNotes().then((count) => {
|
||||||
|
if (count.total === 0) {
|
||||||
|
fetchData(hoursAgo(24, now.current));
|
||||||
|
} else {
|
||||||
|
getLastLoginTime().then((time) => {
|
||||||
|
const parseDate = new Date(time.setting_value);
|
||||||
|
fetchData(parseDate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
router.push('/newsfeed/following');
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe.current;
|
||||||
|
};
|
||||||
|
}, [activeAccount.id, done, pool, relays, router, fetchData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full overflow-hidden">
|
||||||
|
{/* dragging area */}
|
||||||
|
<div data-tauri-drag-region className="absolute top-0 left-0 z-20 h-16 w-full bg-transparent" />
|
||||||
|
{/* end dragging area */}
|
||||||
|
<div className="relative flex h-full flex-col items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<LumeSymbol className="h-16 w-16 text-black dark:text-white" />
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-lg font-semibold leading-tight text-zinc-900 dark:text-zinc-100">Loading...</h3>
|
||||||
|
<p className="font-medium text-zinc-300 dark:text-zinc-600">
|
||||||
|
Keep calm and waiting, Lume is fetching event...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-16 left-1/2 -translate-x-1/2 transform">
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 animate-spin text-black dark:text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page.getLayout = function getLayout(
|
||||||
|
page:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
||||||
|
| ReactFragment
|
||||||
|
| ReactPortal
|
||||||
|
) {
|
||||||
|
return <BaseLayout>{page}</BaseLayout>;
|
||||||
|
};
|
||||||
74
src/pages/newsfeed/[id].tsx
Normal file
74
src/pages/newsfeed/[id].tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import BaseLayout from '@layouts/base';
|
||||||
|
import WithSidebarLayout from '@layouts/withSidebar';
|
||||||
|
|
||||||
|
import FormComment from '@components/form/comment';
|
||||||
|
import { NoteComment } from '@components/note/comment';
|
||||||
|
import { NoteExtend } from '@components/note/extend';
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { getAllCommentNotes, getNoteByID } from '@utils/storage';
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import {
|
||||||
|
JSXElementConstructor,
|
||||||
|
ReactElement,
|
||||||
|
ReactFragment,
|
||||||
|
ReactPortal,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const id = router.query.id || null;
|
||||||
|
|
||||||
|
const relays: any = useAtomValue(relaysAtom);
|
||||||
|
|
||||||
|
const [rootEvent, setRootEvent] = useState(null);
|
||||||
|
const [comments, setComments] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getNoteByID(id)
|
||||||
|
.then((res) => {
|
||||||
|
setRootEvent(res);
|
||||||
|
getAllCommentNotes(id).then((res: any) => setComments(res));
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, [id, pool, relays]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scrollbar-hide flex h-full flex-col gap-2 overflow-y-auto py-3">
|
||||||
|
<div className="flex h-min min-h-min w-full select-text flex-col px-3">
|
||||||
|
{rootEvent && <NoteExtend event={rootEvent} />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FormComment eventID={id} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{comments.length > 0 && comments.map((comment) => <NoteComment key={comment.id} event={comment} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page.getLayout = function getLayout(
|
||||||
|
page:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
||||||
|
| ReactFragment
|
||||||
|
| ReactPortal
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<BaseLayout>
|
||||||
|
<WithSidebarLayout>{page}</WithSidebarLayout>
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
import BaseLayout from '@layouts/base';
|
||||||
import NewsFeedLayout from '@layouts/newsfeed';
|
import WithSidebarLayout from '@layouts/withSidebar';
|
||||||
|
|
||||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<p>Global</p>
|
<p className="text-sm text-zinc-400">Sorry, this feature under development, it will come in the next version</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ Page.getLayout = function getLayout(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<NewsFeedLayout>{page}</NewsFeedLayout>
|
<WithSidebarLayout>{page}</WithSidebarLayout>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,127 +1,48 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
import BaseLayout from '@layouts/base';
|
||||||
import NewsFeedLayout from '@layouts/newsfeed';
|
import WithSidebarLayout from '@layouts/withSidebar';
|
||||||
|
|
||||||
import { DatabaseContext } from '@components/contexts/database';
|
import FormBase from '@components/form/base';
|
||||||
import { NoteConnector } from '@components/note/connector';
|
import { NoteBase } from '@components/note/base';
|
||||||
import { Placeholder } from '@components/note/placeholder';
|
import { Placeholder } from '@components/note/placeholder';
|
||||||
import { Repost } from '@components/note/repost';
|
|
||||||
import { Single } from '@components/note/single';
|
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/getDate';
|
import { notesAtom } from '@stores/note';
|
||||||
|
|
||||||
import { ArrowUpIcon } from '@radix-ui/react-icons';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { writeStorage } from '@rehooks/local-storage';
|
import { useAtom } from 'jotai';
|
||||||
import { useCallback, useState } from 'react';
|
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, Suspense, useRef } from 'react';
|
||||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useEffect, useRef } from 'react';
|
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { db }: any = useContext(DatabaseContext);
|
const [data]: any = useAtom(notesAtom);
|
||||||
|
const parentRef = useRef(null);
|
||||||
|
|
||||||
const [data, setData] = useState([]);
|
const virtualizer = useVirtualizer({
|
||||||
const [parentReload, setParentReload] = useState(false);
|
count: data.length,
|
||||||
const [hasNewNote, setHasNewNote] = useState(false);
|
estimateSize: () => 500,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
const now = useRef(new Date());
|
getItemKey: (index) => data[index].id,
|
||||||
const limit = useRef(30);
|
});
|
||||||
const offset = useRef(0);
|
const items = virtualizer.getVirtualItems();
|
||||||
|
|
||||||
const loadMore = useCallback(async () => {
|
|
||||||
offset.current += limit.current;
|
|
||||||
// next query
|
|
||||||
const result = await db.select(
|
|
||||||
`SELECT * FROM
|
|
||||||
cache_notes
|
|
||||||
WHERE created_at <= ${dateToUnix(now.current)}
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT ${limit.current} OFFSET ${offset.current}`
|
|
||||||
);
|
|
||||||
setData((data) => [...data, ...result]);
|
|
||||||
}, [db]);
|
|
||||||
|
|
||||||
const loadNewest = useCallback(async () => {
|
|
||||||
const result = await db.select(
|
|
||||||
`SELECT * FROM
|
|
||||||
cache_notes
|
|
||||||
WHERE created_at > ${dateToUnix(now.current)}
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT ${limit.current}`
|
|
||||||
);
|
|
||||||
setData((data) => [...result, ...data]);
|
|
||||||
setHasNewNote(false);
|
|
||||||
}, [db]);
|
|
||||||
|
|
||||||
const ItemContent = useCallback(
|
|
||||||
(index: string | number) => {
|
|
||||||
const event = data[index];
|
|
||||||
if (event.content.includes('#[0]') && event.tags[0][0] == 'e') {
|
|
||||||
// type: repost
|
|
||||||
return <Repost root={event.tags} user={event.pubkey} />;
|
|
||||||
} else {
|
|
||||||
// type: default
|
|
||||||
return <Single event={event} />;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[data]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getData = async () => {
|
|
||||||
const result = await db.select(
|
|
||||||
`SELECT * FROM cache_notes WHERE created_at <= ${dateToUnix(now.current)} ORDER BY created_at DESC LIMIT ${
|
|
||||||
limit.current
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
if (result) {
|
|
||||||
setData(result);
|
|
||||||
writeStorage('settings', new Date());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getData().catch(console.error);
|
|
||||||
}, [db, parentReload]);
|
|
||||||
|
|
||||||
const computeItemKey = useCallback(
|
|
||||||
(index: string | number) => {
|
|
||||||
return data[index].id;
|
|
||||||
},
|
|
||||||
[data]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full">
|
<div ref={parentRef} className="scrollbar-hide h-full w-full overflow-y-auto" style={{ contain: 'strict' }}>
|
||||||
<NoteConnector setParentReload={setParentReload} setHasNewNote={setHasNewNote} currentDate={now.current} />
|
<div className="relative">
|
||||||
{hasNewNote && (
|
<FormBase />
|
||||||
<div className="absolute top-16 left-1/2 z-50 -translate-x-1/2 transform">
|
</div>
|
||||||
<button
|
<Suspense fallback={<Placeholder />}>
|
||||||
onClick={() => loadNewest()}
|
<div>
|
||||||
className="inline-flex h-8 transform items-center justify-center gap-1 rounded-full bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 pl-3 pr-3.5 text-sm shadow-lg active:translate-y-1"
|
{items.length > 0 && (
|
||||||
>
|
<div className="relative w-full" style={{ height: virtualizer.getTotalSize() }}>
|
||||||
<ArrowUpIcon className="h-4 w-4" />
|
<div className="absolute top-0 left-0 w-full" style={{ transform: `translateY(${items[0].start}px)` }}>
|
||||||
<span className="drop-shadow-md">Load newest</span>
|
{items.map((virtualRow) => (
|
||||||
</button>
|
<div key={virtualRow.key} data-index={virtualRow.index} ref={virtualizer.measureElement}>
|
||||||
|
<NoteBase event={data[virtualRow.index]} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Suspense>
|
||||||
<Virtuoso
|
|
||||||
data={data}
|
|
||||||
itemContent={ItemContent}
|
|
||||||
components={{
|
|
||||||
EmptyPlaceholder: () => <Placeholder />,
|
|
||||||
ScrollSeekPlaceholder: () => <Placeholder />,
|
|
||||||
}}
|
|
||||||
computeItemKey={computeItemKey}
|
|
||||||
scrollSeekConfiguration={{
|
|
||||||
enter: (velocity) => Math.abs(velocity) > 800,
|
|
||||||
exit: (velocity) => Math.abs(velocity) < 500,
|
|
||||||
}}
|
|
||||||
endReached={loadMore}
|
|
||||||
overscan={800}
|
|
||||||
increaseViewportBy={1000}
|
|
||||||
className="relative h-full w-full"
|
|
||||||
style={{
|
|
||||||
contain: 'strict',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -137,7 +58,7 @@ Page.getLayout = function getLayout(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<NewsFeedLayout>{page}</NewsFeedLayout>
|
<WithSidebarLayout>{page}</WithSidebarLayout>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
import BaseLayout from '@layouts/base';
|
||||||
import OnboardingLayout from '@layouts/onboarding';
|
|
||||||
|
|
||||||
import { DatabaseContext } from '@components/contexts/database';
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
import { RelayContext } from '@components/contexts/relay';
|
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { createAccount } from '@utils/storage';
|
||||||
|
|
||||||
import { EyeClosedIcon, EyeOpenIcon } from '@radix-ui/react-icons';
|
import { EyeClosedIcon, EyeOpenIcon } from '@radix-ui/react-icons';
|
||||||
import { useLocalStorage, writeStorage } from '@rehooks/local-storage';
|
import { useAtomValue } from 'jotai';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools';
|
import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools';
|
||||||
import {
|
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useMemo, useState } from 'react';
|
||||||
JSXElementConstructor,
|
|
||||||
ReactElement,
|
|
||||||
ReactFragment,
|
|
||||||
ReactPortal,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
|
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
@@ -28,12 +20,9 @@ const config: Config = {
|
|||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
const { db }: any = useContext(DatabaseContext);
|
const relays = useAtomValue(relaysAtom);
|
||||||
const relayPool: any = useContext(RelayContext);
|
|
||||||
|
|
||||||
const [relays] = useLocalStorage('relays');
|
|
||||||
|
|
||||||
const [type, setType] = useState('password');
|
const [type, setType] = useState('password');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -44,6 +33,30 @@ export default function Page() {
|
|||||||
const npub = nip19.npubEncode(pubKey);
|
const npub = nip19.npubEncode(pubKey);
|
||||||
const nsec = nip19.nsecEncode(privKey);
|
const nsec = nip19.nsecEncode(privKey);
|
||||||
|
|
||||||
|
// auto-generated profile metadata
|
||||||
|
const metadata = useMemo(
|
||||||
|
() => ({
|
||||||
|
display_name: name,
|
||||||
|
name: name,
|
||||||
|
username: name.toLowerCase(),
|
||||||
|
picture: 'https://void.cat/d/KmypFh2fBdYCEvyJrPiN89',
|
||||||
|
}),
|
||||||
|
[name]
|
||||||
|
);
|
||||||
|
|
||||||
|
// build profile
|
||||||
|
const data = useMemo(
|
||||||
|
() => ({
|
||||||
|
pubkey: pubKey,
|
||||||
|
privkey: privKey,
|
||||||
|
npub: npub,
|
||||||
|
nsec: nsec,
|
||||||
|
metadata: metadata,
|
||||||
|
}),
|
||||||
|
[metadata, npub, nsec, privKey, pubKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
// toggle privatek key
|
||||||
const showPrivateKey = () => {
|
const showPrivateKey = () => {
|
||||||
if (type === 'password') {
|
if (type === 'password') {
|
||||||
setType('text');
|
setType('text');
|
||||||
@@ -52,32 +65,13 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// auto-generated profile
|
// create account and broadcast to all relays
|
||||||
const data = useMemo(
|
const submit = () => {
|
||||||
() => ({
|
|
||||||
display_name: name,
|
|
||||||
name: name,
|
|
||||||
username: name.toLowerCase(),
|
|
||||||
picture: 'https://bafybeidfsbrzqbvontmucteomoz2rkrxugu462l5hyhh6uioslkfzzs4oq.ipfs.w3s.link/avatar-11.png',
|
|
||||||
banner: 'https://bafybeiacwit7hjmdefqggxqtgh6ht5dhth7ndptwn2msl5kpkodudsr7py.ipfs.w3s.link/banner-1.jpg',
|
|
||||||
}),
|
|
||||||
[name]
|
|
||||||
);
|
|
||||||
|
|
||||||
const insertDB = useCallback(async () => {
|
|
||||||
await db.execute(
|
|
||||||
`INSERT INTO accounts (id, privkey, npub, nsec, metadata) VALUES ("${pubKey}", "${privKey}", "${npub}", "${nsec}", '${JSON.stringify(
|
|
||||||
data
|
|
||||||
)}')`
|
|
||||||
);
|
|
||||||
}, [data, db, npub, nsec, privKey, pubKey]);
|
|
||||||
|
|
||||||
const createAccount = async () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// build event
|
// build event
|
||||||
const event: any = {
|
const event: any = {
|
||||||
content: JSON.stringify(data),
|
content: JSON.stringify(metadata),
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
kind: 0,
|
kind: 0,
|
||||||
pubkey: pubKey,
|
pubkey: pubKey,
|
||||||
@@ -86,129 +80,113 @@ export default function Page() {
|
|||||||
event.id = getEventHash(event);
|
event.id = getEventHash(event);
|
||||||
event.sig = signEvent(event, privKey);
|
event.sig = signEvent(event, privKey);
|
||||||
|
|
||||||
insertDB()
|
// insert to database then broadcast
|
||||||
|
createAccount(data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// publish to relays
|
pool.publish(event, relays);
|
||||||
relayPool.publish(event, relays);
|
router.push({
|
||||||
// set currentUser in global state
|
pathname: '/onboarding/create/step-2',
|
||||||
writeStorage('current-user', {
|
query: { id: pubKey, privkey: privKey },
|
||||||
metadata: JSON.stringify(data),
|
|
||||||
npub: npub,
|
|
||||||
privkey: privKey,
|
|
||||||
pubkey: pubKey,
|
|
||||||
});
|
});
|
||||||
// redirect to pre-follow
|
|
||||||
setTimeout(() => {
|
|
||||||
setLoading(false);
|
|
||||||
router.push('/onboarding/create/pre-follows');
|
|
||||||
}, 1500);
|
|
||||||
})
|
})
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col justify-between px-8">
|
<div className="grid h-full w-full grid-rows-5">
|
||||||
<div>{/* spacer */}</div>
|
<div className="row-span-1 flex items-center justify-center">
|
||||||
<motion.div layoutId="form">
|
<div>
|
||||||
<div className="mb-8 flex flex-col gap-3">
|
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
|
||||||
<motion.h1
|
Create new account
|
||||||
layoutId="title"
|
</h1>
|
||||||
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent"
|
|
||||||
>
|
|
||||||
Create new key
|
|
||||||
</motion.h1>
|
|
||||||
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
|
|
||||||
Lume will generate key with default profile for you, you can edit it later, and please store your key safely
|
|
||||||
so you can restore your account or use other client
|
|
||||||
</motion.h2>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="row-span-4">
|
||||||
<label className="text-sm font-semibold text-zinc-400">Public Key</label>
|
<div className="mx-auto w-full max-w-md">
|
||||||
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
<div className="mb-8 flex flex-col gap-4">
|
||||||
<input
|
<div className="flex flex-col gap-1">
|
||||||
readOnly
|
<label className="text-sm font-semibold text-zinc-400">Public Key</label>
|
||||||
value={npub}
|
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
||||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
|
<input
|
||||||
/>
|
readOnly
|
||||||
|
value={npub}
|
||||||
|
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex flex-col gap-1">
|
<label className="text-sm font-semibold text-zinc-400">Private Key</label>
|
||||||
<label className="text-sm font-semibold text-zinc-400">Private Key</label>
|
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
||||||
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
<input
|
||||||
<input
|
readOnly
|
||||||
readOnly
|
type={type}
|
||||||
type={type}
|
value={nsec}
|
||||||
value={nsec}
|
className="relative w-full rounded-lg border border-black/5 py-2.5 pl-3.5 pr-11 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
|
||||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
|
/>
|
||||||
/>
|
<button
|
||||||
<button
|
onClick={() => showPrivateKey()}
|
||||||
onClick={() => showPrivateKey()}
|
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
||||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
>
|
||||||
>
|
{type === 'password' ? (
|
||||||
{type === 'password' ? (
|
<EyeClosedIcon className="h-5 w-5 text-zinc-500 group-hover:text-zinc-200" />
|
||||||
<EyeClosedIcon className="h-5 w-5 text-zinc-500 group-hover:text-zinc-200" />
|
) : (
|
||||||
) : (
|
<EyeOpenIcon className="h-5 w-5 text-zinc-500 group-hover:text-zinc-200" />
|
||||||
<EyeOpenIcon className="h-5 w-5 text-zinc-500 group-hover:text-zinc-200" />
|
)}
|
||||||
)}
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex flex-col gap-1">
|
<label className="text-sm font-semibold text-zinc-400">Default Profile (you can change it later)</label>
|
||||||
<label className="text-sm font-semibold text-zinc-400">Default Profile (you can change it later)</label>
|
<div className="relative w-full shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
||||||
<div className="relative max-w-sm shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
<div className="relative w-full rounded-lg border border-black/5 px-3.5 py-4 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600">
|
||||||
<div className="relative max-w-sm rounded-lg border border-black/5 px-3.5 py-4 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600">
|
<div className="flex space-x-2">
|
||||||
<div className="flex space-x-4">
|
<div className="relative h-11 w-11 rounded-md">
|
||||||
<div className="relative h-10 w-10 rounded-full">
|
<Image className="inline-block rounded-md" src={metadata.picture} alt="" fill={true} />
|
||||||
<Image className="inline-block rounded-full" src={data.picture} alt="" fill={true} />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 space-y-4 py-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p className="font-semibold">{data.display_name}</p>
|
|
||||||
<p className="text-zinc-400">@{data.username}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="flex-1 space-y-2 py-1">
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="flex items-center gap-1">
|
||||||
<div className="col-span-2 h-2 rounded bg-zinc-700"></div>
|
<p className="font-semibold">{metadata.display_name}</p>
|
||||||
<div className="col-span-1 h-2 rounded bg-zinc-700"></div>
|
<p className="text-zinc-400">@{metadata.username}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="col-span-2 h-2 rounded bg-zinc-700"></div>
|
||||||
|
<div className="col-span-1 h-2 rounded bg-zinc-700"></div>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 rounded bg-zinc-700"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 rounded bg-zinc-700"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex h-10 items-center justify-center">
|
||||||
</motion.div>
|
{loading === true ? (
|
||||||
<motion.div layoutId="action" className="pb-5">
|
<svg
|
||||||
<div className="flex h-10 items-center">
|
className="h-5 w-5 animate-spin text-white"
|
||||||
{loading === true ? (
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg
|
fill="none"
|
||||||
className="h-5 w-5 animate-spin text-white"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
>
|
||||||
fill="none"
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
viewBox="0 0 24 24"
|
<path
|
||||||
>
|
className="opacity-75"
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
fill="currentColor"
|
||||||
<path
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
className="opacity-75"
|
></path>
|
||||||
fill="currentColor"
|
</svg>
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
) : (
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => createAccount()}
|
onClick={() => submit()}
|
||||||
className="transform rounded-lg border border-white/10 bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium shadow-input shadow-black/5 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-50 dark:shadow-black/10"
|
className="w-full transform rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<span className="drop-shadow-lg">Continue →</span>
|
<span className="drop-shadow-lg">Continue →</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -222,9 +200,5 @@ Page.getLayout = function getLayout(
|
|||||||
| ReactFragment
|
| ReactFragment
|
||||||
| ReactPortal
|
| ReactPortal
|
||||||
) {
|
) {
|
||||||
return (
|
return <BaseLayout>{page}</BaseLayout>;
|
||||||
<BaseLayout>
|
|
||||||
<OnboardingLayout>{page}</OnboardingLayout>
|
|
||||||
</BaseLayout>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
|
||||||
import OnboardingLayout from '@layouts/onboarding';
|
|
||||||
|
|
||||||
import { DatabaseContext } from '@components/contexts/database';
|
|
||||||
|
|
||||||
import { truncate } from '@utils/truncate';
|
|
||||||
|
|
||||||
import data from '@assets/directory.json';
|
|
||||||
import { CheckCircledIcon } from '@radix-ui/react-icons';
|
|
||||||
import { useLocalStorage } from '@rehooks/local-storage';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { nip19 } from 'nostr-tools';
|
|
||||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useState } from 'react';
|
|
||||||
|
|
||||||
const shuffle = (arr: { name: string; avatar: string; npub: string }[]) => [...arr].sort(() => Math.random() - 0.5);
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const { db }: any = useContext(DatabaseContext);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [follow, setFollow] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [list] = useState(shuffle(data));
|
|
||||||
const [currentUser]: any = useLocalStorage('current-user');
|
|
||||||
|
|
||||||
const followUser = (e) => {
|
|
||||||
const npub = e.currentTarget.getAttribute('data-npub');
|
|
||||||
setFollow((arr) => [...arr, npub]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const insertDB = async () => {
|
|
||||||
// self follow
|
|
||||||
await db.execute(
|
|
||||||
`INSERT INTO follows (pubkey, account, kind) VALUES ("${currentUser.id}", "${currentUser.id}", "0")`
|
|
||||||
);
|
|
||||||
// follow selected
|
|
||||||
follow.forEach(async (npub) => {
|
|
||||||
const { data } = nip19.decode(npub);
|
|
||||||
await db.execute(`INSERT INTO follows (pubkey, account, kind) VALUES ("${data}", "${currentUser.id}", "0")`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFollowing = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
insertDB().then(() =>
|
|
||||||
setTimeout(() => {
|
|
||||||
setLoading(false);
|
|
||||||
router.push('/');
|
|
||||||
}, 1500)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col justify-between px-8">
|
|
||||||
<div>{/* spacer */}</div>
|
|
||||||
<motion.div layoutId="form" className="flex flex-col">
|
|
||||||
<div className="mb-8 flex flex-col gap-3">
|
|
||||||
<motion.h1
|
|
||||||
layoutId="title"
|
|
||||||
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent"
|
|
||||||
>
|
|
||||||
Choose 10 people you want to following
|
|
||||||
</motion.h1>
|
|
||||||
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
|
|
||||||
For better experiences, you should follow the people you care about to personalize your newsfeed, otherwise
|
|
||||||
you will be very bored
|
|
||||||
</motion.h2>
|
|
||||||
</div>
|
|
||||||
<div className="h-full w-full shrink">
|
|
||||||
<div className="scrollbar-hide grid grid-cols-3 gap-4 overflow-y-auto">
|
|
||||||
{list.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
onClick={(e) => followUser(e)}
|
|
||||||
data-npub={item.npub}
|
|
||||||
className={`col-span-1 inline-flex cursor-pointer items-center gap-3 rounded-lg p-2 hover:bg-zinc-700 ${
|
|
||||||
follow.includes(item.npub) ? 'bg-zinc-800' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="relative h-10 w-10 flex-shrink-0">
|
|
||||||
<Image className="rounded-full object-cover" src={item.avatar} alt={item.name} fill={true} />
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex flex-1 items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="truncate text-sm font-medium text-zinc-200">{item.name}</p>
|
|
||||||
<p className="text-sm leading-tight text-zinc-500">{truncate(item.npub, 16, ' .... ')}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{follow.includes(item.npub) ? <CheckCircledIcon className="h-4 w-4 text-green-500" /> : <></>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
<motion.div layoutId="action" className="pb-5">
|
|
||||||
<div className="flex h-10 items-center">
|
|
||||||
{loading === true ? (
|
|
||||||
<svg
|
|
||||||
className="h-5 w-5 animate-spin text-white"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
|
||||||
<button
|
|
||||||
onClick={() => createFollowing()}
|
|
||||||
disabled={follow.length < 10 ? true : false}
|
|
||||||
className="transform rounded-lg border border-white/10 bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium shadow-input shadow-black/5 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-50 dark:shadow-black/10"
|
|
||||||
>
|
|
||||||
<span className="drop-shadow-lg">Finish →</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Page.getLayout = function getLayout(
|
|
||||||
page:
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
|
||||||
| ReactFragment
|
|
||||||
| ReactPortal
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<BaseLayout>
|
|
||||||
<OnboardingLayout>{page}</OnboardingLayout>
|
|
||||||
</BaseLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
203
src/pages/onboarding/create/step-2.tsx
Normal file
203
src/pages/onboarding/create/step-2.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import BaseLayout from '@layouts/base';
|
||||||
|
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
import { UserBase } from '@components/user/base';
|
||||||
|
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { createFollows } from '@utils/storage';
|
||||||
|
|
||||||
|
import { CheckCircledIcon } from '@radix-ui/react-icons';
|
||||||
|
import { createClient } from '@supabase/supabase-js';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { getEventHash, signEvent } from 'nostr-tools';
|
||||||
|
import {
|
||||||
|
JSXElementConstructor,
|
||||||
|
Key,
|
||||||
|
ReactElement,
|
||||||
|
ReactFragment,
|
||||||
|
ReactPortal,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
'https://niwaazauwnrwiwmnocnn.supabase.co',
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5pd2FhemF1d25yd2l3bW5vY25uIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NzYwMjAzMjAsImV4cCI6MTk5MTU5NjMyMH0.IbjrnE6rDgC6lhIAHBIMN4niM2bPjxkRLtvAy_gFgqw'
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialList = [
|
||||||
|
{ pubkey: '82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2' },
|
||||||
|
{ pubkey: 'a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98' },
|
||||||
|
{ pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9' },
|
||||||
|
{ pubkey: 'c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0' },
|
||||||
|
{ pubkey: '6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93' },
|
||||||
|
{ pubkey: 'e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411' },
|
||||||
|
{ pubkey: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d' },
|
||||||
|
{ pubkey: 'c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15' },
|
||||||
|
{ pubkey: 'e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42' },
|
||||||
|
{ pubkey: '84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240' },
|
||||||
|
{ pubkey: '703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898' },
|
||||||
|
{ pubkey: 'bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce' },
|
||||||
|
{ pubkey: '4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0' },
|
||||||
|
{ pubkey: 'c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965' },
|
||||||
|
{ pubkey: 'c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6' },
|
||||||
|
{ pubkey: '6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3' },
|
||||||
|
{ pubkey: '50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63' },
|
||||||
|
{ pubkey: '3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594' },
|
||||||
|
{ pubkey: '6e3f51664e19e082df5217fd4492bb96907405a0b27028671dd7f297b688608c' },
|
||||||
|
{ pubkey: '2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884' },
|
||||||
|
{ pubkey: '3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24' },
|
||||||
|
{ pubkey: 'eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f' },
|
||||||
|
{ pubkey: 'be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479' },
|
||||||
|
{ pubkey: 'a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f' },
|
||||||
|
{ pubkey: '1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b' },
|
||||||
|
{ pubkey: 'c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5' },
|
||||||
|
{ pubkey: '460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c' },
|
||||||
|
{ pubkey: '7f3b464b9ff3623630485060cbda3a7790131c5339a7803bde8feb79a5e1b06a' },
|
||||||
|
{ pubkey: 'b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27' },
|
||||||
|
{ pubkey: 'e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2' },
|
||||||
|
{ pubkey: 'ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14' },
|
||||||
|
{ pubkey: 'ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { id, privkey }: any = router.query || '';
|
||||||
|
|
||||||
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [list, setList]: any = useState(initialList);
|
||||||
|
const [follows, setFollows] = useState([]);
|
||||||
|
|
||||||
|
// toggle follow state
|
||||||
|
const toggleFollow = (pubkey: string) => {
|
||||||
|
const arr = follows.includes(pubkey) ? follows.filter((i) => i !== pubkey) : [...follows, pubkey];
|
||||||
|
setFollows(arr);
|
||||||
|
};
|
||||||
|
|
||||||
|
// build event tags
|
||||||
|
const tags = () => {
|
||||||
|
const arr = [];
|
||||||
|
// push item to tags
|
||||||
|
follows.forEach((item) => {
|
||||||
|
arr.push(['p', item]);
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// save follows to database then broadcast
|
||||||
|
const submit = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// build event
|
||||||
|
const event: any = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: 3,
|
||||||
|
pubkey: id,
|
||||||
|
tags: tags(),
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = signEvent(event, privkey);
|
||||||
|
|
||||||
|
createFollows(follows, id, 0)
|
||||||
|
.then((res) => {
|
||||||
|
if (res === 'ok') {
|
||||||
|
// publish to relays
|
||||||
|
pool.publish(event, relays);
|
||||||
|
router.push('/init');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
const { data } = await supabase.from('random_users').select('pubkey').limit(28);
|
||||||
|
// update state
|
||||||
|
setList((list: any) => [...list, ...data]);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData().catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative grid h-full w-full grid-rows-5">
|
||||||
|
<div className="row-span-1 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium leading-tight text-transparent">
|
||||||
|
Personalized your newsfeed
|
||||||
|
</h1>
|
||||||
|
<h3 className="text-lg text-zinc-500">
|
||||||
|
Follow at least{' '}
|
||||||
|
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
|
||||||
|
{follows.length}/10
|
||||||
|
</span>{' '}
|
||||||
|
plebs
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="scrollbar-hide row-span-4 h-full w-full overflow-y-auto">
|
||||||
|
<div className="grid grid-cols-4 gap-4 px-8 py-4">
|
||||||
|
{list.map((item: { pubkey: string }, index: Key) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => toggleFollow(item.pubkey)}
|
||||||
|
className="flex transform items-center justify-between rounded-lg bg-zinc-900 p-2 ring-amber-100 hover:ring-1 active:translate-y-1"
|
||||||
|
>
|
||||||
|
<UserBase pubkey={item.pubkey} />
|
||||||
|
{follows.includes(item.pubkey) && (
|
||||||
|
<div>
|
||||||
|
<CheckCircledIcon className="h-4 w-4 text-green-400" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{follows.length >= 10 && (
|
||||||
|
<div className="fixed bottom-0 left-0 z-10 flex h-24 w-full items-center justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => submit()}
|
||||||
|
className="relative z-20 inline-flex w-36 transform items-center justify-center rounded-full bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 shadow-xl active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
|
>
|
||||||
|
{loading === true ? (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 animate-spin text-zinc-900"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<span className="drop-shadow-lg">Done →</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page.getLayout = function getLayout(
|
||||||
|
page:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
||||||
|
| ReactFragment
|
||||||
|
| ReactPortal
|
||||||
|
) {
|
||||||
|
return <BaseLayout>{page}</BaseLayout>;
|
||||||
|
};
|
||||||
@@ -1,47 +1,130 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
import BaseLayout from '@layouts/base';
|
||||||
import OnboardingLayout from '@layouts/onboarding';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { ArrowRightIcon } from '@radix-ui/react-icons';
|
||||||
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
||||||
|
|
||||||
|
const PLEBS = [
|
||||||
|
'https://133332.xyz/p.jpg',
|
||||||
|
'https://void.cat/d/3Bp6jSHURFNQ9u3pK8nwtq.webp',
|
||||||
|
'https://i.imgur.com/f8SyhRL.jpg',
|
||||||
|
'http://nostr.build/i/6369.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1622010345589190656/mAPqsmtz_400x400.jpg',
|
||||||
|
'https://media.tenor.com/l5arkXy9RfIAAAAd/thunder.gif',
|
||||||
|
'https://nostr.build/i/p/nostr.build_0e412058980ed2ac4adf3de639304c9e970e2745ba9ca19c75f984f4f6da4971.jpeg',
|
||||||
|
'https://nostr.build/i/nostr.build_864a019a6c1d3a90a17363553d32b71de618d250f02cf0a59ca19fb3029fd5bc.jpg',
|
||||||
|
'https://void.cat/d/8zE9T8a39YfUVjrLM4xcpE.webp',
|
||||||
|
'https://avatars.githubusercontent.com/u/89577423',
|
||||||
|
'https://pbs.twimg.com/profile_images/1363180486080663554/iN-r_BiM_400x400.jpg',
|
||||||
|
'https://void.cat/d/JUBBqXgCcGBEh7jUgJaayy',
|
||||||
|
'https://phase1.attract-eu.com/wp-content/uploads/2020/03/ATTRACT_HPLM.png',
|
||||||
|
'https://www.retro-synthwave.com/wp-content/uploads/2017/01/PowerGlove-23.jpg',
|
||||||
|
'https://void.cat/d/KvAEMvYNmy1rfCH6a7HZzh.webp',
|
||||||
|
'https://media.giphy.com/media/NqfMNCkyGwtXhKFlCR/giphy-downsized-large.gif',
|
||||||
|
'https://i.imgur.com/VGpUNFS.jpg',
|
||||||
|
'https://nostr.build/i/p/nostr.build_b39254db43d5557df99d1eb516f1c2f56a21a01b10c248f6eb66aa827c9a90f4.jpeg',
|
||||||
|
'https://davidcoen.it/wp-content/uploads/2020/11/7004972-taglio.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1570432066348515330/26PtCuwF_400x400.jpg',
|
||||||
|
'https://nostr.build/i/nostr.build_9d33ee801aa08955be174554832952ab95a65d5e015176834c8aa9a4e2f2e3a5.jpg',
|
||||||
|
'https://www.linkpicture.com/q/0FE78CFF-C931-4568-A7AA-DD8AEE889992.jpeg',
|
||||||
|
'https://nostr.build/i/nostr.build_97d6e2d25dd92422eb3d6d645b7cee9ed9c614f331be7e6f7db9ccfdbc5ee260.png',
|
||||||
|
'https://pbs.twimg.com/profile_images/1569570198348337152/-n1KD74u_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1600149653898596354/5PVe-r-J_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1639659216372658178/Dnn-Ysp-_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1554429112978120706/yr1hXl6R_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1615478486688272385/q2ECeZDX_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1638644441773748226/tNsA6RpG_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1607882836740120576/3Tg1mTYJ_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1401907430339002369/WKrP9Esn_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1523971278478131200/TMPzfvhE_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1626421539884204032/aj4tmzsk_400x400.png',
|
||||||
|
'https://pbs.twimg.com/profile_images/1582771691779985408/C9MHYIgt_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1409612480465276931/38Vyx4e8_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1549826566787588098/MlduJCZO_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/539211568035004416/sBMjPR9q_400x400.jpeg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1548660003522887682/1QMHmles_400x400.jpg',
|
||||||
|
'https://pbs.twimg.com/profile_images/1362497143999787013/KLUoN1Vn_400x400.png',
|
||||||
|
'https://pbs.twimg.com/profile_images/1600434913240563713/AssmMGwf_400x400.jpg',
|
||||||
|
];
|
||||||
|
|
||||||
|
const DURATION = 50000;
|
||||||
|
const ROWS = 7;
|
||||||
|
const PLEBS_PER_ROW = 20;
|
||||||
|
|
||||||
|
const random = (min, max) => Math.floor(Math.random() * (max - min)) + min;
|
||||||
|
const shuffle = (arr) => [...arr].sort(() => 0.5 - Math.random());
|
||||||
|
|
||||||
|
const InfiniteLoopSlider = ({ children, duration, reverse }: { children: any; duration: any; reverse: any }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="flex w-fit"
|
||||||
|
style={{
|
||||||
|
animationName: 'loop',
|
||||||
|
animationIterationCount: 'infinite',
|
||||||
|
animationDirection: reverse ? 'reverse' : 'normal',
|
||||||
|
animationDuration: duration + 'ms',
|
||||||
|
animationTimingFunction: 'linear',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col justify-between px-8">
|
<div className="grid h-full w-full grid-rows-5">
|
||||||
<div>{/* spacer */}</div>
|
<div className="row-span-3 overflow-hidden">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="relaive flex w-full max-w-full shrink-0 flex-col gap-4 overflow-hidden p-4">
|
||||||
<motion.h1
|
{[...new Array(ROWS)].map((_, i) => (
|
||||||
layoutId="title"
|
<InfiniteLoopSlider key={i} duration={random(DURATION - 5000, DURATION + 20000)} reverse={i % 2}>
|
||||||
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent"
|
{shuffle(PLEBS)
|
||||||
>
|
.slice(0, PLEBS_PER_ROW)
|
||||||
Other social network require email/password
|
.map((tag) => (
|
||||||
<br />
|
<div
|
||||||
nostr use{' '}
|
key={tag}
|
||||||
<span className="bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 bg-clip-text text-transparent">
|
className="relative mr-4 flex h-11 w-11 items-center gap-2 rounded-md bg-zinc-900 px-4 py-1.5 shadow-xl"
|
||||||
public/private key instead
|
>
|
||||||
</span>
|
<Image
|
||||||
</motion.h1>
|
src={tag}
|
||||||
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
|
alt={tag}
|
||||||
If you have used nostr before, you can import your own private key. Otherwise, you can create a new key or use
|
fill={true}
|
||||||
auto-generated account created by system.
|
className="rounded-md border border-zinc-900"
|
||||||
</motion.h2>
|
placeholder="blur"
|
||||||
<motion.div layoutId="form"></motion.div>
|
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkqAcAAIUAgUW0RjgAAAAASUVORK5CYII="
|
||||||
<motion.div layoutId="action" className="mt-4 flex gap-2">
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</InfiniteLoopSlider>
|
||||||
|
))}
|
||||||
|
<div className="pointer-events-none absolute inset-0 bg-fade" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row-span-2 flex w-full flex-col items-center gap-8 overflow-hidden pt-10">
|
||||||
|
<h1 className="animate-moveBg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-5xl font-bold leading-none text-transparent">
|
||||||
|
Let's start!
|
||||||
|
</h1>
|
||||||
|
<div className="mt-4 flex flex-col items-center gap-1.5">
|
||||||
<Link
|
<Link
|
||||||
href="/onboarding/create"
|
href="/onboarding/create"
|
||||||
className="hover:bg-zinc-900/2.5 transform rounded-lg border border-black/5 bg-zinc-800 px-3.5 py-2 font-medium ring-1 ring-inset ring-zinc-900/10 hover:text-zinc-900 active:translate-y-1 dark:text-zinc-300 dark:ring-white/10 dark:hover:bg-zinc-700 dark:hover:text-white"
|
className="relative inline-flex h-14 w-64 items-center justify-center gap-2 rounded-full bg-zinc-900 px-6 text-lg font-medium ring-1 ring-zinc-800 hover:bg-zinc-800"
|
||||||
>
|
>
|
||||||
Create new key
|
Create new key
|
||||||
|
<ArrowRightIcon className="h-5 w-5" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/onboarding/login"
|
href="/onboarding/login"
|
||||||
className="hover:bg-zinc-900/2.5 transform rounded-lg border border-black/5 bg-zinc-800 px-3.5 py-2 font-medium ring-1 ring-inset ring-zinc-900/10 hover:text-zinc-900 active:translate-y-1 dark:text-zinc-300 dark:ring-white/10 dark:hover:bg-zinc-700 dark:hover:text-white"
|
className="inline-flex h-14 w-64 items-center justify-center gap-2 rounded-full px-6 text-base font-medium text-zinc-300 hover:bg-zinc-800"
|
||||||
>
|
>
|
||||||
Login with private key
|
Login with private key
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>{/* spacer */}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -55,9 +138,5 @@ Page.getLayout = function getLayout(
|
|||||||
| ReactFragment
|
| ReactFragment
|
||||||
| ReactPortal
|
| ReactPortal
|
||||||
) {
|
) {
|
||||||
return (
|
return <BaseLayout>{page}</BaseLayout>;
|
||||||
<BaseLayout>
|
|
||||||
<OnboardingLayout>{page}</OnboardingLayout>
|
|
||||||
</BaseLayout>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
|
||||||
import OnboardingLayout from '@layouts/onboarding';
|
|
||||||
|
|
||||||
import { DatabaseContext } from '@components/contexts/database';
|
|
||||||
import { RelayContext } from '@components/contexts/relay';
|
|
||||||
|
|
||||||
import { useLocalStorage } from '@rehooks/local-storage';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { getPublicKey, nip19 } from 'nostr-tools';
|
|
||||||
import {
|
|
||||||
JSXElementConstructor,
|
|
||||||
ReactElement,
|
|
||||||
ReactFragment,
|
|
||||||
ReactPortal,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const { db }: any = useContext(DatabaseContext);
|
|
||||||
const relayPool: any = useContext(RelayContext);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [relays] = useLocalStorage('relays');
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { privkey }: any = router.query;
|
|
||||||
|
|
||||||
const pubkey = useMemo(() => (privkey ? getPublicKey(privkey) : null), [privkey]);
|
|
||||||
|
|
||||||
// save account to database
|
|
||||||
const insertAccount = useCallback(
|
|
||||||
async (metadata) => {
|
|
||||||
if (loading === false) {
|
|
||||||
const npub = privkey ? nip19.npubEncode(pubkey) : null;
|
|
||||||
const nsec = privkey ? nip19.nsecEncode(privkey) : null;
|
|
||||||
await db.execute(
|
|
||||||
`INSERT OR IGNORE INTO accounts (id, privkey, npub, nsec, metadata) VALUES ("${pubkey}", "${privkey}", "${npub}", "${nsec}", '${metadata}')`
|
|
||||||
);
|
|
||||||
setLoading(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[db, privkey, pubkey, loading]
|
|
||||||
);
|
|
||||||
|
|
||||||
// save follows to database
|
|
||||||
const insertFollows = useCallback(
|
|
||||||
async (follows) => {
|
|
||||||
follows.forEach(async (item) => {
|
|
||||||
if (item) {
|
|
||||||
await db.execute(
|
|
||||||
`INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES ("${item[1]}", "${pubkey}", "0")`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[db, pubkey]
|
|
||||||
);
|
|
||||||
|
|
||||||
relayPool.subscribe(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
authors: [pubkey],
|
|
||||||
kinds: [0, 3],
|
|
||||||
since: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
relays,
|
|
||||||
(event: any) => {
|
|
||||||
if (event.kind === 0) {
|
|
||||||
insertAccount(event.content);
|
|
||||||
} else {
|
|
||||||
if (event.tags.length > 0) {
|
|
||||||
insertFollows(event.tags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
(events: any, relayURL: any) => {
|
|
||||||
console.log(events, relayURL);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col justify-between px-8">
|
|
||||||
<div>{/* spacer */}</div>
|
|
||||||
<motion.div layoutId="form">
|
|
||||||
<div className="mb-8 flex flex-col gap-3">
|
|
||||||
<motion.h1
|
|
||||||
layoutId="title"
|
|
||||||
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent"
|
|
||||||
>
|
|
||||||
Fetching your profile...
|
|
||||||
</motion.h1>
|
|
||||||
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
|
|
||||||
As long as you have private key, you alway can sync your profile and follows list on every nostr client, so
|
|
||||||
please keep your key safely
|
|
||||||
</motion.h2>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
<motion.div layoutId="action" className="pb-5">
|
|
||||||
<div className="flex h-10 items-center">
|
|
||||||
{loading === true ? (
|
|
||||||
<svg
|
|
||||||
className="h-5 w-5 animate-spin text-white"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="transform rounded-lg bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
|
||||||
>
|
|
||||||
<span className="drop-shadow-lg">Finish</span>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Page.getLayout = function getLayout(
|
|
||||||
page:
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
|
||||||
| ReactFragment
|
|
||||||
| ReactPortal
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<BaseLayout>
|
|
||||||
<OnboardingLayout>{page}</OnboardingLayout>
|
|
||||||
</BaseLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
import BaseLayout from '@layouts/base';
|
||||||
import OnboardingLayout from '@layouts/onboarding';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
import { LightningBoltIcon } from '@radix-ui/react-icons';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
||||||
@@ -43,7 +42,7 @@ export default function Page() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: '/onboarding/login/fetch',
|
pathname: '/onboarding/login/step-2',
|
||||||
query: { privkey: privkey },
|
query: { privkey: privkey },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -55,60 +54,72 @@ export default function Page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="flex h-full flex-col justify-between px-8">
|
<form onSubmit={handleSubmit(onSubmit)} className="grid h-full w-full grid-rows-5">
|
||||||
<div>{/* spacer */}</div>
|
<div className="row-span-1 flex items-center justify-center">
|
||||||
<motion.div layoutId="form">
|
<div>
|
||||||
<div className="mb-8 flex flex-col gap-3">
|
<h1 className="bg-gradient-to-br from-zinc-200 via-white to-zinc-300 bg-clip-text text-3xl font-semibold text-transparent">
|
||||||
<motion.h1
|
Login with Private Key
|
||||||
layoutId="title"
|
</h1>
|
||||||
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent"
|
|
||||||
>
|
|
||||||
Import your private key
|
|
||||||
</motion.h1>
|
|
||||||
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
|
|
||||||
You can import private key format as hex string or nsec. If you have installed Nostr Connect compality
|
|
||||||
wallet in your mobile, you can connect by scan QR Code below
|
|
||||||
</motion.h2>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
</div>
|
||||||
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
<div className="row-span-4">
|
||||||
<input
|
<div className="mx-auto w-full max-w-md">
|
||||||
{...register('key', { required: true, minLength: 32 })}
|
<div className="flex flex-col gap-4">
|
||||||
type={'password'}
|
<div>
|
||||||
placeholder="Paste nsec or hex key here..."
|
{/* #TODO: add function */}
|
||||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
<button className="inline-flex w-full transform items-center justify-center gap-1.5 rounded-lg bg-zinc-700 px-3.5 py-2.5 font-medium text-zinc-200 shadow-input ring-1 ring-zinc-600 active:translate-y-1">
|
||||||
/>
|
{/* #TODO: change to nostr connect logo */}
|
||||||
|
<LightningBoltIcon className="h-5 w-5 text-fuchsia-500" />
|
||||||
|
<span>Continue with Nostr Connect</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-zinc-800"></div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center">
|
||||||
|
<span className="bg-black px-2 text-sm text-zinc-500">or</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
||||||
|
<input
|
||||||
|
{...register('key', { required: true, minLength: 32 })}
|
||||||
|
type={'password'}
|
||||||
|
placeholder="Paste private key here..."
|
||||||
|
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 text-center shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-red-400">{errors.key && <p>{errors.key.message}</p>}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex h-10 items-center justify-center">
|
||||||
|
{isSubmitting ? (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 animate-spin text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isDirty || !isValid}
|
||||||
|
className="w-full transform rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
|
>
|
||||||
|
<span className="drop-shadow-lg">Continue →</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-red-400">{errors.key && <p>{errors.key.message}</p>}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
<motion.div layoutId="action" className="pb-5">
|
|
||||||
<div className="flex h-10 items-center">
|
|
||||||
{isSubmitting ? (
|
|
||||||
<svg
|
|
||||||
className="h-5 w-5 animate-spin text-white"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isDirty || !isValid}
|
|
||||||
className="transform rounded-lg bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
|
||||||
>
|
|
||||||
<span className="drop-shadow-lg">Continue →</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -122,9 +133,5 @@ Page.getLayout = function getLayout(
|
|||||||
| ReactFragment
|
| ReactFragment
|
||||||
| ReactPortal
|
| ReactPortal
|
||||||
) {
|
) {
|
||||||
return (
|
return <BaseLayout>{page}</BaseLayout>;
|
||||||
<BaseLayout>
|
|
||||||
<OnboardingLayout>{page}</OnboardingLayout>
|
|
||||||
</BaseLayout>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
158
src/pages/onboarding/login/step-2.tsx
Normal file
158
src/pages/onboarding/login/step-2.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import BaseLayout from '@layouts/base';
|
||||||
|
|
||||||
|
import { RelayContext } from '@components/relaysProvider';
|
||||||
|
|
||||||
|
import { relaysAtom } from '@stores/relays';
|
||||||
|
|
||||||
|
import { createAccount, createFollows } from '@utils/storage';
|
||||||
|
import { tagsToArray } from '@utils/transform';
|
||||||
|
import { truncate } from '@utils/truncate';
|
||||||
|
|
||||||
|
import destr from 'destr';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { getPublicKey, nip19 } from 'nostr-tools';
|
||||||
|
import {
|
||||||
|
JSXElementConstructor,
|
||||||
|
ReactElement,
|
||||||
|
ReactFragment,
|
||||||
|
ReactPortal,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const privkey: any = router.query.privkey || null;
|
||||||
|
const pubkey = privkey ? getPublicKey(privkey) : null;
|
||||||
|
|
||||||
|
const relays = useAtomValue(relaysAtom);
|
||||||
|
const [profile, setProfile] = useState(null);
|
||||||
|
const [done, setDone] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = pool.subscribe(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
authors: [pubkey],
|
||||||
|
kinds: [0, 3],
|
||||||
|
since: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relays,
|
||||||
|
(event: any) => {
|
||||||
|
if (event.kind === 0) {
|
||||||
|
const data = {
|
||||||
|
pubkey: pubkey,
|
||||||
|
privkey: privkey,
|
||||||
|
npub: nip19.npubEncode(pubkey),
|
||||||
|
nsec: nip19.nsecEncode(privkey),
|
||||||
|
metadata: event.content,
|
||||||
|
};
|
||||||
|
setProfile(destr(event.content));
|
||||||
|
createAccount(data);
|
||||||
|
} else {
|
||||||
|
if (event.tags.length > 0) {
|
||||||
|
createFollows(tagsToArray(event.tags), pubkey, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
() => {
|
||||||
|
setDone(true);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unsubscribeOnEose: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe;
|
||||||
|
};
|
||||||
|
}, [pool, privkey, pubkey, relays]);
|
||||||
|
|
||||||
|
// submit then redirect to home
|
||||||
|
const submit = () => {
|
||||||
|
router.push('/init');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid h-full w-full grid-rows-5">
|
||||||
|
<div className="row-span-1 flex items-center justify-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
|
||||||
|
Bringing back your profile...
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row-span-4 flex flex-col gap-8">
|
||||||
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<div className="mb-4 flex flex-col gap-2">
|
||||||
|
<div className="w-full rounded-lg bg-zinc-900 p-4 shadow-input ring-1 ring-zinc-800">
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<div className="relative h-10 w-10 rounded-full">
|
||||||
|
<Image className="inline-block rounded-full" src={profile?.picture} alt="" fill={true} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-4 py-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="font-semibold">{profile?.display_name || profile?.name}</p>
|
||||||
|
<span className="leading-tight text-zinc-500">·</span>
|
||||||
|
<p className="text-zinc-500">@{profile?.username || (pubkey && truncate(pubkey, 16, ' .... '))}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="col-span-2 h-2 rounded bg-zinc-700"></div>
|
||||||
|
<div className="col-span-1 h-2 rounded bg-zinc-700"></div>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 rounded bg-zinc-700"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
{done === false ? (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 animate-spin text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => submit()}
|
||||||
|
className="inline-flex w-full transform items-center justify-center rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
|
>
|
||||||
|
<span className="drop-shadow-lg">Done →</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page.getLayout = function getLayout(
|
||||||
|
page:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
||||||
|
| ReactFragment
|
||||||
|
| ReactPortal
|
||||||
|
) {
|
||||||
|
return <BaseLayout>{page}</BaseLayout>;
|
||||||
|
};
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
|
||||||
|
|
||||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
Page.getLayout = function getLayout(
|
|
||||||
page:
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
|
||||||
| ReactFragment
|
|
||||||
| ReactPortal
|
|
||||||
) {
|
|
||||||
return <BaseLayout>{page}</BaseLayout>;
|
|
||||||
};
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
import BaseLayout from '@layouts/base';
|
|
||||||
import UserLayout from '@layouts/user';
|
|
||||||
|
|
||||||
import { RelayContext } from '@components/contexts/relay';
|
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/getDate';
|
|
||||||
|
|
||||||
import { useLocalStorage } from '@rehooks/local-storage';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { getEventHash, signEvent } from 'nostr-tools';
|
|
||||||
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useState } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import Database from 'tauri-plugin-sql-api';
|
|
||||||
|
|
||||||
type FormValues = {
|
|
||||||
display_name: string;
|
|
||||||
name: string;
|
|
||||||
username: string;
|
|
||||||
picture: string;
|
|
||||||
banner: string;
|
|
||||||
about: string;
|
|
||||||
website: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: update the design
|
|
||||||
export default function Page() {
|
|
||||||
const relayPool: any = useContext(RelayContext);
|
|
||||||
const [relays]: any = useLocalStorage('relays');
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const [currentUser]: any = useLocalStorage('current-user');
|
|
||||||
const profile =
|
|
||||||
currentUser.metadata !== undefined ? JSON.parse(currentUser.metadata) : { display_name: null, username: null };
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isDirty, isValid },
|
|
||||||
} = useForm<FormValues>();
|
|
||||||
|
|
||||||
const onSubmit = async (data: any) => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
// publish account to relays
|
|
||||||
const event: any = {
|
|
||||||
content: JSON.stringify(data),
|
|
||||||
created_at: dateToUnix(),
|
|
||||||
kind: 0,
|
|
||||||
pubkey: currentUser.id,
|
|
||||||
tags: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
event.id = getEventHash(event);
|
|
||||||
event.sig = signEvent(event, currentUser.privkey);
|
|
||||||
|
|
||||||
relayPool.publish(event, relays);
|
|
||||||
|
|
||||||
// save account to database
|
|
||||||
const db = await Database.load('sqlite:lume.db');
|
|
||||||
await db.execute(`UPDATE accounts SET metadata = '${JSON.stringify(data)}' WHERE pubkey = "${currentUser.id}"`);
|
|
||||||
|
|
||||||
// set currentUser in global state
|
|
||||||
currentUser.set({
|
|
||||||
metadata: JSON.stringify(data),
|
|
||||||
npub: currentUser.npub,
|
|
||||||
privkey: currentUser.privkey,
|
|
||||||
pubkey: currentUser.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// redirect to newsfeed
|
|
||||||
setTimeout(() => {
|
|
||||||
setLoading(false);
|
|
||||||
router.reload();
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="flex h-full w-full flex-col justify-between px-6">
|
|
||||||
<div className="mb-8 flex flex-col gap-3 pt-8">
|
|
||||||
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
|
|
||||||
Update profile
|
|
||||||
</h1>
|
|
||||||
<h2 className="w-3/4 text-zinc-400">
|
|
||||||
Your profile will be published to all relays, as long as you have the private key, you always can recover your
|
|
||||||
profile in any client
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<fieldset className="flex flex-col gap-2">
|
|
||||||
<div className="grid grid-cols-4">
|
|
||||||
<div className="col-span-1">
|
|
||||||
<label className="text-zinc-300">Display Name</label>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-3 flex flex-col gap-2">
|
|
||||||
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
|
||||||
<input
|
|
||||||
{...register('display_name')}
|
|
||||||
defaultValue={profile.display_name || ''}
|
|
||||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-red-400">{errors.display_name && <p>{errors.display_name.message}</p>}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4">
|
|
||||||
<div className="col-span-1">
|
|
||||||
<label className="text-zinc-300">Name</label>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-3 flex flex-col gap-2">
|
|
||||||
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
|
||||||
<input
|
|
||||||
{...register('name')}
|
|
||||||
defaultValue={profile.name || ''}
|
|
||||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-red-400">{errors.name && <p>{errors.name.message}</p>}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4">
|
|
||||||
<div className="col-span-1">
|
|
||||||
<label className="text-zinc-300">Username</label>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-3 flex flex-col gap-2">
|
|
||||||
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
|
||||||
<input
|
|
||||||
{...register('username')}
|
|
||||||
defaultValue={profile.username || ''}
|
|
||||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-red-400">{errors.username && <p>{errors.username.message}</p>}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4">
|
|
||||||
<div className="col-span-1">
|
|
||||||
<label className="text-zinc-300">Profile Picture</label>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-3 flex flex-col gap-2">
|
|
||||||
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
|
||||||
<input
|
|
||||||
{...register('picture')}
|
|
||||||
defaultValue={profile.picture || ''}
|
|
||||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-red-400">{errors.picture && <p>{errors.picture.message}</p>}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4">
|
|
||||||
<div className="col-span-1">
|
|
||||||
<label className="text-zinc-300">Banner</label>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-3 flex flex-col gap-2">
|
|
||||||
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
|
||||||
<input
|
|
||||||
{...register('banner')}
|
|
||||||
defaultValue={profile.banner || ''}
|
|
||||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-red-400">{errors.banner && <p>{errors.banner.message}</p>}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4">
|
|
||||||
<div className="col-span-1">
|
|
||||||
<label className="text-zinc-300">About</label>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-3 flex flex-col gap-2">
|
|
||||||
<div className="relative h-24 shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
|
|
||||||
<textarea
|
|
||||||
{...register('about')}
|
|
||||||
defaultValue={profile.about || ''}
|
|
||||||
className="relative h-24 w-full resize-none rounded-lg border border-black/5 px-3.5 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-red-400">{errors.about && <p>{errors.about.message}</p>}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<div className="pb-5">
|
|
||||||
<div className="flex h-10 items-center">
|
|
||||||
{loading === true ? (
|
|
||||||
<svg
|
|
||||||
className="h-5 w-5 animate-spin text-white"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isDirty || !isValid}
|
|
||||||
className="transform rounded-lg bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
|
||||||
>
|
|
||||||
<span className="drop-shadow-lg">Update</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Page.getLayout = function getLayout(
|
|
||||||
page:
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
|
||||||
| ReactFragment
|
|
||||||
| ReactPortal
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<BaseLayout>
|
|
||||||
<UserLayout>{page}</UserLayout>
|
|
||||||
</BaseLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
69
src/pages/users/[id].tsx
Normal file
69
src/pages/users/[id].tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import BaseLayout from '@layouts/base';
|
||||||
|
import WithSidebarLayout from '@layouts/withSidebar';
|
||||||
|
|
||||||
|
import ProfileFollowers from '@components/profile/followers';
|
||||||
|
import ProfileFollows from '@components/profile/follows';
|
||||||
|
import ProfileMetadata from '@components/profile/metadata';
|
||||||
|
import ProfileNotes from '@components/profile/notes';
|
||||||
|
|
||||||
|
import * as Tabs from '@radix-ui/react-tabs';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const router = useRouter();
|
||||||
|
const id: any = router.query.id || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="scrollbar-hide h-full w-full overflow-y-auto">
|
||||||
|
<ProfileMetadata id={id} />
|
||||||
|
<Tabs.Root className="flex w-full flex-col" defaultValue="notes">
|
||||||
|
<Tabs.List className="flex border-b border-zinc-800">
|
||||||
|
<Tabs.Trigger
|
||||||
|
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium leading-none text-zinc-400 outline-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
|
||||||
|
value="notes"
|
||||||
|
>
|
||||||
|
Notes
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger
|
||||||
|
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium text-zinc-400 outline-none placeholder:leading-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
|
||||||
|
value="followers"
|
||||||
|
>
|
||||||
|
Followers
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger
|
||||||
|
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium leading-none text-zinc-400 outline-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
|
||||||
|
value="following"
|
||||||
|
>
|
||||||
|
Following
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Content value="notes">
|
||||||
|
<ProfileNotes id={id} />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="followers">
|
||||||
|
<ProfileFollowers id={id} />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="following">
|
||||||
|
<ProfileFollows id={id} />
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Page.getLayout = function getLayout(
|
||||||
|
page:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
|
||||||
|
| ReactFragment
|
||||||
|
| ReactPortal
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<BaseLayout>
|
||||||
|
<WithSidebarLayout>{page}</WithSidebarLayout>
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
9
src/stores/account.tsx
Normal file
9
src/stores/account.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { isSSR } from '@utils/ssr';
|
||||||
|
import { getActiveAccount } from '@utils/storage';
|
||||||
|
|
||||||
|
import { atomWithCache } from 'jotai-cache';
|
||||||
|
|
||||||
|
export const activeAccountAtom = atomWithCache(async () => {
|
||||||
|
const response = isSSR ? {} : await getActiveAccount();
|
||||||
|
return response;
|
||||||
|
});
|
||||||
24
src/stores/note.tsx
Normal file
24
src/stores/note.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { isSSR } from '@utils/ssr';
|
||||||
|
import { getAllNotes } from '@utils/storage';
|
||||||
|
|
||||||
|
import { atom } from 'jotai';
|
||||||
|
import { atomsWithQuery } from 'jotai-tanstack-query';
|
||||||
|
import { atomWithReset } from 'jotai/utils';
|
||||||
|
|
||||||
|
// note content
|
||||||
|
export const noteContentAtom = atomWithReset('');
|
||||||
|
// notify user that connector has receive newer note
|
||||||
|
export const hasNewerNoteAtom = atom(false);
|
||||||
|
// query notes from database
|
||||||
|
export const [notesAtom] = atomsWithQuery(() => ({
|
||||||
|
queryKey: ['notes'],
|
||||||
|
queryFn: async ({ queryKey: [] }) => {
|
||||||
|
const res = isSSR ? [] : await getAllNotes();
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
refetchInterval: 1000000,
|
||||||
|
refetchOnReconnect: true,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
refetchOnMount: true,
|
||||||
|
keepPreviousData: false,
|
||||||
|
}));
|
||||||
9
src/stores/relays.tsx
Normal file
9
src/stores/relays.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { isSSR } from '@utils/ssr';
|
||||||
|
import { getAllRelays } from '@utils/storage';
|
||||||
|
|
||||||
|
import { atomWithCache } from 'jotai-cache';
|
||||||
|
|
||||||
|
export const relaysAtom = atomWithCache(async () => {
|
||||||
|
const response = isSSR ? [] : await getAllRelays();
|
||||||
|
return response;
|
||||||
|
});
|
||||||
1
src/utils/ssr.tsx
Normal file
1
src/utils/ssr.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const isSSR = typeof window === 'undefined';
|
||||||
165
src/utils/storage.tsx
Normal file
165
src/utils/storage.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { getParentID } from '@utils/transform';
|
||||||
|
|
||||||
|
import Database from 'tauri-plugin-sql-api';
|
||||||
|
|
||||||
|
let db: null | Database = null;
|
||||||
|
|
||||||
|
// connect database (sqlite)
|
||||||
|
// path: tauri::api::path::BaseDirectory::App
|
||||||
|
export async function connect(): Promise<Database> {
|
||||||
|
if (db) {
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
db = await Database.load('sqlite:lume.db');
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all relays
|
||||||
|
export async function getAllRelays() {
|
||||||
|
const db = await connect();
|
||||||
|
const result: any = await db.select('SELECT relay_url FROM relays WHERE relay_status = "1";');
|
||||||
|
return result.reduce((relays, { relay_url }) => {
|
||||||
|
relays.push(relay_url);
|
||||||
|
return relays;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get active account
|
||||||
|
export async function getActiveAccount() {
|
||||||
|
const db = await connect();
|
||||||
|
const result = await db.select(`SELECT * FROM accounts LIMIT 1;`);
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all accounts
|
||||||
|
export async function getAccounts() {
|
||||||
|
const db = await connect();
|
||||||
|
return await db.select(`SELECT * FROM accounts`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all follows by account id
|
||||||
|
export async function getAllFollowsByID(id) {
|
||||||
|
const db = await connect();
|
||||||
|
return await db.select(`SELECT pubkey FROM follows WHERE account = "${id}";`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create account
|
||||||
|
export async function createAccount(data) {
|
||||||
|
const db = await connect();
|
||||||
|
return await db.execute(
|
||||||
|
'INSERT OR IGNORE INTO accounts (id, privkey, npub, nsec, metadata) VALUES (?, ?, ?, ?, ?);',
|
||||||
|
[data.pubkey, data.privkey, data.npub, data.nsec, data.metadata]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create follow
|
||||||
|
export async function createFollow(pubkey, account, kind) {
|
||||||
|
const db = await connect();
|
||||||
|
return await db.execute('INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES (?, ?, ?);', [
|
||||||
|
pubkey,
|
||||||
|
account,
|
||||||
|
kind || 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create follow
|
||||||
|
export async function createFollows(data, account, kind) {
|
||||||
|
const db = await connect();
|
||||||
|
data.forEach(async (item) => {
|
||||||
|
await db.execute('INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES (?, ?, ?);', [
|
||||||
|
item,
|
||||||
|
account,
|
||||||
|
kind || 0,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
// create cache profile
|
||||||
|
export async function createCacheProfile(id, metadata) {
|
||||||
|
const db = await connect();
|
||||||
|
return await db.execute('INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES (?, ?);', [id, metadata]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get cache profile
|
||||||
|
export async function getCacheProfile(id) {
|
||||||
|
const db = await connect();
|
||||||
|
const result = await db.select(`SELECT metadata FROM cache_profiles WHERE id = "${id}"`);
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all notes
|
||||||
|
export async function getAllNotes() {
|
||||||
|
const db = await connect();
|
||||||
|
return await db.select(`SELECT * FROM cache_notes GROUP BY parent_id ORDER BY created_at DESC LIMIT 500`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get note by id
|
||||||
|
export async function getNoteByID(id) {
|
||||||
|
const db = await connect();
|
||||||
|
const result = await db.select(`SELECT * FROM cache_notes WHERE id = "${id}"`);
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// create cache note
|
||||||
|
export async function createCacheNote(data) {
|
||||||
|
const db = await connect();
|
||||||
|
return await db.execute(
|
||||||
|
'INSERT OR IGNORE INTO cache_notes (id, pubkey, created_at, kind, content, tags, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?);',
|
||||||
|
[
|
||||||
|
data.id,
|
||||||
|
data.pubkey,
|
||||||
|
data.created_at,
|
||||||
|
data.kind,
|
||||||
|
data.content,
|
||||||
|
JSON.stringify(data.tags),
|
||||||
|
getParentID(data.tags, data.id),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all comment notes
|
||||||
|
export async function getAllCommentNotes(eid) {
|
||||||
|
const db = await connect();
|
||||||
|
return await db.select(
|
||||||
|
`SELECT * FROM cache_notes WHERE parent_comment_id = "${eid}" ORDER BY created_at DESC LIMIT 500`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create cache comment note
|
||||||
|
export async function createCacheCommentNote(data, eid) {
|
||||||
|
const db = await connect();
|
||||||
|
return await db.execute(
|
||||||
|
'INSERT OR IGNORE INTO cache_notes (id, pubkey, created_at, kind, content, tags, parent_id, parent_comment_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?);',
|
||||||
|
[
|
||||||
|
data.id,
|
||||||
|
data.pubkey,
|
||||||
|
data.created_at,
|
||||||
|
data.kind,
|
||||||
|
data.content,
|
||||||
|
JSON.stringify(data.tags),
|
||||||
|
getParentID(data.tags, data.id),
|
||||||
|
eid,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create cache comment note
|
||||||
|
export async function countTotalNotes() {
|
||||||
|
const db = await connect();
|
||||||
|
const result = await db.select('SELECT COUNT(*) AS "total" FROM cache_notes;');
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// get last login time
|
||||||
|
export async function getLastLoginTime() {
|
||||||
|
const db = await connect();
|
||||||
|
const result = await db.select('SELECT setting_value FROM settings WHERE setting_key = "last_login"');
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// update last login time
|
||||||
|
export async function updateLastLoginTime(time) {
|
||||||
|
const db = await connect();
|
||||||
|
return await db.execute(`UPDATE settings SET setting_value = "${time}" WHERE setting_key = "last_login"`);
|
||||||
|
}
|
||||||
38
src/utils/transform.tsx
Normal file
38
src/utils/transform.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import destr from 'destr';
|
||||||
|
|
||||||
|
export const tagsToArray = (arr) => {
|
||||||
|
const newarr = [];
|
||||||
|
// push item to newarr
|
||||||
|
arr.forEach((item) => {
|
||||||
|
newarr.push(item[1]);
|
||||||
|
});
|
||||||
|
return newarr;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pubkeyArray = (arr) => {
|
||||||
|
const newarr = [];
|
||||||
|
// push item to newarr
|
||||||
|
arr.forEach((item) => {
|
||||||
|
newarr.push(item.pubkey);
|
||||||
|
});
|
||||||
|
return newarr;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getParentID = (arr, fallback) => {
|
||||||
|
const tags = destr(arr);
|
||||||
|
let parentID = fallback;
|
||||||
|
|
||||||
|
if (tags.length > 0) {
|
||||||
|
if (tags[0][0] === 'e' || tags[0][2] === 'root' || tags[0][3] === 'root') {
|
||||||
|
parentID = tags[0][1];
|
||||||
|
} else {
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
if (tag[0] === 'e' && (tag[2] === 'root' || tag[3] === 'root')) {
|
||||||
|
parentID = tag[1];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentID;
|
||||||
|
};
|
||||||
@@ -28,6 +28,12 @@ module.exports = {
|
|||||||
0 2px 2px rgb(4 4 7 / 45%),
|
0 2px 2px rgb(4 4 7 / 45%),
|
||||||
0 8px 24px rgb(4 4 7 / 60%)
|
0 8px 24px rgb(4 4 7 / 60%)
|
||||||
`,
|
`,
|
||||||
|
button: `
|
||||||
|
rgba(112, 26, 117, 0.5) 0px 2px 8px,
|
||||||
|
rgb(112, 26, 117) 0px 2px 4px,
|
||||||
|
rgb(112, 26, 117) 0px 0px 0px 1px,
|
||||||
|
rgba(255, 255, 255, 0.2) 0px 0px 0px 1px inset
|
||||||
|
`,
|
||||||
},
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
'near-black': '#07070d',
|
'near-black': '#07070d',
|
||||||
@@ -35,6 +41,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
'gradient-conic': 'conic-gradient(var(--tw-gradient-stops))',
|
'gradient-conic': 'conic-gradient(var(--tw-gradient-stops))',
|
||||||
|
fade: 'linear-gradient(120deg, #000, transparent 30%, transparent 70%, #000)',
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
disco: {
|
disco: {
|
||||||
@@ -65,6 +72,14 @@ module.exports = {
|
|||||||
from: { opacity: 0, transform: 'translateX(2px)' },
|
from: { opacity: 0, transform: 'translateX(2px)' },
|
||||||
to: { opacity: 1, transform: 'translateX(0)' },
|
to: { opacity: 1, transform: 'translateX(0)' },
|
||||||
},
|
},
|
||||||
|
moveBg: {
|
||||||
|
'0%': { backgroundPosition: '50px' },
|
||||||
|
'20%': { backgroundPosition: '150px' },
|
||||||
|
'40%': { backgroundPosition: '250px' },
|
||||||
|
'60%': { backgroundPosition: '350px' },
|
||||||
|
'80%': { backgroundPosition: '450px' },
|
||||||
|
'100%': { backgroundPosition: '550px' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
disco: 'disco 1.5s linear infinite',
|
disco: 'disco 1.5s linear infinite',
|
||||||
@@ -74,6 +89,7 @@ module.exports = {
|
|||||||
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
moveBg: 'moveBg 3s ease-in-out infinite alternate running forwards',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"@pages/*": ["src/pages/*"],
|
"@pages/*": ["src/pages/*"],
|
||||||
"@layouts/*": ["src/layouts/*"],
|
"@layouts/*": ["src/layouts/*"],
|
||||||
"@components/*": ["src/components/*"],
|
"@components/*": ["src/components/*"],
|
||||||
|
"@stores/*": ["src/stores/*"],
|
||||||
"@utils/*": ["src/utils/*"],
|
"@utils/*": ["src/utils/*"],
|
||||||
"@assets/*": ["src/assets/*"]
|
"@assets/*": ["src/assets/*"]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user