Merge pull request #66 from luminous-devs/v1.2.0

prepare for v1.2.0
This commit is contained in:
Ren Amamiya 2023-08-07 09:08:56 +07:00 committed by GitHub
commit ed759086c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
154 changed files with 3338 additions and 3298 deletions

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
/**/node_modules/*
node_modules/
dist/

View File

@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lume</title>
</head>
<body class="cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen dark:bg-black dark:text-zinc-100">
<body class="relative cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen text-white">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

View File

@ -18,13 +18,18 @@
},
"dependencies": {
"@headlessui/react": "^1.7.16",
"@nostr-dev-kit/ndk": "^0.7.7",
"@noble/ciphers": "^0.2.0",
"@noble/curves": "^1.1.0",
"@noble/hashes": "^1.3.1",
"@nostr-dev-kit/ndk": "^0.8.3",
"@nostr-fetch/adapter-ndk": "^0.11.0",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-tooltip": "^1.0.6",
"@tanstack/react-query": "^4.32.0",
"@tanstack/react-query-devtools": "^4.32.0",
"@scure/base": "^1.1.1",
"@tanstack/react-query": "^4.32.6",
"@tanstack/react-query-devtools": "^4.32.6",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "2.0.0-alpha.5",
"@tauri-apps/plugin-app": "github:tauri-apps/tauri-plugin-app#v2",
@ -36,7 +41,9 @@
"@tauri-apps/plugin-notification": "github:tauri-apps/tauri-plugin-notification#v2",
"@tauri-apps/plugin-os": "github:tauri-apps/tauri-plugin-os#v2",
"@tauri-apps/plugin-process": "github:tauri-apps/tauri-plugin-process#v2",
"@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2",
"@tauri-apps/plugin-sql": "github:tauri-apps/tauri-plugin-sql#v2",
"@tauri-apps/plugin-store": "github:tauri-apps/tauri-plugin-store#v2",
"@tauri-apps/plugin-stronghold": "github:tauri-apps/tauri-plugin-stronghold#v2",
"@tauri-apps/plugin-upload": "github:tauri-apps/tauri-plugin-upload#v2",
"@tauri-apps/plugin-window": "2.0.0-alpha.0",
@ -55,12 +62,14 @@
"html-to-text": "^9.0.5",
"immer": "^10.0.2",
"light-bolt11-decoder": "^3.0.0",
"lru-cache": "^10.0.0",
"million": "2.5.4-beta.2",
"nostr-fetch": "^0.12.2",
"nostr-tools": "^1.13.1",
"nostr-tools": "^1.14.0",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.45.2",
"react-hook-form": "^7.45.4",
"react-hotkeys-hook": "^4.4.1",
"react-markdown": "^8.0.7",
"react-player": "^2.12.0",
@ -70,15 +79,15 @@
"remark-gfm": "^3.0.1",
"tauri-controls": "^0.0.5",
"tippy.js": "^6.3.7",
"zustand": "^4.3.9"
"zustand": "^4.4.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@tauri-apps/cli": "2.0.0-alpha.10",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/html-to-text": "^9.0.1",
"@types/node": "^18.17.1",
"@types/react": "^18.2.17",
"@types/node": "^18.17.3",
"@types/react": "^18.2.18",
"@types/react-dom": "^18.2.7",
"@types/youtube-player": "^5.5.7",
"@typescript-eslint/eslint-plugin": "^5.62.0",
@ -90,9 +99,9 @@
"csstype": "^3.1.2",
"encoding": "^0.1.13",
"eslint": "^8.46.0",
"eslint-config-prettier": "^8.9.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react": "^7.33.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.3",
@ -103,7 +112,7 @@
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3",
"typescript": "^4.9.5",
"vite": "^4.4.7",
"vite": "^4.4.8",
"vite-plugin-top-level-await": "^1.3.1",
"vite-tsconfig-paths": "^4.2.0"
}

File diff suppressed because it is too large Load Diff

BIN
public/clapping_hands.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

BIN
public/clown_face.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/crying_face.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/face_with_tongue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

BIN
public/lume.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

358
src-tauri/Cargo.lock generated
View File

@ -182,7 +182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854"
dependencies = [
"clipboard-win",
"core-graphics",
"core-graphics 0.22.3",
"image",
"log",
"objc",
@ -309,7 +309,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -326,7 +326,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -641,9 +641,12 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.79"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0"
dependencies = [
"libc",
]
[[package]]
name = "cesu8"
@ -757,7 +760,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -787,8 +790,24 @@ dependencies = [
"block",
"cocoa-foundation",
"core-foundation",
"core-graphics",
"foreign-types",
"core-graphics 0.22.3",
"foreign-types 0.3.2",
"libc",
"objc",
]
[[package]]
name = "cocoa"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
dependencies = [
"bitflags 1.3.2",
"block",
"cocoa-foundation",
"core-foundation",
"core-graphics 0.23.1",
"foreign-types 0.5.0",
"libc",
"objc",
]
@ -803,7 +822,7 @@ dependencies = [
"block",
"core-foundation",
"core-graphics-types",
"foreign-types",
"foreign-types 0.3.2",
"libc",
"objc",
]
@ -901,7 +920,20 @@ dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-graphics-types",
"foreign-types",
"foreign-types 0.3.2",
"libc",
]
[[package]]
name = "core-graphics"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-graphics-types",
"foreign-types 0.5.0",
"libc",
]
@ -1036,7 +1068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -1092,7 +1124,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -1103,7 +1135,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -1117,6 +1149,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "deranged"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929"
dependencies = [
"serde",
]
[[package]]
name = "derivative"
version = "2.2.0"
@ -1317,7 +1358,7 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -1328,9 +1369,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f"
dependencies = [
"errno-dragonfly",
"libc",
@ -1396,7 +1437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
dependencies = [
"cfg-if",
"rustix 0.38.4",
"rustix 0.38.7",
"windows-sys 0.48.0",
]
@ -1421,13 +1462,13 @@ dependencies = [
[[package]]
name = "filetime"
version = "0.2.21"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.2.16",
"redox_syscall 0.3.5",
"windows-sys 0.48.0",
]
@ -1465,7 +1506,28 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
"foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared 0.3.1",
]
[[package]]
name = "foreign-types-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.28",
]
[[package]]
@ -1474,6 +1536,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "form_urlencoded"
version = "1.2.0"
@ -1575,7 +1643,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -1874,9 +1942,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
version = "0.4.12"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006"
checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d"
dependencies = [
"aho-corasick",
"bstr",
@ -2224,6 +2292,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
"serde",
]
[[package]]
@ -2314,6 +2383,15 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-terminal"
version = "0.4.9"
@ -2321,10 +2399,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi",
"rustix 0.38.4",
"rustix 0.38.7",
"windows-sys 0.48.0",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "itertools"
version = "0.10.5"
@ -2523,9 +2611,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "linux-raw-sys"
version = "0.4.3"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0"
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
[[package]]
name = "lock_api"
@ -2562,7 +2650,7 @@ dependencies = [
name = "lume"
version = "1.1.1"
dependencies = [
"cocoa",
"cocoa 0.24.1",
"objc",
"rand 0.8.5",
"rust-argon2",
@ -2580,12 +2668,16 @@ dependencies = [
"tauri-plugin-notification",
"tauri-plugin-os",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-sql",
"tauri-plugin-store",
"tauri-plugin-stronghold",
"tauri-plugin-updater",
"tauri-plugin-upload",
"tauri-plugin-window",
"window-shadows",
"window-vibrancy",
]
[[package]]
@ -2996,6 +3088,17 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "open"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a083c0c7e5e4a8ec4176346cf61f67ac674e8bfb059d9226e1c54a96b377c12"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "ordered-stream"
version = "0.2.0"
@ -3017,6 +3120,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "os_pipe"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177"
dependencies = [
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -3084,6 +3197,12 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pbkdf2"
version = "0.11.0"
@ -3223,7 +3342,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -3589,13 +3708,13 @@ dependencies = [
[[package]]
name = "regex"
version = "1.9.1"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.3.4",
"regex-automata 0.3.6",
"regex-syntax 0.7.4",
]
@ -3610,9 +3729,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.3.4"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294"
checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69"
dependencies = [
"aho-corasick",
"memchr",
@ -3785,22 +3904,22 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.4"
version = "0.38.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399"
dependencies = [
"bitflags 2.3.3",
"errno",
"libc",
"linux-raw-sys 0.4.3",
"linux-raw-sys 0.4.5",
"windows-sys 0.48.0",
]
[[package]]
name = "rustls"
version = "0.21.5"
version = "0.21.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36"
checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb"
dependencies = [
"ring",
"rustls-webpki",
@ -3930,22 +4049,22 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.178"
version = "1.0.181"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60363bdd39a7be0266a520dab25fdc9241d2f987b08a01e01f0ec6d06a981348"
checksum = "6d3e73c93c3240c0bda063c239298e633114c69a888c3e37ca8bb33f343e9890"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.178"
version = "1.0.181"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28482318d6641454cb273da158647922d1be6b5a2fcc6165cd89ebdd7ed576b"
checksum = "be02f6cb0cd3a5ec20bbcfbcbd749f57daddb1a0882dc2e46a6c236c90b977ed"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -3967,7 +4086,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -3993,14 +4112,15 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.1.0"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3"
checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49"
dependencies = [
"base64 0.21.2",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.0.0",
"serde",
"serde_json",
"serde_with_macros",
@ -4009,14 +4129,14 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.1.0"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65"
checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -4095,6 +4215,16 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shared_child"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "signal-hook"
version = "0.3.17"
@ -4610,9 +4740,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.27"
version = "2.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0"
checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
dependencies = [
"proc-macro2",
"quote",
@ -4651,9 +4781,9 @@ dependencies = [
"bitflags 1.3.2",
"cairo-rs",
"cc",
"cocoa",
"cocoa 0.24.1",
"core-foundation",
"core-graphics",
"core-graphics 0.22.3",
"crossbeam-channel",
"dirs-next",
"dispatch",
@ -4715,9 +4845,9 @@ dependencies = [
[[package]]
name = "target-lexicon"
version = "0.12.10"
version = "0.12.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e"
checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a"
[[package]]
name = "tauri"
@ -4727,7 +4857,7 @@ checksum = "2e18377a75e314aa1d476896af881ed63f57a78ca84889fe63e69067f0de158d"
dependencies = [
"anyhow",
"bytes",
"cocoa",
"cocoa 0.24.1",
"dirs-next",
"embed_plist",
"futures-util",
@ -4829,7 +4959,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-app"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"tauri",
]
@ -4837,7 +4967,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-autostart"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"auto-launch",
"log",
@ -4850,7 +4980,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-clipboard-manager"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"arboard",
"log",
@ -4864,7 +4994,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"glib",
"log",
@ -4881,7 +5011,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"anyhow",
"glob",
@ -4894,7 +5024,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-http"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"bytes",
"glob",
@ -4912,7 +5042,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-notification"
version = "2.0.0-alpha.1"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"log",
"notify-rust",
@ -4930,7 +5060,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-os"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"gethostname 0.4.3",
"log",
@ -4946,15 +5076,32 @@ dependencies = [
[[package]]
name = "tauri-plugin-process"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"tauri",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"encoding_rs",
"log",
"open",
"os_pipe",
"regex",
"serde",
"serde_json",
"shared_child",
"tauri",
"thiserror",
]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"log",
"serde",
@ -4968,7 +5115,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-sql"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"futures-core",
"log",
@ -4981,10 +5128,22 @@ dependencies = [
"tokio",
]
[[package]]
name = "tauri-plugin-store"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"log",
"serde",
"serde_json",
"tauri",
"thiserror",
]
[[package]]
name = "tauri-plugin-stronghold"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"hex",
"iota-crypto 0.23.0",
@ -5000,7 +5159,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-updater"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"base64 0.21.2",
"dirs-next",
@ -5027,7 +5186,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-upload"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"futures-util",
"log",
@ -5044,7 +5203,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-window"
version = "2.0.0-alpha.0"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6f01bc11ab5be762d6cbe0ca924f15cdde47ce0d"
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#7f2e2dd5b8af2c8e7e224bed132e2b76da983818"
dependencies = [
"serde",
"tauri",
@ -5078,7 +5237,7 @@ version = "0.13.0-alpha.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1231be42085f3a8b150e615601f8a070bd16bf579771a5dafe2931970a05b518"
dependencies = [
"cocoa",
"cocoa 0.24.1",
"gtk",
"jni",
"percent-encoding",
@ -5151,7 +5310,7 @@ dependencies = [
"cfg-if",
"fastrand 2.0.0",
"redox_syscall 0.3.5",
"rustix 0.38.4",
"rustix 0.38.7",
"windows-sys 0.48.0",
]
@ -5189,7 +5348,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -5215,10 +5374,11 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.23"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea"
dependencies = [
"deranged",
"itoa 1.0.9",
"serde",
"time-core",
@ -5233,9 +5393,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.10"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd"
dependencies = [
"time-core",
]
@ -5281,7 +5441,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -5370,7 +5530,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]
@ -5633,7 +5793,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
"wasm-bindgen-shared",
]
@ -5667,7 +5827,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -5844,6 +6004,28 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "window-shadows"
version = "0.2.1"
source = "git+https://github.com/tauri-apps/window-shadows?branch=dev#dd8ca283fa50b1484043e686fa0b1e45ba5a969d"
dependencies = [
"cocoa 0.25.0",
"objc",
"raw-window-handle",
"windows-sys 0.48.0",
]
[[package]]
name = "window-vibrancy"
version = "0.4.0"
source = "git+https://github.com/tauri-apps/window-vibrancy?branch=dev#d1b349f3c3f524ffd3ba37b64349ec6ad6410272"
dependencies = [
"cocoa 0.25.0",
"objc",
"raw-window-handle",
"windows-sys 0.48.0",
]
[[package]]
name = "windows"
version = "0.36.1"
@ -6128,9 +6310,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.5.1"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25b5872fa2e10bd067ae946f927e726d7d603eaeb6e02fa6a350e0722d2b8c11"
checksum = "acaaa1190073b2b101e15083c38ee8ec891b5e05cbee516521e94ec008f61e64"
dependencies = [
"memchr",
]
@ -6162,8 +6344,8 @@ checksum = "7d15f9f827d537cefe6d047be3930f5d89b238dfb85e08ba6a319153217635aa"
dependencies = [
"base64 0.13.1",
"block",
"cocoa",
"core-graphics",
"cocoa 0.24.1",
"core-graphics 0.22.3",
"crossbeam-channel",
"dunce",
"gdk",
@ -6349,7 +6531,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
"syn 2.0.28",
]
[[package]]

View File

@ -16,7 +16,10 @@ tauri-build = { version = "2.0.0-alpha.6", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2.0.0-alpha.10", features = ["system-tray"] }
tauri = { version = "2.0.0-alpha.10", features = [
"macos-private-api",
"system-tray",
] }
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-stronghold = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
@ -31,6 +34,10 @@ tauri-plugin-app = { git = "https://github.com/tauri-apps/plugins-workspace", br
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-window = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
window-shadows = { git = "https://github.com/tauri-apps/window-shadows", branch = "dev" }
window-vibrancy = { git = "https://github.com/tauri-apps/window-vibrancy", branch = "dev" }
sqlx-cli = { version = "0.7.0", default-features = false, features = [
"sqlite",
] }

View File

@ -0,0 +1,2 @@
-- Add migration script here
ALTER TABLE accounts ADD network JSON;

View File

@ -3,19 +3,12 @@
windows_subsystem = "windows"
)]
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
// use rand::distributions::{Alphanumeric, DistString};
use tauri::{Manager, WindowEvent};
use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_sql::{Migration, MigrationKind};
#[cfg(target_os = "macos")]
use window_ext::WindowExt;
#[cfg(target_os = "macos")]
mod window_ext;
use window_shadows::set_shadow;
use window_vibrancy::{apply_mica, apply_vibrancy, NSVisualEffectMaterial};
#[derive(Clone, serde::Serialize)]
struct Payload {
@ -23,6 +16,16 @@ struct Payload {
cwd: String,
}
#[tauri::command]
async fn close_splashscreen(window: tauri::Window) {
// Close splashscreen
if let Some(splashscreen) = window.get_window("splashscreen") {
splashscreen.close().unwrap();
}
// Show main window
window.get_window("main").unwrap().show().unwrap();
}
fn main() {
tauri::Builder::default()
.plugin(
@ -96,6 +99,12 @@ fn main() {
sql: include_str!("../migrations/20230725010250_update_default_relays.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230804083544,
description: "add network to accounts",
sql: include_str!("../migrations/20230804083544_add_network_to_account.sql"),
kind: MigrationKind::Up,
},
],
)
.build(),
@ -144,29 +153,25 @@ fn main() {
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_window::init())
.plugin(tauri_plugin_store::Builder::default().build())
.plugin(tauri_plugin_shell::init())
.setup(|app| {
#[cfg(target_os = "macos")]
let main_window = app.get_window("main").unwrap();
let window = app.get_window("main").unwrap();
// native shadow
set_shadow(&window, true).expect("Unsupported platform!");
#[cfg(target_os = "macos")]
main_window.position_traffic_lights(13.0, 17.0); // set inset for traffic lights (macos)
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, None)
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
#[cfg(target_os = "windows")]
apply_mica(&window, None, None)
.expect("Unsupported platform! 'apply_blur' is only supported on Windows");
Ok(())
})
.on_window_event(|e| {
#[cfg(target_os = "macos")]
let apply_offset = || {
let win = e.window();
// keep inset for traffic lights when window resize (macos)
win.position_traffic_lights(13.0, 17.0);
};
#[cfg(target_os = "macos")]
match e.event() {
WindowEvent::Resized(..) => apply_offset(),
WindowEvent::ThemeChanged(..) => apply_offset(),
_ => {}
}
})
.invoke_handler(tauri::generate_handler![close_splashscreen])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

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

View File

@ -94,18 +94,32 @@
},
"windows": [
{
"fullscreen": false,
"width": 1080,
"height": 800,
"hiddenTitle": true,
"minHeight": 720,
"minWidth": 1080,
"minHeight": 800,
"resizable": true,
"theme": "Dark",
"title": "Lume",
"titleBarStyle": "Overlay",
"transparent": false,
"width": 1080
"transparent": true,
"center": true,
"fullscreen": false,
"hiddenTitle": true,
"visible": false
},
{
"width": 400,
"height": 500,
"decorations": true,
"hiddenTitle": true,
"center": true,
"resizable": false,
"titleBarStyle": "Overlay",
"label": "splashscreen",
"url": "splashscreen"
}
]
],
"macOSPrivateApi": true
}
}
}

View File

@ -1,4 +1,4 @@
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { RouterProvider, createBrowserRouter, redirect } from 'react-router-dom';
import { AuthCreateScreen } from '@app/auth/create';
import { CreateStep1Screen } from '@app/auth/create/step-1';
@ -15,34 +15,63 @@ import { OnboardingScreen } from '@app/auth/onboarding';
import { ResetScreen } from '@app/auth/reset';
import { UnlockScreen } from '@app/auth/unlock';
import { WelcomeScreen } from '@app/auth/welcome';
import { ChannelScreen } from '@app/channel';
import { ChatScreen } from '@app/chats';
import { ErrorScreen } from '@app/error';
import { NoteScreen } from '@app/note';
import { Root } from '@app/root';
import { EventScreen } from '@app/events';
import { AccountSettingsScreen } from '@app/settings/account';
import { GeneralSettingsScreen } from '@app/settings/general';
import { ShortcutsSettingsScreen } from '@app/settings/shortcuts';
import { SpaceScreen } from '@app/space';
import { SplashScreen } from '@app/splash';
import { TrendingScreen } from '@app/trending';
import { UserScreen } from '@app/users';
import { getActiveAccount } from '@libs/storage';
import { AppLayout } from '@shared/appLayout';
import { AuthLayout } from '@shared/authLayout';
import { Protected } from '@shared/protected';
import { LoaderIcon } from '@shared/icons';
import { SettingsLayout } from '@shared/settingsLayout';
import './index.css';
const appLoader = async () => {
const account = await getActiveAccount();
const stronghold = sessionStorage.getItem('stronghold');
const privkey = JSON.parse(stronghold).state.privkey || null;
if (!account) {
return redirect('/auth/welcome');
}
if (account && account.privkey.length > 35) {
return redirect('/auth/migrate');
}
if (account && !privkey) {
return redirect('/auth/unlock');
}
return null;
};
const router = createBrowserRouter([
{
path: '/',
element: (
// @ts-expect-error, todo
<Protected>
<Root />
</Protected>
),
element: <AppLayout />,
errorElement: <ErrorScreen />,
loader: appLoader,
children: [
{ path: '', element: <SpaceScreen /> },
{ path: 'trending', element: <TrendingScreen /> },
{ path: 'events/:id', element: <EventScreen /> },
{ path: 'users/:pubkey', element: <UserScreen /> },
{ path: 'chats/:pubkey', element: <ChatScreen /> },
],
},
{
path: '/splashscreen',
element: <SplashScreen />,
errorElement: <ErrorScreen />,
},
{
@ -76,31 +105,9 @@ const router = createBrowserRouter([
{ path: 'reset', element: <ResetScreen /> },
],
},
{
path: '/app',
element: (
// @ts-expect-error, todo
<Protected>
<AppLayout />
</Protected>
),
children: [
{ path: 'space', element: <SpaceScreen /> },
{ path: 'trending', element: <TrendingScreen /> },
{ path: 'note/:id', element: <NoteScreen /> },
{ path: 'users/:pubkey', element: <UserScreen /> },
{ path: 'chats/:pubkey', element: <ChatScreen /> },
{ path: 'channel/:id', element: <ChannelScreen /> },
],
},
{
path: '/settings',
element: (
// @ts-expect-error, todo
<Protected>
<SettingsLayout />
</Protected>
),
element: <SettingsLayout />,
children: [
{ path: 'general', element: <GeneralSettingsScreen /> },
{ path: 'shortcuts', element: <ShortcutsSettingsScreen /> },
@ -113,7 +120,11 @@ export default function App() {
return (
<RouterProvider
router={router}
fallbackElement={<p>Loading..</p>}
fallbackElement={
<div className="flex h-full w-full items-center justify-center bg-black/90">
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
</div>
}
future={{ v7_startTransition: true }}
/>
);

View File

@ -11,10 +11,10 @@ export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }
if (status === 'loading') {
return (
<div className="flex items-center gap-2">
<div className="relative h-10 w-10 shrink-0 animate-pulse rounded-md bg-zinc-800" />
<div className="relative h-10 w-10 shrink-0 animate-pulse rounded-md bg-white/10" />
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
<span className="h-4 w-1/2 animate-pulse rounded bg-zinc-800" />
<span className="h-3 w-1/3 animate-pulse rounded bg-zinc-800" />
<span className="h-4 w-1/2 animate-pulse rounded bg-white/10" />
<span className="h-3 w-1/3 animate-pulse rounded bg-white/10" />
</div>
</div>
);
@ -31,10 +31,10 @@ export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }
/>
</div>
<div className="flex w-full flex-1 flex-col items-start text-start">
<span className="truncate font-medium leading-tight text-zinc-100">
<span className="truncate font-medium leading-tight text-white">
{user?.name || user?.displayName || user?.display_name}
</span>
<span className="max-w-[15rem] truncate text-base leading-tight text-zinc-400">
<span className="max-w-[15rem] truncate text-base leading-tight text-white/50">
{user?.nip05?.toLowerCase() || shortenKey(pubkey)}
</span>
</div>

View File

@ -37,13 +37,9 @@ export function CreateStep1Screen() {
};
const download = async () => {
await writeTextFile(
'lume-keys.txt',
`Public key: ${pubkey}\nPrivate key: ${privkey}`,
{
dir: BaseDirectory.Download,
}
);
await writeTextFile('lume-keys.txt', `Public key: ${npub}\nPrivate key: ${nsec}`, {
dir: BaseDirectory.Download,
});
setDownloaded(true);
};
@ -81,47 +77,39 @@ export function CreateStep1Screen() {
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">Save your access key!</h1>
<h1 className="text-xl font-semibold text-white">Save your access key!</h1>
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<span className="text-base font-semibold text-zinc-400">Public Key</span>
<span className="text-base font-semibold text-white/50">Public Key</span>
<input
readOnly
value={npub}
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none placeholder:text-white/50"
/>
</div>
<div className="flex flex-col gap-1">
<span className="text-base font-semibold text-zinc-400">Private Key</span>
<span className="text-base font-semibold text-white/50">Private Key</span>
<div className="relative">
<input
readOnly
type={privkeyInput}
value={nsec}
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
className="relative h-11 w-full rounded-lg bg-white/10 py-1 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
/>
<button
type="button"
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-white/10"
>
{privkeyInput === 'password' ? (
<EyeOffIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
) : (
<EyeOnIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
)}
</button>
</div>
<div className="mt-2 text-sm text-zinc-500">
<div className="mt-2 text-sm text-white/50">
<p>
Your private key is your password. If you lose this key, you will lose
access to your account! Copy it and keep it in a safe place. There is no way
@ -132,13 +120,15 @@ export function CreateStep1Screen() {
<div className="flex flex-col gap-2">
<Button preset="large" onClick={() => submit()}>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
) : (
'I have saved my key, continue →'
)}
</Button>
{downloaded ? (
<span className="text-sm text-zinc-400">Saved in download folder</span>
<span className="inline-flex h-11 w-full items-center justify-center text-sm text-white/50">
Saved in Download folder
</span>
) : (
<Button preset="large-alt" onClick={() => download()}>
Download

View File

@ -74,7 +74,7 @@ export function CreateStep2Screen() {
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">
<h1 className="text-xl font-semibold text-white">
Set password to secure your key
</h1>
</div>
@ -85,29 +85,21 @@ export function CreateStep2Screen() {
<input
{...register('password', { required: true })}
type={passwordInput}
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-center text-white !outline-none placeholder:text-white/50"
/>
<button
type="button"
onClick={() => showPassword()}
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-white/10"
>
{passwordInput === 'password' ? (
<EyeOffIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
) : (
<EyeOnIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
)}
</button>
</div>
<div className="text-sm text-zinc-500">
<div className="text-sm text-white/50">
<p>
Password is use to secure your key store in local machine, when you move
to other clients, you just need to copy your private key as nsec or
@ -122,10 +114,10 @@ export function CreateStep2Screen() {
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
) : (
'Continue →'
)}

View File

@ -45,22 +45,12 @@ export function CreateStep3Screen() {
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">Create your profile</h1>
<h1 className="text-xl font-semibold text-white">Create your profile</h1>
</div>
<div className="w-full overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="w-full overflow-hidden rounded-xl bg-white/10">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
<input
type={'hidden'}
{...register('picture')}
value={picture}
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
<input
type={'hidden'}
{...register('banner')}
value={banner}
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
<input type={'hidden'} {...register('picture')} value={picture} />
<input type={'hidden'} {...register('banner')} value={banner} />
<div className="relative">
<div className="relative h-44 w-full bg-zinc-800">
<Image
@ -79,7 +69,7 @@ export function CreateStep3Screen() {
src={picture}
fallback={DEFAULT_AVATAR}
alt="user's avatar"
className="h-14 w-14 rounded-lg object-cover ring-2 ring-zinc-900"
className="h-14 w-14 rounded-lg object-cover ring-2 ring-white/10"
/>
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
<AvatarUploader setPicture={setPicture} />
@ -91,7 +81,7 @@ export function CreateStep3Screen() {
<div className="flex flex-col gap-1">
<label
htmlFor="name"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
className="text-sm font-semibold uppercase tracking-wider text-white/50"
>
Name *
</label>
@ -102,26 +92,26 @@ export function CreateStep3Screen() {
minLength: 4,
})}
spellCheck={false}
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="about"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
className="text-sm font-semibold uppercase tracking-wider text-white/50"
>
Bio
</label>
<textarea
{...register('about')}
spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
className="relative h-20 w-full resize-none rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="website"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
className="text-sm font-semibold uppercase tracking-wider text-white/50"
>
Website
</label>
@ -131,16 +121,16 @@ export function CreateStep3Screen() {
required: false,
})}
spellCheck={false}
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
/>
</div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
) : (
'Continue →'
)}

View File

@ -53,10 +53,10 @@ export function CreateStep4Screen() {
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">Create your Lume ID</h1>
<h1 className="text-xl font-semibold text-white">Create your Lume ID</h1>
</div>
<div className="flex w-full flex-col items-center justify-center gap-4">
<div className="inline-flex w-full items-center justify-center gap-2 rounded-lg bg-zinc-800">
<div className="inline-flex w-full items-center justify-center gap-2 rounded-lg bg-white/10">
<input
type="text"
value={username}
@ -65,7 +65,7 @@ export function CreateStep4Screen() {
autoCorrect="none"
spellCheck="false"
placeholder="satoshi"
className="relative w-full bg-transparent py-3 pl-3.5 text-zinc-100 !outline-none placeholder:text-zinc-500"
className="relative h-11 w-full bg-transparent py-1 pl-3.5 text-white !outline-none placeholder:text-white/50"
/>
<span className="pr-3.5 font-semibold text-fuchsia-500">@lume.nu</span>
</div>
@ -75,7 +75,7 @@ export function CreateStep4Screen() {
disabled={username.length === 0}
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
) : (
'Continue →'
)}

View File

@ -138,7 +138,7 @@ export function CreateStep5Screen() {
const update = useMutation({
mutationFn: (follows: string[]) => {
return updateAccount('follows', follows, account.pubkey);
return updateAccount('follows', follows);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
@ -168,13 +168,11 @@ export function CreateStep5Screen() {
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">
Personalized your newsfeed
</h1>
<h1 className="text-xl font-semibold text-white">Personalized your newsfeed</h1>
</div>
<div className="flex flex-col gap-4">
<div className="w-full overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="inline-flex h-10 w-full items-center gap-1 border-b border-zinc-800 px-4 text-base font-medium text-zinc-400">
<div className="w-full overflow-hidden rounded-xl bg-white/10">
<div className="inline-flex h-10 w-full items-center gap-1 border-b border-white/10 px-4 text-base font-medium text-white/50">
Follow at least
<span className="font-semibold text-fuchsia-500">
{follows.length}/10
@ -183,7 +181,7 @@ export function CreateStep5Screen() {
</div>
{status === 'loading' ? (
<div className="inline-flex h-11 w-full items-center justify-center px-4 py-2">
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
</div>
) : (
<div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2">
@ -192,7 +190,7 @@ export function CreateStep5Screen() {
key={item.pubkey}
type="button"
onClick={() => toggleFollow(item.pubkey)}
className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1"
className="inline-flex transform items-center justify-between bg-white/10 px-4 py-2 hover:bg-white/20 active:translate-y-1"
>
<User pubkey={item.pubkey} fallback={item.profile?.content} />
{follows.includes(item.pubkey) && (
@ -209,10 +207,10 @@ export function CreateStep5Screen() {
<button
type="button"
onClick={() => submit()}
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600"
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
) : (
'Finish →'
)}

View File

@ -98,17 +98,17 @@ export function ImportStep1Screen() {
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">Import your key</h1>
<h1 className="text-xl font-semibold text-white">Import your key</h1>
</div>
<div className="flex flex-col gap-4">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
<div className="flex flex-col gap-1">
<span className="text-base font-semibold text-zinc-400">Private key</span>
<span className="text-base font-semibold text-white/50">Private key</span>
<input
{...register('privkey', { required: true, minLength: 32 })}
type={'password'}
placeholder="nsec or hexstring"
className="relative w-full rounded-lg bg-zinc-800 px-3 py-3 text-zinc-100 !outline-none placeholder:text-zinc-500"
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
/>
<span className="text-sm text-red-400">
{errors.privkey && <p>{errors.privkey.message}</p>}
@ -118,10 +118,10 @@ export function ImportStep1Screen() {
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
) : (
'Continue →'
)}

View File

@ -74,7 +74,7 @@ export function ImportStep2Screen() {
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">
<h1 className="text-xl font-semibold text-white">
Set password to secure your key
</h1>
</div>
@ -85,29 +85,21 @@ export function ImportStep2Screen() {
<input
{...register('password', { required: true })}
type={passwordInput}
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
className="relative h-11 w-full rounded-lg bg-white/10 px-3.5 py-1 text-center text-white !outline-none placeholder:text-white/50"
/>
<button
type="button"
onClick={() => showPassword()}
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-white/10"
>
{passwordInput === 'password' ? (
<EyeOffIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
) : (
<EyeOnIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
)}
</button>
</div>
<div className="text-sm text-zinc-500">
<div className="text-sm text-white/50">
<p>
Password is use to unlock app and secure your key store in local machine.
When you move to other clients, you just need to copy your private key as
@ -122,10 +114,10 @@ export function ImportStep2Screen() {
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
) : (
'Continue →'
)}

View File

@ -1,54 +1,42 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user';
import { useNDK } from '@libs/ndk/provider';
import { updateAccount } from '@libs/storage';
import { updateLastLogin } from '@libs/storage';
import { Button } from '@shared/button';
import { LoaderIcon } from '@shared/icons';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { useAccount } from '@utils/hooks/useAccount';
import { setToArray } from '@utils/transform';
import { useNostr } from '@utils/hooks/useNostr';
export function ImportStep3Screen() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const { ndk } = useNDK();
const { status, account } = useAccount();
const update = useMutation({
mutationFn: (follows: string[]) => {
return updateAccount('follows', follows, account.pubkey);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
},
});
const { fetchNotes, fetchChats } = useNostr();
const submit = async () => {
try {
// show loading indicator
setLoading(true);
const user = ndk.getUser({ hexpubkey: account.pubkey });
const follows = await user.follows();
const now = Math.floor(Date.now() / 1000);
await fetchNotes();
await fetchChats();
await updateLastLogin(now);
// follows as list
const followsList = setToArray(follows);
queryClient.invalidateQueries(['currentAccount']);
// update
update.mutate([...followsList, account.pubkey]);
// redirect to next step
setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1200);
} catch {
console.log('error');
navigate('/', { replace: true });
} catch (e) {
console.log('error: ', e);
setLoading(false);
}
};
@ -56,30 +44,42 @@ export function ImportStep3Screen() {
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold">
{loading ? 'Creating...' : 'Continue with'}
{loading ? 'Prefetching data...' : 'Continue with'}
</h1>
</div>
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-4">
<div className="w-full rounded-xl bg-white/10 p-4">
{status === 'loading' ? (
<div className="w-full">
<div className="flex items-center gap-2">
<div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" />
<div className="h-11 w-11 animate-pulse rounded-lg bg-white/10" />
<div>
<div className="mb-1 h-4 w-16 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-36 animate-pulse rounded bg-zinc-800" />
<div className="mb-1 h-4 w-16 animate-pulse rounded bg-white/10" />
<div className="h-3 w-36 animate-pulse rounded bg-white/10" />
</div>
</div>
</div>
) : (
<div className="flex flex-col gap-3">
<User pubkey={account.pubkey} />
<Button preset="large" onClick={() => submit()}>
<button
type="button"
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
onClick={() => submit()}
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<>
<span className="w-5" />
<span>It might take a bit, please patient...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : (
'Continue →'
<>
<span className="w-5" />
<span>Continue</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</Button>
</button>
</div>
)}
</div>

View File

@ -92,7 +92,7 @@ export function MigrateScreen() {
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">
<h1 className="text-xl font-semibold text-white">
Upgrade security for your account
</h1>
</div>
@ -100,15 +100,15 @@ export function MigrateScreen() {
<div className="flex flex-col gap-4">
<div>
<div className="mt-1">
<p className="text-sm text-zinc-400">
<p className="text-sm text-white/50">
You&apos;re using old Lume version which store your private key as
plaintext in database, this is huge security risk.
</p>
<p className="mt-2 text-sm text-zinc-400">
<p className="mt-2 text-sm text-white/50">
To secure your private key, please set a password and Lume will put your
private key in secure storage.
</p>
<p className="mt-2 text-sm text-zinc-400">
<p className="mt-2 text-sm text-white/50">
It is not possible to start the app without applying this step, it is
easy and fast!
</p>
@ -124,7 +124,7 @@ export function MigrateScreen() {
{...register('password', { required: true })}
type={passwordInput}
placeholder="min. 4 characters"
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
/>
<button
type="button"
@ -135,13 +135,13 @@ export function MigrateScreen() {
<EyeOffIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
className="text-white/50 group-hover:text-white"
/>
) : (
<EyeOnIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
className="text-white/50 group-hover:text-white"
/>
)}
</button>
@ -154,10 +154,10 @@ export function MigrateScreen() {
<button
type="submit"
disabled={!isDirty || !isValid}
className="mt-3 inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
className="mt-3 inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
) : (
'Continue →'
)}

View File

@ -38,7 +38,7 @@ export function OnboardingScreen() {
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md">
<div className="mb-4 text-center">
<h1 className="mb-2 text-xl font-semibold text-zinc-100">
<h1 className="mb-2 text-xl font-semibold text-white">
👋 Hello, welcome you to Lume
</h1>
<p className="text-sm text-zinc-300">
@ -54,7 +54,7 @@ export function OnboardingScreen() {
{status === 'success' && (
<User pubkey={account.pubkey} time={Math.floor(Date.now() / 1000)} />
)}
<div className="-mt-6 select-text whitespace-pre-line break-words pl-[49px] text-base text-zinc-100">
<div className="-mt-6 select-text whitespace-pre-line break-words pl-[49px] text-base text-white">
<p>Running Lume, join with me #nostr #lume</p>
<a
href="https://lume.nu"
@ -71,12 +71,12 @@ export function OnboardingScreen() {
<button
type="button"
onClick={() => submit()}
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium text-zinc-100 hover:bg-fuchsia-600"
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium text-white hover:bg-fuchsia-600"
>
{loading ? (
<>
<span className="w-5" />
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
<span className="w-5" />
</>
) : (

View File

@ -102,74 +102,64 @@ export function ResetScreen() {
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">Reset unlock password</h1>
<div className="mb-6 text-center">
<h1 className="text-2xl font-semibold text-white">Reset unlock password</h1>
</div>
<div className="flex flex-col gap-4">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
<div className="flex flex-col gap-1">
<label htmlFor="privkey" className="font-medium text-zinc-200">
Private key
</label>
<div className="relative">
<input
{...register('privkey', { required: true })}
type="text"
placeholder="nsec..."
className="relative w-full rounded-lg bg-zinc-800 px-3.5 py-3 text-zinc-100 !outline-none placeholder:text-zinc-400"
/>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
<div className="flex flex-col gap-1">
<label htmlFor="privkey" className="font-medium text-white/50">
Private key
</label>
<div className="relative">
<input
{...register('privkey', { required: true })}
type="text"
placeholder="nsec..."
className="relative h-12 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none placeholder:text-white/10"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="password" className="font-medium text-zinc-200">
Set a new password to protect your key
</label>
<div className="relative">
<input
{...register('password', { required: true })}
type={passwordInput}
placeholder="min. 4 characters"
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
/>
<button
type="button"
onClick={() => showPassword()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
>
{passwordInput === 'password' ? (
<EyeOffIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
) : (
<EyeOnIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
)}
</button>
</div>
<span className="text-sm text-red-400">
{errors.password && <p>{errors.password.message}</p>}
</span>
</div>
<div className="flex items-center justify-center">
</div>
<div className="flex flex-col gap-1">
<label htmlFor="password" className="font-medium text-white/50">
Set a new password to protect your key
</label>
<div className="relative">
<input
{...register('password', { required: true })}
type={passwordInput}
placeholder="min. 4 characters"
className="relative h-12 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none placeholder:text-white/10"
/>
<button
type="submit"
disabled={!isDirty || !isValid}
className="mt-3 inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
type="button"
onClick={() => showPassword()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-white/10"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
{passwordInput === 'password' ? (
<EyeOffIcon className="h-5 w-5 text-white/50 group-hover:text-white" />
) : (
'Continue →'
<EyeOnIcon className="h-5 w-5 text-white/50 group-hover:text-white" />
)}
</button>
</div>
</form>
</div>
<span className="text-sm text-red-400">
{errors.password && <p>{errors.password.message}</p>}
</span>
</div>
<div className="flex items-center justify-center">
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-12 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
) : (
'Continue →'
)}
</button>
</div>
</form>
</div>
</div>
);

View File

@ -81,65 +81,53 @@ export function UnlockScreen() {
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">
Enter password to unlock
</h1>
<div className="mb-6 text-center">
<h1 className="text-2xl font-semibold text-white">Enter password to unlock</h1>
</div>
<div className="flex flex-col gap-4">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<div className="relative">
<input
{...register('password', { required: true })}
type={passwordInput}
className="relative w-full rounded-lg bg-zinc-800 py-3 text-center text-zinc-100 !outline-none placeholder:text-zinc-400"
/>
<button
type="button"
onClick={() => showPassword()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
>
{passwordInput === 'password' ? (
<EyeOffIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
) : (
<EyeOnIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
)}
</button>
</div>
<span className="text-sm text-red-400">
{errors.password && <p>{errors.password.message}</p>}
</span>
</div>
<div className="flex flex-col items-center justify-center">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
<div className="flex flex-col gap-1">
<div className="relative">
<input
{...register('password', { required: true })}
type={passwordInput}
className="relative h-12 w-full rounded-lg bg-white/10 py-1 text-center text-white !outline-none placeholder:text-white/10"
/>
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600"
type="button"
onClick={() => showPassword()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-white/10"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
{passwordInput === 'password' ? (
<EyeOffIcon className="h-5 w-5 text-white/50 group-hover:text-white" />
) : (
'Continue →'
<EyeOnIcon className="h-5 w-5 text-white/50 group-hover:text-white" />
)}
</button>
<Link
to="/auth/reset"
className="inline-flex h-12 items-center justify-center text-center text-sm text-zinc-400"
>
Reset password
</Link>
</div>
</form>
</div>
<span className="text-sm text-red-400">
{errors.password && <p>{errors.password.message}</p>}
</span>
</div>
<div className="flex flex-col items-center justify-center">
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-12 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
) : (
'Continue →'
)}
</button>
<Link
to="/auth/reset"
className="inline-flex h-14 items-center justify-center text-center text-white/50"
>
Reset password
</Link>
</div>
</form>
</div>
</div>
);

View File

@ -1,29 +1,39 @@
import { LogicalSize, appWindow } from '@tauri-apps/plugin-window';
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
export function WelcomeScreen() {
useEffect(() => {
async function setWindow() {
await appWindow.setSize(new LogicalSize(400, 500));
await appWindow.setResizable(false);
}
setWindow();
return () => {
appWindow.setSize(new LogicalSize(1080, 800)).then(() => {
appWindow.setResizable(false);
appWindow.center();
});
};
}, []);
return (
<div className="grid h-full w-full grid-cols-12 gap-4 px-4 py-4">
<div className="col-span-5 flex flex-col rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="flex h-full w-full flex-col justify-center gap-2 px-4 py-4">
<h1 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
Preserve your <span className="text-fuchsia-300">freedom</span>
</h1>
<h2 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
Protect your <span className="text-red-300">future</span>
</h2>
<h3 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
Stack <span className="text-orange-300">bitcoin</span>
</h3>
<h3 className="text-4xl font-bold leading-none text-transparent text-zinc-700">
Use <span className="text-purple-300">nostr</span>
<div className="flex h-screen w-full flex-col justify-between bg-white/10">
<div className="flex flex-col gap-10 pt-16">
<div className="flex flex-col gap-2 text-center">
<h1 className="text-3xl font-medium text-white">Welcome to Lume</h1>
<h3 className="mx-auto w-2/3 text-white/50">
Let&apos;s get you up and connecting with all peoples around the world on
Nostr
</h3>
</div>
<div className="mt-auto flex w-full flex-col gap-2 px-4 py-4">
<div className="inline-flex w-full flex-col items-center gap-3 px-4 pb-10">
<Link
to="/auth/import"
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium text-zinc-100 hover:bg-fuchsia-600"
className="inline-flex h-11 w-2/3 items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
>
<span className="w-5" />
<span>Login with private key</span>
@ -31,24 +41,15 @@ export function WelcomeScreen() {
</Link>
<Link
to="/auth/create"
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-zinc-800 px-6 font-medium text-zinc-200 hover:bg-zinc-700"
className="inline-flex h-11 w-2/3 items-center justify-center gap-2 rounded-lg bg-white/10 px-6 font-medium leading-none text-zinc-200 hover:bg-white/20 focus:outline-none"
>
Create new key
</Link>
</div>
</div>
<div
className="col-span-5 rounded-xl bg-zinc-900 bg-cover bg-center"
style={{
backgroundImage: `url("https://void.cat/d/Ps1b36vu5pdkEA2w75usuB")`,
}}
/>
<div
className="col-span-2 rounded-xl bg-zinc-900 bg-cover bg-center"
style={{
backgroundImage: `url("https://void.cat/d/5FdJcBP5ZXKAjYqV8hpcp3")`,
}}
/>
<div className="flex flex-1 items-end justify-center pb-10">
<img src="/lume.png" alt="lume" className="h-auto w-1/3" />
</div>
</div>
);
}

View File

@ -18,7 +18,7 @@ export function ChannelBlackList({ blacklist }: { blacklist: any }) {
<MuteIcon
width={16}
height={16}
className="text-zinc-400 group-hover:text-zinc-100"
className="text-white/50 group-hover:text-white"
/>
</Popover.Button>
<Transition
@ -37,7 +37,7 @@ export function ChannelBlackList({ blacklist }: { blacklist: any }) {
<h3 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text font-semibold leading-none text-transparent">
Your muted list
</h3>
<p className="text-base leading-tight text-zinc-400">
<p className="text-base leading-tight text-white/50">
Currently, unmute only affect locally, when you move to new client,
muted list will loaded again
</p>

View File

@ -93,7 +93,7 @@ export function ChannelCreateModal() {
// close modal
setIsOpen(false);
// redirect to channel page
navigate(`/app/channel/${event.id}`);
navigate(`/channel/${event.id}`);
}, 1000);
} catch (e) {
console.log('error: ', e);
@ -112,10 +112,10 @@ export function ChannelCreateModal() {
className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<PlusIcon width={12} height={12} className="text-zinc-500" />
<PlusIcon width={12} height={12} className="text-white/50" />
</div>
<div>
<h5 className="font-medium text-zinc-400">Create channel</h5>
<h5 className="font-medium text-white/50">Create channel</h5>
</div>
</button>
<Transition appear show={isOpen} as={Fragment}>
@ -147,7 +147,7 @@ export function ChannelCreateModal() {
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-zinc-100"
className="text-lg font-semibold leading-none text-white"
>
Create channel
</Dialog.Title>
@ -159,7 +159,7 @@ export function ChannelCreateModal() {
<CancelIcon width={20} height={20} className="text-zinc-300" />
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
<Dialog.Description className="text-sm leading-tight text-white/50">
Channels are freedom square, everyone can speech freely, no one can
stop you or deceive what to speech
</Dialog.Description>
@ -174,10 +174,10 @@ export function ChannelCreateModal() {
type={'hidden'}
{...register('picture')}
value={image}
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-white/50 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-white/50"
/>
<div className="flex flex-col gap-1">
<span className="text-sm font-medium uppercase tracking-wider text-zinc-400">
<span className="text-sm font-medium uppercase tracking-wider text-white/50">
Picture
</span>
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
@ -195,7 +195,7 @@ export function ChannelCreateModal() {
<div className="flex flex-col gap-1">
<label
htmlFor="name"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
className="text-sm font-semibold uppercase tracking-wider text-white/50"
>
Channel name *
</label>
@ -206,28 +206,28 @@ export function ChannelCreateModal() {
minLength: 4,
})}
spellCheck={false}
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="about"
className="text-sm font-semibold uppercase tracking-wider text-zinc-400"
className="text-sm font-semibold uppercase tracking-wider text-white/50"
>
Description
</label>
<textarea
{...register('about')}
spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/>
</div>
<div className="flex h-20 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2">
<div className="flex flex-col gap-1">
<span className="font-semibold leading-none text-zinc-100">
<span className="font-semibold leading-none text-white">
Encrypted
</span>
<p className="w-4/5 text-sm leading-none text-zinc-400">
<p className="w-4/5 text-sm leading-none text-white/50">
All messages are encrypted and only invited members can view and
send message
</p>
@ -248,10 +248,10 @@ export function ChannelCreateModal() {
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
className="inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
) : (
'Create channel →'
)}

View File

@ -7,17 +7,17 @@ export function ChannelsListItem({ data }: { data: any }) {
const channel = useChannelProfile(data.event_id);
return (
<NavLink
to={`/app/channel/${data.event_id}`}
to={`/channel/${data.event_id}`}
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
isActive ? 'bg-zinc-900/50 text-white' : ''
)
}
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<span className="text-xs text-zinc-100">#</span>
<span className="text-xs text-white">#</span>
</div>
<div className="inline-flex w-full items-center justify-between">
<h5 className="truncate font-medium text-zinc-200">{channel?.name}</h5>

View File

@ -74,7 +74,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
<div className="flex w-full flex-col">
<UserReply pubkey={replyTo.pubkey} />
<div className="-mt-5 pl-[38px]">
<div className="text-base text-zinc-100">{replyTo.content}</div>
<div className="text-base text-white">{replyTo.content}</div>
</div>
</div>
<button
@ -82,7 +82,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
onClick={() => stopReply()}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
>
<CancelIcon width={12} height={12} className="text-zinc-100" />
<CancelIcon width={12} height={12} className="text-white" />
</button>
</div>
</div>
@ -95,10 +95,10 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
placeholder="Message"
className={`relative ${
replyTo.id ? 'h-36 pt-16' : 'h-24 pt-3'
} w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-zinc-500`}
} w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-white/50`}
/>
<div className="absolute bottom-0 right-2 h-11">
<div className="flex h-full items-center justify-end gap-3 text-zinc-500">
<div className="flex h-full items-center justify-end gap-3 text-white/50">
<MediaUploader setState={setValue} />
<button
type="button"

View File

@ -99,7 +99,7 @@ export function MessageHideButton({ id }: { id: string }) {
<CancelIcon width={20} height={20} className="text-zinc-300" />
</button>
</div>
<Dialog.Description className="leading-tight text-zinc-400">
<Dialog.Description className="leading-tight text-white/50">
This message will be hidden from your feed.
</Dialog.Description>
</div>
@ -109,14 +109,14 @@ export function MessageHideButton({ id }: { id: string }) {
<button
type="button"
onClick={closeModal}
className="inline-flex h-9 items-center justify-center rounded-md px-2 text-base font-medium text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
className="inline-flex h-9 items-center justify-center rounded-md px-2 text-base font-medium text-white/50 hover:bg-zinc-800 hover:text-white"
>
Cancel
</button>
<button
type="button"
onClick={() => hideMessage()}
className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-2 text-base font-medium text-zinc-100 hover:bg-red-600"
className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-2 text-base font-medium text-white hover:bg-red-600"
>
Confirm
</button>

View File

@ -19,7 +19,7 @@ export function ChannelMessageItem({ data }: { data: LumeEvent }) {
<div className="flex flex-col">
<User pubkey={data.pubkey} time={data.created_at} isChat={true} />
<div className="-mt-[20px] pl-[49px]">
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
<p className="select-text whitespace-pre-line break-words text-base text-white">
{content.parsed}
</p>
{Array.isArray(content.images) && content.images.length ? (

View File

@ -99,7 +99,7 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
<CancelIcon width={20} height={20} className="text-zinc-300" />
</button>
</div>
<Dialog.Description className="leading-tight text-zinc-400">
<Dialog.Description className="leading-tight text-white/50">
You will no longer see messages from this user.
</Dialog.Description>
</div>
@ -109,14 +109,14 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
<button
type="button"
onClick={closeModal}
className="inline-flex h-9 items-center justify-center rounded-md px-2 text-base font-medium text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
className="inline-flex h-9 items-center justify-center rounded-md px-2 text-base font-medium text-white/50 hover:bg-zinc-800 hover:text-white"
>
Cancel
</button>
<button
type="button"
onClick={() => muteUser()}
className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-2 text-base font-medium text-zinc-100 hover:bg-red-600"
className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-2 text-base font-medium text-white hover:bg-red-600"
>
Confirm
</button>

View File

@ -13,7 +13,7 @@ export function UserReply({ pubkey }: { pubkey: string }) {
{isError || isLoading ? (
<>
<div className="relative h-9 w-9 shrink animate-pulse overflow-hidden rounded bg-zinc-800" />
<span className="h-2 w-10 animate-pulse rounded bg-zinc-800 text-base font-medium leading-none text-zinc-500" />
<span className="h-2 w-10 animate-pulse rounded bg-zinc-800 text-base font-medium leading-none text-white/50" />
</>
) : (
<>
@ -25,7 +25,7 @@ export function UserReply({ pubkey }: { pubkey: string }) {
className="h-9 w-9 rounded object-cover"
/>
</div>
<span className="max-w-[10rem] truncate text-sm font-medium leading-none text-zinc-500">
<span className="max-w-[10rem] truncate text-sm font-medium leading-none text-white/50">
Replying to {user?.name || shortenKey(pubkey)}
</span>
</>

View File

@ -32,10 +32,10 @@ export function ChannelMetadata({ id }: { id: string }) {
<div className="inline-flex items-center gap-1">
<h5 className="text-lg font-semibold leading-none">{metadata?.name}</h5>
<button type="button" onClick={() => copyNoteID()}>
<CopyIcon width={14} height={14} className="text-zinc-400" />
<CopyIcon width={14} height={14} className="text-white/50" />
</button>
</div>
<p className="leading-tight text-zinc-400">
<p className="leading-tight text-white/50">
{metadata?.about || (noteID && `${noteID.substring(0, 24)}...`)}
</p>
</div>

View File

@ -51,10 +51,10 @@ export function MutedItem({ data }: { data: any }) {
/>
</div>
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
<span className="truncate text-base font-medium leading-none text-zinc-100">
<span className="truncate text-base font-medium leading-none text-white">
{user?.displayName || user?.name || 'Pleb'}
</span>
<span className="text-base leading-none text-zinc-400">
<span className="text-base leading-none text-white/50">
{shortenKey(data.content)}
</span>
</div>
@ -64,7 +64,7 @@ export function MutedItem({ data }: { data: any }) {
<button
type="button"
onClick={() => unmute()}
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-base font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-base font-medium leading-none text-white/50 hover:bg-zinc-800 hover:text-fuchsia-500"
>
Unmute
</button>
@ -72,7 +72,7 @@ export function MutedItem({ data }: { data: any }) {
<button
type="button"
onClick={() => mute()}
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-base font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-base font-medium leading-none text-white/50 hover:bg-zinc-800 hover:text-fuchsia-500"
>
Mute
</button>

View File

@ -23,7 +23,7 @@ const Header = (
<div className="w-full border-t border-zinc-800" />
</div>
<div className="relative flex justify-center">
<div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-sm font-medium text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-800">
<div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white/50 shadow-sm ring-1 ring-inset ring-zinc-800">
{getHourAgo(24, now).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
@ -40,7 +40,7 @@ const Empty = (
<h3 className="text-base font-semibold leading-none text-white">
Nothing to see here yet
</h3>
<p className="text-base leading-none text-zinc-400">
<p className="text-base leading-none text-white/50">
Be the first to share a message in this channel.
</p>
</div>
@ -102,7 +102,7 @@ export function ChannelScreen() {
data-tauri-drag-region
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
>
<h3 className="font-semibold text-zinc-100">Public Channel</h3>
<h3 className="font-semibold text-white">Public Channel</h3>
</div>
<div className="h-full w-full flex-1 p-3">
<div className="flex h-full flex-col justify-between overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">

View File

@ -6,48 +6,45 @@ import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { displayNpub } from '@utils/shortenKey';
import { Chats } from '@utils/types';
export function ChatsListItem({ data }: { data: any }) {
export function ChatsListItem({ data }: { data: Chats }) {
const { status, user } = useProfile(data.sender_pubkey);
if (status === 'loading') {
return (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-2.5 w-2/3 animate-pulse rounded bg-zinc-800" />
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-white/10" />
<div className="h-2.5 w-2/3 animate-pulse rounded bg-white/10" />
</div>
);
}
return (
<NavLink
to={`/app/chats/${data.sender_pubkey}`}
to={`/chats/${data.sender_pubkey}`}
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
'inline-flex h-9 items-center gap-2.5 rounded-md px-2',
isActive ? 'bg-white/10 text-white' : 'text-white/80'
)
}
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<Image
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}
alt={data.sender_pubkey}
className="h-6 w-6 rounded object-cover"
/>
</div>
<div className="inline-flex w-full items-center justify-between">
<div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[10rem] truncate font-medium text-zinc-200">
{user?.nip05 ||
user?.name ||
user?.displayName ||
shortenKey(data.sender_pubkey)}
</h5>
</div>
<Image
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}
alt={data.sender_pubkey}
className="h-6 w-6 shrink-0 rounded object-cover"
/>
<div className="inline-flex w-full flex-1 items-center justify-between">
<h5 className="max-w-[10rem] truncate">
{user?.nip05 ||
user?.name ||
user?.display_name ||
displayNpub(data.sender_pubkey, 16)}
</h5>
<div className="flex items-center">
{data.new_messages > 0 && (
<span className="inline-flex w-8 items-center justify-center rounded bg-fuchsia-400/10 px-1 py-1 text-xs font-medium text-fuchsia-500 ring-1 ring-inset ring-fuchsia-400/20">

View File

@ -23,7 +23,7 @@ export function ChatsList() {
const renderItem = useCallback(
(item: Chats) => {
if (account.pubkey !== item.sender_pubkey) {
if (account?.pubkey !== item.sender_pubkey) {
return <ChatsListItem key={item.sender_pubkey} data={item} />;
}
},
@ -34,12 +34,12 @@ export function ChatsList() {
return (
<div className="flex flex-col">
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-white/10" />
<div className="h-3 w-full animate-pulse rounded-sm bg-white/10" />
</div>
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-white/10" />
<div className="h-3 w-full animate-pulse rounded-sm bg-white/10" />
</div>
</div>
);
@ -50,20 +50,20 @@ export function ChatsList() {
{account ? (
<ChatsListSelfItem data={account} />
) : (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-white/10" />
<div className="h-3 w-full animate-pulse rounded-sm bg-white/10" />
</div>
)}
{chats.follows.map((item) => renderItem(item))}
{chats.unknowns.length > 0 && <UnknownsModal data={chats.unknowns} />}
<NewMessageModal />
{isFetching && (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" />
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-white/10" />
<div className="h-3 w-full animate-pulse rounded-sm bg-white/10" />
</div>
)}
{chats.unknowns.length > 0 && <UnknownsModal data={chats.unknowns} />}
<NewMessageModal />
</div>
);
}

View File

@ -51,10 +51,10 @@ export function ChatMessageForm({
onKeyDown={handleEnterPress}
spellCheck={false}
placeholder="Message"
className="relative h-11 w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-zinc-500"
className="relative h-11 w-full resize-none rounded-md bg-white/10 px-5 text-white !outline-none placeholder:text-white/50"
/>
<div className="absolute right-2 top-0 h-11">
<div className="flex h-full items-center justify-end gap-3 text-zinc-500">
<div className="flex h-full items-center justify-end gap-3 text-white/50">
<MediaUploader setState={setValue} />
<button
type="button"

View File

@ -26,11 +26,11 @@ export function ChatMessageItem({
const content = parser(data);
return (
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
<div className="flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-white/10">
<div className="flex flex-col">
<User pubkey={data.sender_pubkey} time={data.created_at} isChat={true} />
<div className="-mt-[20px] pl-[49px]">
<p className="select-text whitespace-pre-line break-words text-base text-zinc-100">
<p className="select-text whitespace-pre-line break-words text-base text-white">
{content.parsed}
</p>
{content.images.length > 0 && <ImagePreview urls={content.images} />}

View File

@ -1,5 +1,5 @@
import { Dialog, Transition } from '@headlessui/react';
import { Fragment, useState } from 'react';
import * as Dialog from '@radix-ui/react-dialog';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user';
@ -10,114 +10,77 @@ import { useAccount } from '@utils/hooks/useAccount';
export function NewMessageModal() {
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false);
const [open, setOpen] = useState(false);
const { status, account } = useAccount();
const follows = account ? JSON.parse(account.follows as string) : [];
const closeModal = () => {
setIsOpen(false);
};
const openModal = () => {
setIsOpen(true);
};
const openChat = (pubkey: string) => {
closeModal();
navigate(`/app/chats/${pubkey}`);
setOpen(false);
navigate(`/chats/${pubkey}`);
};
return (
<>
<button
type="button"
onClick={() => openModal()}
className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<PlusIcon className="h-3 w-3 text-zinc-200" />
</div>
<div>
<h5 className="font-medium text-zinc-400">New chat</h5>
</div>
</button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-lg border-t border-zinc-800/50 bg-zinc-900">
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-zinc-100"
>
New chat
</Dialog.Title>
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-9 items-center gap-2.5 rounded-md px-2"
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
<PlusIcon className="h-3 w-3 text-white" />
</div>
<div>
<h5 className="text-white/50">New chat</h5>
</div>
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10">
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<Dialog.Title className="text-lg font-semibold leading-none text-white">
New chat
</Dialog.Title>
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-white/10">
<CancelIcon className="h-4 w-4 text-white/50" />
</Dialog.Close>
</div>
<Dialog.Description className="text-sm leading-none text-white/50">
All messages will be encrypted, but anyone can see who you chat
</Dialog.Description>
</div>
</div>
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-2 pt-2">
{status === 'loading' ? (
<div className="inline-flex items-center justify-center px-4 py-3">
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</div>
) : (
account?.follows?.map((follow) => (
<div
key={follow}
className="group flex items-center justify-between px-4 py-2 hover:bg-white/10"
>
<User pubkey={follow} />
<div>
<button
type="button"
onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
onClick={() => openChat(follow)}
className="hidden w-max rounded bg-white/10 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
>
<CancelIcon className="h-4 w-4 text-zinc-300" />
Chat
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
All messages will be encrypted, but anyone can see who you chat
</Dialog.Description>
</div>
</div>
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-5">
{status === 'loading' ? (
<div className="inline-flex items-center justify-center px-4 py-3">
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
</div>
) : (
follows.map((follow) => (
<div
key={follow}
className="group flex items-center justify-between px-4 py-3 hover:bg-zinc-800"
>
<User pubkey={follow} />
<div>
<button
type="button"
onClick={() => openChat(follow)}
className="hidden w-max rounded-md bg-zinc-700 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
>
Chat
</button>
</div>
</div>
))
)}
</div>
</Dialog.Panel>
</Transition.Child>
))
)}
</div>
</div>
</Dialog>
</Transition>
</>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@ -6,17 +6,17 @@ import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { displayNpub } from '@utils/shortenKey';
export function ChatsListSelfItem({ data }: { data: { pubkey: string } }) {
const { status, user } = useProfile(data.pubkey);
if (status === 'loading') {
return (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-white/10" />
<div>
<div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-base font-medium" />
<div className="h-2.5 w-full animate-pulse truncate rounded bg-white/10 text-base font-medium" />
</div>
</div>
);
@ -24,28 +24,26 @@ export function ChatsListSelfItem({ data }: { data: { pubkey: string } }) {
return (
<NavLink
to={`/app/chats/${data.pubkey}`}
to={`/chats/${data.pubkey}`}
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
'inline-flex h-9 items-center gap-2.5 rounded-md px-2',
isActive ? 'bg-white/10 text-white' : 'text-white/80'
)
}
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<Image
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}
alt={data.pubkey}
className="h-6 w-6 rounded bg-white object-cover"
/>
</div>
<Image
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}
alt={data.pubkey}
className="h-6 w-6 shrink-0 rounded bg-white object-cover"
/>
<div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[9rem] truncate font-medium text-zinc-200">
{user?.nip05 || user?.name || shortenKey(data.pubkey)}
<h5 className="max-w-[10rem] truncate">
{user?.nip05 || user?.name || displayNpub(data.pubkey, 16)}
</h5>
<span className="text-zinc-500">(you)</span>
<span className="text-white/50">(you)</span>
</div>
</NavLink>
);

View File

@ -5,7 +5,7 @@ import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { displayNpub } from '@utils/shortenKey';
export function ChatSidebar({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey);
@ -24,17 +24,17 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<h3 className="text-lg font-semibold leading-none">
{user?.displayName || user?.name}
{user?.display_name || user?.name}
</h3>
<h5 className="leading-none text-zinc-400">
{user?.nip05 || shortenKey(pubkey)}
<h5 className="leading-none text-white/50">
{user?.nip05 || displayNpub(pubkey, 16)}
</h5>
</div>
<div>
<p className="leading-tight">{user?.bio || user?.about}</p>
<Link
to={`/app/users/${pubkey}`}
className="mt-3 inline-flex h-10 w-full items-center justify-center rounded-md bg-zinc-900 text-sm font-medium text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100"
to={`/users/${pubkey}`}
className="mt-3 inline-flex h-10 w-full items-center justify-center rounded-md bg-white/10 text-sm font-medium text-white hover:bg-fuchsia-500"
>
View full profile
</Link>

View File

@ -1,117 +1,81 @@
import { Dialog, Transition } from '@headlessui/react';
import { Fragment, useState } from 'react';
import * as Dialog from '@radix-ui/react-dialog';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user';
import { CancelIcon, StrangersIcon } from '@shared/icons';
import { CancelIcon, PlusIcon } from '@shared/icons';
import { compactNumber } from '@utils/number';
import { Chats } from '@utils/types';
export function UnknownsModal({ data }: { data: Chats[] }) {
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false);
const closeModal = () => {
setIsOpen(false);
};
const openModal = () => {
setIsOpen(true);
};
const [open, setOpen] = useState(false);
const openChat = (pubkey: string) => {
closeModal();
navigate(`/app/chats/${pubkey}`);
setOpen(false);
navigate(`/chats/${pubkey}`);
};
return (
<>
<button
type="button"
onClick={() => openModal()}
className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<StrangersIcon className="h-3 w-3 text-zinc-200" />
</div>
<div>
<h5 className="font-medium text-zinc-400">
{compactNumber.format(data.length)} unknowns
</h5>
</div>
</button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-lg border-t border-zinc-800/50 bg-zinc-900">
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-zinc-100"
>
{data.length} unknowns
</Dialog.Title>
<button
type="button"
onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
>
<CancelIcon className="h-4 w-4 text-zinc-300" />
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
All messages from people you not follow
</Dialog.Description>
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-9 items-center gap-2.5 rounded-md px-2"
>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
<PlusIcon className="h-3 w-3 text-white" />
</div>
<div>
<h5 className="text-white/50">
{compactNumber.format(data.length)} unknowns
</h5>
</div>
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10">
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<Dialog.Title className="text-lg font-semibold leading-none text-white">
{data.length} unknowns
</Dialog.Title>
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-white/10">
<CancelIcon className="h-4 w-4 text-white/50" />
</Dialog.Close>
</div>
<Dialog.Description className="text-sm leading-none text-white/50">
All messages from people you not follow
</Dialog.Description>
</div>
</div>
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-2 pt-2">
{data.map((user) => (
<div
key={user.sender_pubkey}
className="group flex items-center justify-between px-4 py-2 hover:bg-white/10"
>
<User pubkey={user.sender_pubkey} />
<div>
<button
type="button"
onClick={() => openChat(user.sender_pubkey)}
className="hidden w-max rounded bg-white/10 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
>
Chat
</button>
</div>
</div>
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-5">
{data.map((user) => (
<div
key={user.sender_pubkey}
className="group flex items-center justify-between px-4 py-3 hover:bg-zinc-800"
>
<User pubkey={user.sender_pubkey} />
<div>
<button
type="button"
onClick={() => openChat(user.sender_pubkey)}
className="hidden w-max rounded-md bg-zinc-700 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
>
Chat
</button>
</div>
</div>
))}
</div>
</Dialog.Panel>
</Transition.Child>
))}
</div>
</div>
</Dialog>
</Transition>
</>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@ -1,7 +1,9 @@
import { nip04 } from 'nostr-tools';
import { useEffect, useState } from 'react';
export function useDecryptMessage(data: any, userPubkey: string, userPriv: string) {
import { Chats } from '@utils/types';
export function useDecryptMessage(data: Chats, userPubkey: string, userPriv: string) {
const [content, setContent] = useState(data.content);
useEffect(() => {

View File

@ -14,6 +14,7 @@ import { createChat, getChatMessages } from '@libs/storage';
import { useStronghold } from '@stores/stronghold';
import { useAccount } from '@utils/hooks/useAccount';
import { Chats } from '@utils/types';
export function ChatScreen() {
const queryClient = useQueryClient();
@ -34,7 +35,7 @@ export function ChatScreen() {
const userPrivkey = useStronghold((state) => state.privkey);
const itemContent: any = useCallback(
const itemContent = useCallback(
(index: string | number) => {
return (
<ChatMessageItem
@ -55,7 +56,7 @@ export function ChatScreen() {
);
const chat = useMutation({
mutationFn: (data: any) => {
mutationFn: (data: Chats) => {
return createChat(
data.id,
data.receiver_pubkey,
@ -100,16 +101,10 @@ export function ChatScreen() {
}, [pubkey]);
return (
<div className="grid h-full w-full grid-cols-3">
<div className="col-span-2 flex flex-col justify-between border-r border-zinc-900">
<div
data-tauri-drag-region
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
>
<h3 className="font-semibold text-zinc-100">Encrypted Chat</h3>
</div>
<div className="grid h-full w-full grid-cols-3 bg-white/10">
<div className="col-span-2 border-r border-white/5">
<div className="h-full w-full flex-1 p-3">
<div className="flex h-full flex-col justify-between overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="flex h-full flex-col justify-between overflow-hidden rounded-xl bg-white/10">
<div className="h-full w-full flex-1">
{status === 'loading' ? (
<p>Loading...</p>
@ -131,7 +126,7 @@ export function ChatScreen() {
/>
)}
</div>
<div className="z-50 shrink-0 rounded-b-xl border-t border-zinc-800 bg-zinc-900 p-3 px-5">
<div className="z-50 shrink-0 rounded-b-xl border-t border-white/5 bg-white/10 p-3 px-5">
<ChatMessageForm
receiverPubkey={pubkey}
userPubkey={account.pubkey}
@ -141,11 +136,7 @@ export function ChatScreen() {
</div>
</div>
</div>
<div className="col-span-1">
<div
data-tauri-drag-region
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
/>
<div className="col-span-1 pt-3">
<ChatSidebar pubkey={pubkey} />
</div>
</div>
@ -155,7 +146,7 @@ export function ChatScreen() {
const Empty = (
<div className="absolute left-1/2 top-1/2 flex w-full -translate-x-1/2 -translate-y-1/2 transform flex-col gap-1 text-center">
<h3 className="mb-2 text-4xl">🙌</h3>
<p className="leading-none text-zinc-400">
<p className="leading-none text-white/50">
You two didn&apos;t talk yet, let&apos;s send first message
</p>
</div>

51
src/app/events/index.tsx Normal file
View File

@ -0,0 +1,51 @@
import { useParams } from 'react-router-dom';
import {
NoteActions,
NoteContent,
NoteReplyForm,
NoteStats,
ThreadUser,
} from '@shared/notes';
import { RepliesList } from '@shared/notes/replies/list';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { useAccount } from '@utils/hooks/useAccount';
import { useEvent } from '@utils/hooks/useEvent';
export function EventScreen() {
const { id } = useParams();
const { account } = useAccount();
const { status, data } = useEvent(id);
return (
<div className="mx-auto w-[600px]">
<div className="scrollbar-hide flex h-full w-full flex-col gap-1.5 overflow-y-auto pt-11">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>
) : (
<div className="h-min w-full px-3 pt-1.5">
<div className="rounded-xl bg-white/10 px-3 pt-3">
<ThreadUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-2">
<NoteContent content={data.content} />
</div>
<div>
<NoteActions id={id} pubkey={data.pubkey} noOpenThread={true} />
<NoteStats id={id} />
</div>
</div>
</div>
)}
<div className="px-3">
<NoteReplyForm id={id} pubkey={account.pubkey} />
<RepliesList id={id} />
</div>
</div>
</div>
);
}

View File

@ -1,49 +0,0 @@
import { useParams } from 'react-router-dom';
import { useLiveThread } from '@app/space/hooks/useLiveThread';
import { NoteMetadata } from '@shared/notes/metadata';
import { NoteReplyForm } from '@shared/notes/replies/form';
import { RepliesList } from '@shared/notes/replies/list';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { User } from '@shared/user';
import { useAccount } from '@utils/hooks/useAccount';
import { useEvent } from '@utils/hooks/useEvent';
export function NoteScreen() {
const { id } = useParams();
const { account } = useAccount();
const { status, data } = useEvent(id);
useLiveThread(id);
return (
<div className="mx-auto w-[600px]">
<div className="scrollbar-hide flex h-full w-full flex-col gap-1.5 overflow-y-auto pb-20 pt-16">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
<NoteSkeleton />
</div>
</div>
) : (
<div className="h-min w-full px-3 py-1.5">
<div className="rounded-md bg-zinc-900 px-5 pt-5">
<User pubkey={data.pubkey} time={data.created_at} />
<div className="mt-3">
<NoteMetadata id={data.event_id || id} />
</div>
</div>
<div className="mt-3 rounded-md bg-zinc-900">
{account && <NoteReplyForm rootID={id} userPubkey={account.pubkey} />}
</div>
</div>
)}
<div className="px-3">
<RepliesList id={id} />
</div>
</div>
</div>
);
}

View File

@ -1,200 +0,0 @@
import { NDKUser } from '@nostr-dev-kit/ndk';
import { nip19 } from 'nostr-tools';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNDK } from '@libs/ndk/provider';
import {
countTotalNotes,
createChat,
createNote,
getLastLogin,
updateAccount,
updateLastLogin,
} from '@libs/storage';
import { LoaderIcon } from '@shared/icons';
import { nHoursAgo } from '@utils/date';
import { useAccount } from '@utils/hooks/useAccount';
const totalNotes = await countTotalNotes();
const lastLogin = await getLastLogin();
export function Root() {
const navigate = useNavigate();
const { ndk, relayUrls, fetcher } = useNDK();
const { status, account } = useAccount();
async function getFollows() {
const authors: string[] = [];
const user = ndk.getUser({ hexpubkey: account.pubkey });
const follows = await user.follows();
follows.forEach((follow: NDKUser) => {
authors.push(nip19.decode(follow.npub).data as string);
});
// update follows in db
await updateAccount('follows', authors, account.pubkey);
return authors;
}
async function fetchNotes() {
try {
const follows = await getFollows();
if (follows.length > 0) {
let since: number;
if (totalNotes === 0 || lastLogin === 0) {
since = nHoursAgo(48);
} else {
since = lastLogin;
}
const events = fetcher.allEventsIterator(
relayUrls,
{ kinds: [1], authors: follows },
{ since: since },
{ skipVerification: true }
);
for await (const event of events) {
await createNote(
event.id,
event.pubkey,
event.kind,
event.tags,
event.content,
event.created_at
);
}
}
return true;
} catch (e) {
console.log('error: ', e);
}
}
async function fetchChats() {
try {
const sendMessages = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [4],
authors: [account.pubkey],
},
{ since: lastLogin }
);
const receiveMessages = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [4],
'#p': [account.pubkey],
},
{ since: lastLogin }
);
const events = [...sendMessages, ...receiveMessages];
for (const event of events) {
const receiverPubkey = event.tags.find((t) => t[0] === 'p')[1] || account.pubkey;
await createChat(
event.id,
receiverPubkey,
event.pubkey,
event.content,
event.tags,
event.created_at
);
}
return true;
} catch (e) {
console.log('error: ', e);
}
}
/*
async function fetchChannelMessages() {
try {
const ids = [];
const channels: any = await getChannels();
channels.forEach((channel) => {
ids.push(channel.event_id);
});
const since = lastLogin === 0 ? dateToUnix(getHourAgo(48, now.current)) : lastLogin;
const filter: NDKFilter = {
'#e': ids,
kinds: [42],
since: since,
};
const events = await prefetchEvents(ndk, filter);
events.forEach((event) => {
const channel_id = event.tags[0][1];
if (channel_id) {
createChannelMessage(
channel_id,
event.id,
event.pubkey,
event.kind,
event.content,
event.tags,
event.created_at
);
}
});
return true;
} catch (e) {
console.log('error: ', e);
}
}
*/
useEffect(() => {
async function prefetch() {
const notes = await fetchNotes();
const chats = await fetchChats();
if (notes && chats) {
const now = Math.floor(Date.now() / 1000);
await updateLastLogin(now);
navigate('/app/space', { replace: true });
}
}
if (status === 'success' && account) {
prefetch();
}
}, [status]);
return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100">
<div className="flex h-screen w-full flex-col">
<div
data-tauri-drag-region
className="relative h-11 shrink-0 border border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
/>
<div className="relative flex min-h-0 w-full flex-1 items-center justify-center">
<div className="flex flex-col items-center justify-center gap-4">
<LoaderIcon className="h-6 w-6 animate-spin text-zinc-100" />
<div className="text-center">
<h3 className="text-lg font-semibold leading-tight text-zinc-100">
Prefetching data...
</h3>
<p className="text-zinc-600">
This may take a few seconds, please don&apos;t close app.
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -23,36 +23,36 @@ export function AccountSettingsScreen() {
return (
<div className="h-full w-full px-3 pt-12">
<div className="flex flex-col gap-2">
<h1 className="text-lg font-semibold text-zinc-100">Account</h1>
<h1 className="text-lg font-semibold text-white">Account</h1>
<div className="">
{status === 'loading' ? (
<p>Loading...</p>
) : (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<label htmlFor="pubkey" className="text-base font-semibold text-zinc-400">
<label htmlFor="pubkey" className="text-base font-semibold text-white/50">
Public Key
</label>
<input
readOnly
value={account.pubkey}
className="relative w-2/3 rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
className="relative w-2/3 rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="npub" className="text-base font-semibold text-zinc-400">
<label htmlFor="npub" className="text-base font-semibold text-white/50">
Npub
</label>
<input
readOnly
value={account.npub}
className="relative w-2/3 rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
className="relative w-2/3 rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="privkey"
className="text-base font-semibold text-zinc-400"
className="text-base font-semibold text-white/50"
>
Private Key
</label>
@ -61,7 +61,7 @@ export function AccountSettingsScreen() {
readOnly
type={type}
value={privkey}
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
/>
<button
type="button"
@ -72,13 +72,13 @@ export function AccountSettingsScreen() {
<EyeOffIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
className="text-white/50 group-hover:text-white"
/>
) : (
<EyeOnIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
className="text-white/50 group-hover:text-white"
/>
)}
</button>

View File

@ -36,7 +36,7 @@ export function AutoStartSetting() {
<div className="inline-flex items-center justify-between px-5 py-4">
<div className="flex flex-col gap-1">
<span className="font-medium leading-none text-zinc-200">Auto start</span>
<span className="text-sm leading-none text-zinc-400">Auto start at login</span>
<span className="text-sm leading-none text-white/50">Auto start at login</span>
</div>
<Switch
checked={enabled}

View File

@ -20,7 +20,7 @@ export function CacheTimeSetting() {
<span className="font-medium leading-none text-zinc-200">
Cache time (milliseconds)
</span>
<span className="text-sm leading-none text-zinc-400">
<span className="text-sm leading-none text-white/50">
The length of time before inactive data gets removed from the cache
</span>
</div>
@ -37,7 +37,7 @@ export function CacheTimeSetting() {
onClick={() => update()}
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 font-medium hover:bg-fuchsia-500"
>
<CheckCircleIcon className="h-4 w-4 text-zinc-100" />
<CheckCircleIcon className="h-4 w-4 text-white" />
</button>
</div>
</div>

View File

@ -9,7 +9,7 @@ export function VersionSetting() {
<div className="inline-flex items-center justify-between px-5 py-4">
<div className="flex flex-col gap-1">
<span className="font-medium leading-none text-zinc-200">Version</span>
<span className="text-sm leading-none text-zinc-400">
<span className="text-sm leading-none text-white/50">
You&apos;re using latest version
</span>
</div>
@ -19,7 +19,7 @@ export function VersionSetting() {
type="button"
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 font-medium hover:bg-fuchsia-500"
>
<RefreshIcon className="h-4 w-4 text-zinc-100" />
<RefreshIcon className="h-4 w-4 text-white" />
</button>
</div>
</div>

View File

@ -6,7 +6,7 @@ export function GeneralSettingsScreen() {
return (
<div className="h-full w-full px-3 pt-12">
<div className="flex flex-col gap-2">
<h1 className="text-lg font-semibold text-zinc-100">General</h1>
<h1 className="text-lg font-semibold text-white">General</h1>
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="flex h-full w-full flex-col divide-y divide-zinc-800">
<AutoStartSetting />

View File

@ -4,7 +4,7 @@ export function ShortcutsSettingsScreen() {
return (
<div className="h-full w-full px-3 pt-12">
<div className="flex flex-col gap-2">
<h1 className="text-lg font-semibold text-zinc-100">Shortcuts</h1>
<h1 className="text-lg font-semibold text-white">Shortcuts</h1>
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="flex h-full w-full flex-col divide-y divide-zinc-800">
<div className="inline-flex items-center justify-between px-5 py-4">
@ -15,10 +15,10 @@ export function ShortcutsSettingsScreen() {
</div>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<CommandIcon width={12} height={12} className="text-zinc-500" />
<CommandIcon width={12} height={12} className="text-white/50" />
</div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<span className="text-sm leading-none text-zinc-500">N</span>
<span className="text-sm leading-none text-white/50">N</span>
</div>
</div>
</div>
@ -30,10 +30,10 @@ export function ShortcutsSettingsScreen() {
</div>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<CommandIcon width={12} height={12} className="text-zinc-500" />
<CommandIcon width={12} height={12} className="text-white/50" />
</div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<span className="text-sm leading-none text-zinc-500">I</span>
<span className="text-sm leading-none text-white/50">I</span>
</div>
</div>
</div>
@ -45,10 +45,10 @@ export function ShortcutsSettingsScreen() {
</div>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<CommandIcon width={12} height={12} className="text-zinc-500" />
<CommandIcon width={12} height={12} className="text-white/50" />
</div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<span className="text-sm leading-none text-zinc-500">F</span>
<span className="text-sm leading-none text-white/50">F</span>
</div>
</div>
</div>
@ -60,10 +60,10 @@ export function ShortcutsSettingsScreen() {
</div>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<CommandIcon width={12} height={12} className="text-zinc-500" />
<CommandIcon width={12} height={12} className="text-white/50" />
</div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<span className="text-sm leading-none text-zinc-500">P</span>
<span className="text-sm leading-none text-white/50">P</span>
</div>
</div>
</div>
@ -75,10 +75,10 @@ export function ShortcutsSettingsScreen() {
</div>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<CommandIcon width={12} height={12} className="text-zinc-500" />
<CommandIcon width={12} height={12} className="text-white/50" />
</div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800">
<span className="text-sm leading-none text-zinc-500">B</span>
<span className="text-sm leading-none text-white/50">B</span>
</div>
</div>
</div>

View File

@ -1,13 +0,0 @@
import { AddFeedBlock } from '@app/space/components/addFeed';
import { AddHashTagBlock } from '@app/space/components/addHashtag';
import { AddImageBlock } from '@app/space/components/addImage';
export function AddBlock() {
return (
<div className="flex flex-col gap-1">
<AddImageBlock />
<AddFeedBlock />
<AddHashTagBlock />
</div>
);
}

View File

@ -1,252 +0,0 @@
import { Dialog, Transition } from '@headlessui/react';
import { Combobox } from '@headlessui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools';
import { Fragment, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { User } from '@app/auth/components/user';
import { createBlock } from '@libs/storage';
import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
import { ADD_FEEDBLOCK_SHORTCUT } from '@stores/shortcuts';
import { useAccount } from '@utils/hooks/useAccount';
export function AddFeedBlock() {
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState([]);
const [query, setQuery] = useState('');
const { status, account } = useAccount();
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
useHotkeys(ADD_FEEDBLOCK_SHORTCUT, () => openModal());
const block = useMutation({
mutationFn: (data: { kind: number; title: string; content: string }) => {
return createBlock(data.kind, data.title, data.content);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['blocks'] });
},
});
const {
register,
handleSubmit,
reset,
formState: { isDirty, isValid },
} = useForm();
const onSubmit = (data: { kind: number; title: string; content: string }) => {
setLoading(true);
selected.forEach((item, index) => {
if (item.substring(0, 4) === 'npub') {
selected[index] = nip19.decode(item).data;
}
});
// insert to database
block.mutate({
kind: BLOCK_KINDS.feed,
title: data.title,
content: JSON.stringify(selected),
});
setLoading(false);
// reset form
reset();
// close modal
closeModal();
};
return (
<>
<button
type="button"
onClick={() => openModal()}
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<CommandIcon width={12} height={12} className="text-zinc-500" />
</div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<span className="text-sm leading-none text-zinc-500">F</span>
</div>
</div>
<div>
<h5 className="font-medium text-zinc-400">New feed block</h5>
</div>
</button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-zinc-100"
>
Create feed block
</Dialog.Title>
<button
type="button"
onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
>
<CancelIcon width={14} height={14} className="text-zinc-300" />
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
Specific newsfeed space for people you want to keep up to date
</Dialog.Description>
</div>
</div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="mb-0 flex h-full w-full flex-col gap-4"
>
<div className="flex flex-col gap-1">
<label
htmlFor="title"
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
>
Title *
</label>
<input
type={'text'}
{...register('title', {
required: true,
})}
spellCheck={false}
className="relative h-10 w-full rounded-md bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/>
</div>
<div className="flex flex-col gap-1">
<span className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Choose at least 1 user *
</span>
<div className="flex h-[300px] w-full flex-col overflow-y-auto overflow-x-hidden rounded-lg border-t border-zinc-700/50 bg-zinc-800">
<div className="w-full px-3 py-2">
<Combobox value={selected} onChange={setSelected} multiple>
<Combobox.Input
onChange={(event) => setQuery(event.target.value)}
spellCheck={false}
placeholder="Enter pubkey or npub..."
className="relative mb-2 h-10 w-full rounded-md bg-zinc-700 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
/>
<Combobox.Options static>
{query.length > 0 && (
<Combobox.Option
value={query}
className="group flex w-full items-center justify-between rounded-md px-2 py-2 hover:bg-zinc-700"
>
{({ selected }) => (
<>
<div className="flex items-center gap-2">
<img
alt={query}
src={DEFAULT_AVATAR}
className="h-11 w-11 shrink-0 rounded object-cover"
/>
<div className="inline-flex flex-col gap-1">
<span className="text-base leading-tight text-zinc-400">
{query}
</span>
</div>
</div>
{selected && (
<CheckCircleIcon className="h-4 w-4 text-green-500" />
)}
</>
)}
</Combobox.Option>
)}
{status === 'loading' ? (
<p>Loading...</p>
) : (
JSON.parse(account.follows as string).map((follow) => (
<Combobox.Option
key={follow}
value={follow}
className="group flex w-full items-center justify-between rounded-md px-2 py-2 hover:bg-zinc-700"
>
{({ selected }) => (
<>
<User pubkey={follow} />
{selected && (
<CheckCircleIcon className="h-4 w-4 text-green-500" />
)}
</>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
</Combobox>
</div>
</div>
</div>
<div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : (
'Confirm'
)}
</button>
</div>
</form>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
</>
);
}

View File

@ -1,174 +0,0 @@
import { Dialog, Transition } from '@headlessui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Fragment, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { createBlock } from '@libs/storage';
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
import { BLOCK_KINDS } from '@stores/constants';
import { ADD_HASHTAGBLOCK_SHORTCUT } from '@stores/shortcuts';
export function AddHashTagBlock() {
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
useHotkeys(ADD_HASHTAGBLOCK_SHORTCUT, () => openModal());
const {
register,
handleSubmit,
reset,
formState: { isDirty, isValid },
} = useForm();
const block = useMutation({
mutationFn: (data: { kind: number; title: string; content: string }) => {
return createBlock(data.kind, data.title, data.content);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['blocks'] });
},
});
const onSubmit = async (data: { hashtag: string }) => {
setLoading(true);
// mutate
block.mutate({
kind: BLOCK_KINDS.hashtag,
title: data.hashtag,
content: data.hashtag.replace('#', ''),
});
setLoading(false);
// reset form
reset();
// close modal
closeModal();
};
return (
<>
<button
type="button"
onClick={() => openModal()}
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<CommandIcon width={12} height={12} className="text-zinc-500" />
</div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<span className="text-sm leading-none text-zinc-500">T</span>
</div>
</div>
<div>
<h5 className="font-medium text-zinc-400">New hashtag block</h5>
</div>
</button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-zinc-100"
>
Create hashtag block
</Dialog.Title>
<button
type="button"
onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
>
<CancelIcon width={14} height={14} className="text-zinc-300" />
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
Pin the hashtag you want to keep follow up
</Dialog.Description>
</div>
</div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="mb-0 flex h-full w-full flex-col gap-4"
>
<div className="flex flex-col gap-1">
<label
htmlFor="title"
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
>
Hashtag *
</label>
<div className="after:shadow-highlight relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] 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-[6px] 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
type={'text'}
{...register('hashtag', {
required: true,
})}
spellCheck={false}
placeholder="#"
className="shadow-input relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
</div>
<div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : (
'Confirm'
)}
</button>
</div>
</form>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
</>
);
}

View File

@ -1,226 +0,0 @@
import { Dialog, Transition } from '@headlessui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Fragment, useEffect, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { createBlock } from '@libs/storage';
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
import { ADD_IMAGEBLOCK_SHORTCUT } from '@stores/shortcuts';
import { usePublish } from '@utils/hooks/usePublish';
import { useImageUploader } from '@utils/hooks/useUploader';
export function AddImageBlock() {
const queryClient = useQueryClient();
const upload = useImageUploader();
const { publish } = usePublish();
const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [image, setImage] = useState('');
const tags = useRef(null);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
useHotkeys(ADD_IMAGEBLOCK_SHORTCUT, () => openModal());
const {
register,
handleSubmit,
reset,
setValue,
formState: { isDirty, isValid },
} = useForm();
const block = useMutation({
mutationFn: (data: { kind: number; title: string; content: string }) => {
return createBlock(data.kind, data.title, data.content);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['blocks'] });
},
});
const uploadImage = async () => {
const image = await upload(null);
if (image.url) {
setImage(image.url);
}
};
const onSubmit = async (data: { kind: number; title: string; content: string }) => {
setLoading(true);
// publish file metedata
await publish({ content: data.title, kind: 1063, tags: tags.current });
// mutate
block.mutate({ kind: BLOCK_KINDS.image, title: data.title, content: data.content });
setLoading(false);
// reset form
reset();
// close modal
closeModal();
};
useEffect(() => {
setValue('content', image);
}, [setValue, image]);
return (
<>
<button
type="button"
onClick={() => openModal()}
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<CommandIcon width={12} height={12} className="text-zinc-500" />
</div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<span className="text-sm leading-none text-zinc-500">I</span>
</div>
</div>
<div>
<h5 className="font-medium text-zinc-400">New image block</h5>
</div>
</button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-zinc-100"
>
Create image block
</Dialog.Title>
<button
type="button"
onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
>
<CancelIcon width={14} height={14} className="text-zinc-300" />
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
Pin your favorite image to Space then you can view every time that
you use Lume, your image will be broadcast to Nostr Relay as well
</Dialog.Description>
</div>
</div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="mb-0 flex h-full w-full flex-col gap-4"
>
<input
type={'hidden'}
{...register('content')}
value={image}
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
<div className="flex flex-col gap-1">
<label
htmlFor="title"
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
>
Title *
</label>
<div className="after:shadow-highlight relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] 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-[6px] 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
type={'text'}
{...register('title', {
required: true,
})}
spellCheck={false}
className="shadow-input relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="picture"
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
>
Picture
</label>
<div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
<Image
src={image}
fallback={DEFAULT_AVATAR}
alt="content"
className="relative z-10 h-auto max-h-[156px] w-[150px] rounded-md object-cover"
/>
<div className="absolute bottom-3 right-3 z-10">
<button
onClick={() => uploadImage()}
type="button"
className="inline-flex h-6 items-center justify-center rounded bg-zinc-900 px-3 text-sm font-medium text-zinc-300 ring-1 ring-zinc-800 hover:bg-zinc-800"
>
Upload
</button>
</div>
</div>
</div>
<div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : (
'Confirm'
)}
</button>
</div>
</form>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
</>
);
}

View File

@ -34,6 +34,7 @@ export function FeedBlock({ params }: { params: Block }) {
});
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();
useEffect(() => {
const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse();
@ -113,24 +114,20 @@ export function FeedBlock({ params }: { params: Block }) {
);
return (
<div className="w-[400px] shrink-0 border-r border-zinc-900">
<div className="relative w-[400px] shrink-0 bg-white/10">
<TitleBar id={params.id} title={params.title} />
<div
ref={parentRef}
className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5"
style={{ contain: 'strict' }}
>
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>
) : itemsVirtualizer.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-6">
<div className="bbg-white/10 rounded-xl px-3 py-6">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-zinc-300">
<p className="text-center text-sm text-white">
Not found any posts from last 48 hours
</p>
</div>
@ -140,7 +137,7 @@ export function FeedBlock({ params }: { params: Block }) {
<div
className="relative w-full"
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
height: `${totalSize}px`,
}}
>
<div
@ -157,7 +154,7 @@ export function FeedBlock({ params }: { params: Block }) {
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>

View File

@ -16,7 +16,7 @@ export function HashtagBlock({ params }: { params: Block }) {
const events = (await fetcher.fetchAllEvents(
relayUrls,
{ kinds: [1], '#t': [params.content] },
{ since: nHoursAgo(48) }
{ since: nHoursAgo(24) }
)) as unknown as LumeEvent[];
return events;
});
@ -29,27 +29,24 @@ export function HashtagBlock({ params }: { params: Block }) {
});
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();
return (
<div className="w-[400px] shrink-0 border-r border-zinc-900">
<TitleBar id={params.id} title={params.title + ' in 48 hours ago'} />
<div
ref={parentRef}
className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5"
style={{ contain: 'strict' }}
>
<div className="relative w-[400px] shrink-0 bg-white/10">
<TitleBar id={params.id} title={params.title + ' in 24 hours ago'} />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>
) : itemsVirtualizer.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-6">
<div className="rounded-xl bg-white/10 px-3 py-6">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-zinc-300">
No new posts about this hashtag in 48 hours ago
<p className="text-center text-sm font-medium text-white">
No new posts about this hashtag in 24 hours ago
</p>
</div>
</div>
@ -58,7 +55,7 @@ export function HashtagBlock({ params }: { params: Block }) {
<div
className="relative w-full"
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
height: `${totalSize}px`,
}}
>
<div

View File

@ -1,23 +1,23 @@
import { CancelIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { useBlocks } from '@stores/blocks';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useBlock } from '@utils/hooks/useBlock';
import { Block } from '@utils/types';
export function ImageBlock({ params }: { params: Block }) {
const { remove } = useBlock();
const removeBlock = useBlocks((state) => state.removeBlock);
return (
<div className="flex h-full w-[350px] shrink-0 flex-col justify-between border-r border-zinc-900">
<div className="flex h-full w-[400px] shrink-0 flex-col justify-between">
<div className="relative h-full w-full flex-1 overflow-hidden p-3">
<div className="absolute left-0 top-3 h-16 w-full px-3">
<div className="flex h-16 items-center justify-between overflow-hidden rounded-t-xl px-5">
<h3 className="font-medium text-white drop-shadow-lg">{params.title}</h3>
<button
type="button"
onClick={() => remove.mutate(params.id)}
onClick={() => removeBlock(params.id)}
className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-white/30 backdrop-blur-lg"
>
<CancelIcon width={16} height={16} className="text-white" />
@ -28,7 +28,7 @@ export function ImageBlock({ params }: { params: Block }) {
src={params.content}
fallback={DEFAULT_AVATAR}
alt={params.title}
className="h-full w-full rounded-xl border-t border-zinc-800/50 object-cover"
className="h-full w-full rounded-xl object-cover"
/>
</div>
</div>

View File

@ -16,7 +16,7 @@ import { LumeEvent } from '@utils/types';
const ITEM_PER_PAGE = 10;
export function FollowingBlock() {
export function NetworkBlock() {
// subscribe for live update
useNewsfeed();
@ -40,9 +40,10 @@ export function FollowingBlock() {
});
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();
useEffect(() => {
const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse();
const [lastItem] = [...itemsVirtualizer].reverse();
if (!lastItem) {
return;
@ -51,7 +52,7 @@ export function FollowingBlock() {
if (lastItem.index >= notes.length - 1 && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
}, [notes.length, fetchNextPage, itemsVirtualizer]);
const renderItem = useCallback(
(index: string | number) => {
@ -125,30 +126,26 @@ export function FollowingBlock() {
);
return (
<div className="relative w-[400px] shrink-0 border-r border-zinc-900">
<TitleBar title="Your Circle" />
<div
ref={parentRef}
className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5"
style={{ contain: 'strict' }}
>
<div className="relative w-[400px] shrink-0 bg-white/10">
<TitleBar title="Network" />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>
) : itemsVirtualizer.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-6">
<div className="rounded-xl bg-white/10 px-3 py-6">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-zinc-300">
<p className="text-center text-sm text-white">
You not have any posts to see yet
<br />
Follow more people to have more fun.
</p>
<Link
to="/app/trending"
to="/trending"
className="inline-flex w-max rounded bg-fuchsia-500 px-2.5 py-1.5 text-sm hover:bg-fuchsia-600"
>
Trending
@ -160,7 +157,7 @@ export function FollowingBlock() {
<div
className="relative w-full"
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
height: `${totalSize}px`,
}}
>
<div
@ -177,7 +174,7 @@ export function FollowingBlock() {
)}
{isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>

View File

@ -22,31 +22,35 @@ export function ThreadBlock({ params }: { params: Block }) {
// useLiveThread(params.content);
return (
<div className="w-[400px] shrink-0 border-r border-zinc-900">
<div className="scrollbar-hide h-full w-[400px] shrink-0 overflow-y-auto bg-white/10 pb-20">
<TitleBar id={params.id} title={params.title} />
<div className="scrollbar-hide flex h-full w-full flex-col gap-3 overflow-y-auto pb-20 pt-1.5">
<div className="h-full">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>
) : (
<div className="h-min w-full px-3 pt-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
<div className="rounded-xl bg-white/10 px-3 pt-3">
<ThreadUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-2">
<NoteContent content={data.content} />
</div>
<div>
<NoteActions id={data.id} pubkey={data.pubkey} noOpenThread={true} />
<NoteStats id={data.id} />
<NoteActions
id={params.content}
pubkey={data.pubkey}
noOpenThread={true}
/>
<NoteStats id={params.content} />
</div>
</div>
</div>
)}
<div className="px-3">
<NoteReplyForm id={params.content} pubkey={account.pubkey} />
{account && <NoteReplyForm id={params.content} pubkey={account.pubkey} />}
<RepliesList id={params.content} />
</div>
</div>

View File

@ -35,31 +35,28 @@ export function UserBlock({ params }: { params: Block }) {
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
return (
<div className="h-full w-[400px] shrink-0 border-r border-zinc-900">
<div className="relative w-[400px] shrink-0 bg-white/10">
<TitleBar id={params.id} title={params.title} />
<div
ref={parentRef}
className="scrollbar-hide flex h-full flex-1 flex-col gap-1.5 overflow-y-auto pt-1.5"
>
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
<div className="px-3 pt-1.5">
<UserProfile pubkey={params.content} />
</div>
<div>
<h3 className="mt-2 px-3 text-lg font-semibold text-zinc-300">
<h3 className="mt-4 px-3 text-lg font-semibold text-white">
Latest activities
</h3>
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
<div className="rounded-md bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>
) : itemsVirtualizer.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-6">
<div className="rounded-xl bg-white/10 px-3 py-6">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-zinc-300">
<p className="text-center text-sm text-white">
No new posts about this hashtag in 48 hours ago
</p>
</div>

View File

@ -0,0 +1,212 @@
import { Combobox } from '@headlessui/react';
import * as Dialog from '@radix-ui/react-dialog';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { nip19 } from 'nostr-tools';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { User } from '@app/auth/components/user';
import { createBlock } from '@libs/storage';
import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
import { ADD_FEEDBLOCK_SHORTCUT } from '@stores/shortcuts';
import { useAccount } from '@utils/hooks/useAccount';
export function FeedModal() {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [selected, setSelected] = useState([]);
const [query, setQuery] = useState('');
const { status, account } = useAccount();
useHotkeys(ADD_FEEDBLOCK_SHORTCUT, () => setOpen(true));
const block = useMutation({
mutationFn: (data: { kind: number; title: string; content: string }) => {
return createBlock(data.kind, data.title, data.content);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['blocks'] });
},
});
const {
register,
handleSubmit,
reset,
formState: { isDirty, isValid },
} = useForm();
const onSubmit = (data: { kind: number; title: string; content: string }) => {
setLoading(true);
selected.forEach((item, index) => {
if (item.substring(0, 4) === 'npub') {
selected[index] = nip19.decode(item).data;
}
});
// insert to database
block.mutate({
kind: BLOCK_KINDS.feed,
title: data.title,
content: JSON.stringify(selected),
});
setLoading(false);
// reset form
reset();
// close modal
setOpen(false);
};
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
<CommandIcon className="h-3 w-3 text-white" />
</div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
<span className="text-sm leading-none text-white">F</span>
</div>
</div>
<h5 className="font-medium text-white/50">New feed block</h5>
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10">
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<Dialog.Title className="text-lg font-semibold leading-none text-white">
Create feed block
</Dialog.Title>
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-white/10">
<CancelIcon className="h-4 w-4 text-white/50" />
</Dialog.Close>
</div>
<Dialog.Description className="text-sm leading-tight text-white/50">
Specific newsfeed space for people you want to keep up to date
</Dialog.Description>
</div>
</div>
<div className="flex flex-col overflow-y-auto overflow-x-hidden px-5 pb-5 pt-2">
<form
onSubmit={handleSubmit(onSubmit)}
className="mb-0 flex h-full w-full flex-col gap-4"
>
<div className="flex flex-col gap-1">
<label
htmlFor="title"
className="text-sm font-medium uppercase tracking-wider text-white/50"
>
Title *
</label>
<input
type={'text'}
{...register('title', {
required: true,
})}
spellCheck={false}
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/>
</div>
<div className="flex flex-col gap-1">
<span className="text-sm font-medium uppercase tracking-wider text-white/50">
Choose at least 1 user *
</span>
<div className="flex h-[300px] w-full flex-col overflow-y-auto overflow-x-hidden rounded-lg bg-white/10">
<div className="w-full px-3 py-2">
<Combobox value={selected} onChange={setSelected} multiple>
<Combobox.Input
onChange={(event) => setQuery(event.target.value)}
spellCheck={false}
placeholder="Enter pubkey or npub..."
className="relative mb-2 h-10 w-full rounded-md bg-white/10 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/>
<Combobox.Options static>
{query.length > 0 && (
<Combobox.Option
value={query}
className="group flex w-full items-center justify-between rounded-md px-2 py-2 hover:bg-white/10"
>
{({ selected }) => (
<>
<div className="flex items-center gap-2">
<img
alt={query}
src={DEFAULT_AVATAR}
className="h-11 w-11 shrink-0 rounded object-cover"
/>
<div className="inline-flex flex-col gap-1">
<span className="text-base leading-tight text-white/50">
{query}
</span>
</div>
</div>
{selected && (
<CheckCircleIcon className="h-4 w-4 text-green-500" />
)}
</>
)}
</Combobox.Option>
)}
{status === 'loading' ? (
<p>Loading...</p>
) : (
account?.follows?.map((follow) => (
<Combobox.Option
key={follow}
value={follow}
className="group flex w-full items-center justify-between rounded-md px-2 py-2 hover:bg-white/10"
>
{({ selected }) => (
<>
<User pubkey={follow} />
{selected && (
<CheckCircleIcon className="h-4 w-4 text-green-500" />
)}
</>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
</Combobox>
</div>
</div>
</div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
) : (
'Confirm'
)}
</button>
</form>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@ -0,0 +1,132 @@
import * as Dialog from '@radix-ui/react-dialog';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { createBlock } from '@libs/storage';
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
import { BLOCK_KINDS } from '@stores/constants';
import { ADD_HASHTAGBLOCK_SHORTCUT } from '@stores/shortcuts';
export function HashtagModal() {
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
useHotkeys(ADD_HASHTAGBLOCK_SHORTCUT, () => setOpen(false));
const {
register,
handleSubmit,
reset,
formState: { isDirty, isValid },
} = useForm();
const block = useMutation({
mutationFn: (data: { kind: number; title: string; content: string }) => {
return createBlock(data.kind, data.title, data.content);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['blocks'] });
},
});
const onSubmit = async (data: { hashtag: string }) => {
setLoading(true);
// mutate
block.mutate({
kind: BLOCK_KINDS.hashtag,
title: data.hashtag,
content: data.hashtag.replace('#', ''),
});
setLoading(false);
// reset form
reset();
// close modal
setOpen(false);
};
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
<CommandIcon className="h-3 w-3 text-white" />
</div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
<span className="text-sm leading-none text-white">T</span>
</div>
</div>
<h5 className="font-medium text-white/50">New hashtag block</h5>
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10">
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<Dialog.Title className="text-lg font-semibold leading-none text-white">
Create hashtag block
</Dialog.Title>
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-white/10">
<CancelIcon className="h-4 w-4 text-white/50" />
</Dialog.Close>
</div>
<Dialog.Description className="text-sm leading-tight text-white/50">
Pin the hashtag you want to keep follow up
</Dialog.Description>
</div>
</div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="mb-0 flex h-full w-full flex-col gap-3"
>
<div className="flex flex-col gap-1">
<label
htmlFor="title"
className="text-sm font-medium uppercase tracking-wider text-white/50"
>
Hashtag *
</label>
<input
type={'text'}
{...register('hashtag', {
required: true,
})}
spellCheck={false}
placeholder="#"
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/>
</div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
) : (
'Confirm'
)}
</button>
</form>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@ -0,0 +1,179 @@
import * as Dialog from '@radix-ui/react-dialog';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useEffect, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { createBlock } from '@libs/storage';
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
import { ADD_IMAGEBLOCK_SHORTCUT } from '@stores/shortcuts';
import { usePublish } from '@utils/hooks/usePublish';
import { useImageUploader } from '@utils/hooks/useUploader';
export function ImageModal() {
const queryClient = useQueryClient();
const upload = useImageUploader();
const { publish } = usePublish();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [image, setImage] = useState('');
const tags = useRef(null);
useHotkeys(ADD_IMAGEBLOCK_SHORTCUT, () => setOpen(false));
const {
register,
handleSubmit,
reset,
setValue,
formState: { isDirty, isValid },
} = useForm();
const block = useMutation({
mutationFn: (data: { kind: number; title: string; content: string }) => {
return createBlock(data.kind, data.title, data.content);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['blocks'] });
},
});
const uploadImage = async () => {
const image = await upload(null);
if (image.url) {
setImage(image.url);
}
};
const onSubmit = async (data: { kind: number; title: string; content: string }) => {
setLoading(true);
// publish file metedata
await publish({ content: data.title, kind: 1063, tags: tags.current });
// mutate
block.mutate({ kind: BLOCK_KINDS.image, title: data.title, content: data.content });
setLoading(false);
// reset form
reset();
// close modal
setOpen(false);
};
useEffect(() => {
setValue('content', image);
}, [setValue, image]);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
<CommandIcon width={12} height={12} className="text-white" />
</div>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
<span className="text-sm leading-none text-white">I</span>
</div>
</div>
<h5 className="font-medium text-white/50">New image block</h5>
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10">
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<Dialog.Title className="text-lg font-semibold leading-none text-white">
Create image block
</Dialog.Title>
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-white/10">
<CancelIcon className="h-4 w-4 text-white/50" />
</Dialog.Close>
</div>
<Dialog.Description className="text-sm leading-tight text-white/50">
Pin your favorite image to Space then you can view every time that you
use Lume, your image will be broadcast to Nostr Relay as well
</Dialog.Description>
</div>
</div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="mb-0 flex h-full w-full flex-col gap-3"
>
<input type={'hidden'} {...register('content')} value={image} />
<div className="flex flex-col gap-1">
<label
htmlFor="title"
className="text-sm font-medium uppercase tracking-wider text-white/50"
>
Title *
</label>
<input
type={'text'}
{...register('title', {
required: true,
})}
spellCheck={false}
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="picture"
className="text-sm font-medium uppercase tracking-wider text-white/50"
>
Picture
</label>
<div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg bg-white/10">
<Image
src={image}
fallback={DEFAULT_AVATAR}
alt="content"
className="relative z-10 h-auto max-h-[156px] w-[150px] rounded-md object-cover"
/>
<div className="absolute bottom-3 right-3 z-10">
<button
onClick={() => uploadImage()}
type="button"
className="inline-flex h-6 items-center justify-center rounded bg-white/10 px-3 text-sm font-medium text-white hover:bg-fuchsia-500"
>
Upload
</button>
</div>
</div>
</div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
) : (
'Confirm'
)}
</button>
</form>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@ -19,11 +19,9 @@ export function useNewsfeed() {
useEffect(() => {
if (status === 'success' && account) {
const follows = account ? JSON.parse(account.follows as string) : [];
const filter: NDKFilter = {
kinds: [1, 6],
authors: follows,
authors: account.follows,
since: now.current,
};

View File

@ -1,37 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import { AddBlock } from '@app/space/components/add';
import { FeedBlock } from '@app/space/components/blocks/feed';
import { FollowingBlock } from '@app/space/components/blocks/following';
import { HashtagBlock } from '@app/space/components/blocks/hashtag';
import { ImageBlock } from '@app/space/components/blocks/image';
import { NetworkBlock } from '@app/space/components/blocks/network';
import { ThreadBlock } from '@app/space/components/blocks/thread';
import { UserBlock } from '@app/space/components/blocks/user';
import { getBlocks } from '@libs/storage';
import { FeedModal } from '@app/space/components/modals/feed';
import { HashtagModal } from '@app/space/components/modals/hashtag';
import { ImageModal } from '@app/space/components/modals/image';
import { LoaderIcon } from '@shared/icons';
import { useBlocks } from '@stores/blocks';
import { Block } from '@utils/types';
export function SpaceScreen() {
const {
status,
data: blocks,
isFetching,
} = useQuery(
['blocks'],
async () => {
return await getBlocks();
},
{
staleTime: Infinity,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
}
);
const [blocks, fetchBlocks] = useBlocks((state) => [state.blocks, state.fetchBlocks]);
const renderBlock = useCallback(
(block: Block) => {
@ -53,41 +39,30 @@ export function SpaceScreen() {
[blocks]
);
return (
<div className="scrollbar-hide flex h-full w-full flex-nowrap overflow-x-auto overflow-y-hidden">
<FollowingBlock />
{status === 'loading' ? (
<div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900">
<div
data-tauri-drag-region
className="group flex h-11 w-full items-center justify-between overflow-hidden border-b border-zinc-900 px-3"
/>
useEffect(() => {
fetchBlocks();
}, [fetchBlocks]);
return (
<div className="scrollbar-hide flex h-full w-full flex-nowrap divide-x divide-white/5 overflow-x-auto overflow-y-hidden">
<NetworkBlock />
{!blocks ? (
<div className="flex w-[350px] shrink-0 flex-col">
<div className="flex w-full flex-1 items-center justify-center p-3">
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-5 w-5 animate-spin text-white/10" />
</div>
</div>
) : (
blocks.map((block: Block) => renderBlock(block))
blocks.map((block) => renderBlock(block))
)}
{isFetching && (
<div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900">
<div
data-tauri-drag-region
className="group flex h-11 w-full items-center justify-between overflow-hidden border-b border-zinc-900 px-3"
/>
<div className="flex w-full flex-1 items-center justify-center p-3">
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
</div>
</div>
)}
<div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900">
<div className="inline-flex h-full w-full items-center justify-center">
<AddBlock />
<div className="flex w-[350px] shrink-0 flex-col">
<div className="inline-flex h-full w-full flex-col items-center justify-center gap-1">
<FeedModal />
<ImageModal />
<HashtagModal />
</div>
</div>
<div className="w-[350px] shrink-0" />
<div className="w-[250px] shrink-0" />
</div>
);
}

58
src/app/splash.tsx Normal file
View File

@ -0,0 +1,58 @@
import { invoke } from '@tauri-apps/api/tauri';
import { platform } from '@tauri-apps/plugin-os';
import { appWindow } from '@tauri-apps/plugin-window';
import { useEffect } from 'react';
import { getActiveAccount, updateLastLogin } from '@libs/storage';
import { LoaderIcon } from '@shared/icons';
import { useNostr } from '@utils/hooks/useNostr';
const account = await getActiveAccount();
const osPlatform = await platform();
if (osPlatform !== 'macos') {
appWindow.setDecorations(false);
}
export function SplashScreen() {
const { fetchChats, fetchNotes } = useNostr();
useEffect(() => {
async function prefetch() {
const notes = await fetchNotes();
const chats = await fetchChats();
if (notes && chats) {
const now = Math.floor(Date.now() / 1000);
await updateLastLogin(now);
invoke('close_splashscreen');
}
}
if (account) {
prefetch();
}
}, []);
if (!account) {
setTimeout(() => invoke('close_splashscreen'), 1000);
}
return (
<div className="relative flex h-screen w-screen items-center justify-center bg-black">
<div data-tauri-drag-region className="absolute left-0 top-0 z-10 h-11 w-full" />
<div className="flex min-h-0 w-full flex-1 items-center justify-center">
<div className="flex flex-col items-center justify-center gap-4">
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
<div className="flex flex-col gap-1 text-center">
<h3 className="font-semibold leading-none text-white">Prefetching data</h3>
<p className="text-sm leading-none text-white/50">
This may take a few seconds, please don&apos;t close app.
</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -11,14 +11,23 @@ import { compactNumber } from '@utils/number';
import { shortenKey } from '@utils/shortenKey';
export function Profile({ data }: { data: any }) {
const { status, data: userStats } = useQuery(['user-stats', data.pubkey], async () => {
const res = await fetch(`https://api.nostr.band/v0/stats/profile/${data.pubkey}`);
return res.json();
});
const { status: socialStatus, userFollows, follow, unfollow } = useSocial();
const { status, data: userStats } = useQuery(
['user-stats', data.pubkey],
async () => {
const res = await fetch(`https://api.nostr.band/v0/stats/profile/${data.pubkey}`);
return res.json();
},
{
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
}
);
const embedProfile = data.profile ? JSON.parse(data.profile.content) : null;
const profile = embedProfile;
const { status: socialStatus, userFollows, follow, unfollow } = useSocial();
const [followed, setFollowed] = useState(false);
@ -50,29 +59,28 @@ export function Profile({ data }: { data: any }) {
}
}, [status]);
if (!profile)
if (!profile) {
return (
<div className="rounded-md bg-zinc-900 px-5 py-5">
<div className="rounded-xl bg-white/10 px-5 py-5">
<p>Can&apos;t fetch profile</p>
</div>
);
}
return (
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-5 py-5">
<div className="rounded-xl bg-white/10 px-5 py-5">
<div className="flex items-center justify-between">
<div className="inline-flex items-center gap-2">
<div className="h-11 w-11 shrink-0">
<Image
src={profile.picture}
fallback={DEFAULT_AVATAR}
className="h-11 w-11 rounded-lg object-cover"
/>
</div>
<Image
src={profile.picture}
fallback={DEFAULT_AVATAR}
className="h-11 w-11 shrink-0 rounded-lg object-cover"
/>
<div className="inline-flex flex-col gap-1">
<h3 className="max-w-[15rem] truncate font-semibold leading-none text-zinc-100">
<h3 className="max-w-[15rem] truncate font-semibold leading-none text-white">
{profile.display_name || profile.name}
</h3>
<p className="max-w-[10rem] truncate text-sm leading-none text-zinc-400">
<p className="max-w-[10rem] truncate text-sm leading-none text-white/50">
{profile.nip05 || shortenKey(data.pubkey)}
</p>
</div>
@ -81,15 +89,15 @@ export function Profile({ data }: { data: any }) {
{socialStatus === 'loading' ? (
<button
type="button"
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500"
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-white/10 hover:bg-fuchsia-500"
>
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
</button>
) : followed ? (
<button
type="button"
onClick={() => unfollowUser(data.pubkey)}
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 text-zinc-400 hover:bg-fuchsia-500 hover:text-white"
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-white/10 text-white hover:bg-fuchsia-500 hover:text-white"
>
<UnfollowIcon className="h-4 w-4" />
</button>
@ -97,7 +105,7 @@ export function Profile({ data }: { data: any }) {
<button
type="button"
onClick={() => followUser(data.pubkey)}
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 text-zinc-400 hover:bg-fuchsia-500 hover:text-white"
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-white/10 text-white hover:bg-fuchsia-500 hover:text-white"
>
<FollowIcon className="h-4 w-4" />
</button>
@ -105,7 +113,7 @@ export function Profile({ data }: { data: any }) {
</div>
</div>
<div className="mt-2">
<p className="whitespace-pre-line break-words text-zinc-100">
<p className="whitespace-pre-line break-words text-white">
{profile.about || profile.bio}
</p>
</div>
@ -115,26 +123,26 @@ export function Profile({ data }: { data: any }) {
) : (
<div className="flex w-full items-center gap-8">
<div className="inline-flex flex-col gap-1">
<span className="font-semibold leading-none text-zinc-100">
<span className="font-semibold leading-none text-white">
{userStats.stats[data.pubkey].followers_pubkey_count ?? 0}
</span>
<span className="text-sm leading-none text-zinc-400">Followers</span>
<span className="text-sm leading-none text-white/50">Followers</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="font-semibold leading-none text-zinc-100">
<span className="font-semibold leading-none text-white">
{userStats.stats[data.pubkey].pub_following_pubkey_count ?? 0}
</span>
<span className="text-sm leading-none text-zinc-400">Following</span>
<span className="text-sm leading-none text-white/50">Following</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="font-semibold leading-none text-zinc-100">
<span className="font-semibold leading-none text-white">
{userStats.stats[data.pubkey].zaps_received
? compactNumber.format(
userStats.stats[data.pubkey].zaps_received.msats / 1000
)
: 0}
</span>
<span className="text-sm leading-none text-zinc-400">Zaps received</span>
<span className="text-sm leading-none text-white/50">Zaps received</span>
</div>
</div>
)}

View File

@ -1,33 +1,52 @@
import { useQuery } from '@tanstack/react-query';
import { fetch } from '@tauri-apps/plugin-http';
import { NoteKind_1 } from '@shared/notes';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
import { LumeEvent } from '@utils/types';
interface Response {
ok: boolean;
data: {
notes: Array<{ event: LumeEvent }>;
};
}
export function TrendingNotes() {
const { status, data, error } = useQuery(['trending-notes'], async () => {
const res = await fetch('https://api.nostr.band/v0/trending/notes');
if (!res.ok) {
throw new Error('Error');
const { status, data, error } = useQuery(
['trending-notes'],
async () => {
const res: Response = await fetch('https://api.nostr.band/v0/trending/notes');
if (!res.ok) {
throw new Error('Error');
}
return res.data?.notes;
},
{
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
}
return res.json();
});
);
return (
<div className="flex w-[360px] shrink-0 flex-col border-r border-zinc-900">
<div className="scrollbar-hide relative h-full w-[400px] shrink-0 overflow-y-auto bg-white/10 pb-20">
<TitleBar title="Trending Posts" />
<div className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5">
<div className="h-full">
{error && <p>Failed to fetch</p>}
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>
) : (
<div className="relative flex w-full flex-col pt-1.5">
{data.notes.map((item) => (
<NoteKind_1 key={item.id} event={item.event} skipMetadata={true} />
<div className="relative flex w-full flex-col">
{data.map((item) => (
<NoteKind_1 key={item.event.id} event={item.event} skipMetadata={true} />
))}
</div>
)}

View File

@ -1,33 +1,50 @@
import { useQuery } from '@tanstack/react-query';
import { fetch } from '@tauri-apps/plugin-http';
import { Profile } from '@app/trending/components/profile';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
interface Response {
ok: boolean;
data: {
profiles: Array<{ pubkey: string }>;
};
}
export function TrendingProfiles() {
const { status, data, error } = useQuery(['trending-profiles'], async () => {
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
if (!res.ok) {
throw new Error('Error');
const { status, data, error } = useQuery(
['trending-profiles'],
async () => {
const res: Response = await fetch('https://api.nostr.band/v0/trending/profiles');
if (!res.ok) {
throw new Error('Error');
}
return res.data?.profiles;
},
{
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
}
return res.json();
});
);
return (
<div className="flex w-[360px] shrink-0 flex-col border-r border-zinc-900">
<div className="scrollbar-hide relative h-full w-[400px] shrink-0 overflow-y-auto bg-white/10 pb-20">
<TitleBar title="Trending Profiles" />
<div className="scrollbar-hide flex h-full w-full flex-col justify-between gap-1.5 overflow-y-auto pb-20 pt-1.5">
<div className="h-full">
{error && <p>Failed to fetch</p>}
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
<div className="rounded-xl bg-white/10 px-3 py-3">
<NoteSkeleton />
</div>
</div>
) : (
<div className="relative flex w-full flex-col gap-3 px-3 pt-3">
{data.profiles.map((item) => (
<div className="relative flex w-full flex-col gap-3 px-3 pt-1.5">
{data.map((item) => (
<Profile key={item.pubkey} data={item} />
))}
</div>

View File

@ -3,7 +3,7 @@ import { TrendingProfiles } from '@app/trending/components/trendingProfiles';
export function TrendingScreen() {
return (
<div className="scrollbar-hide flex h-full w-full flex-nowrap overflow-x-auto overflow-y-hidden">
<div className="scrollbar-hide flex h-full w-full flex-nowrap divide-x divide-white/5 overflow-x-auto overflow-y-hidden">
<TrendingProfiles />
<TrendingNotes />
</div>

View File

@ -18,32 +18,32 @@ export function UserMetadata({ pubkey }: { pubkey: string }) {
return (
<div className="flex w-full items-center gap-10">
<div className="inline-flex flex-col gap-1">
<span className="font-semibold leading-none text-zinc-100">
<span className="font-semibold leading-none text-white">
{compactNumber.format(data.stats[pubkey].followers_pubkey_count) ?? 0}
</span>
<span className="text-sm leading-none text-zinc-400">Followers</span>
<span className="text-sm leading-none text-white/50">Followers</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="font-semibold leading-none text-zinc-100">
<span className="font-semibold leading-none text-white">
{compactNumber.format(data.stats[pubkey].pub_following_pubkey_count) ?? 0}
</span>
<span className="text-sm leading-none text-zinc-400">Following</span>
<span className="text-sm leading-none text-white/50">Following</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="font-semibold leading-none text-zinc-100">
<span className="font-semibold leading-none text-white">
{data.stats[pubkey].zaps_received
? compactNumber.format(data.stats[pubkey].zaps_received.msats / 1000)
: 0}
</span>
<span className="text-sm leading-none text-zinc-400">Zaps received</span>
<span className="text-sm leading-none text-white/50">Zaps received</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="font-semibold leading-none text-zinc-100">
<span className="font-semibold leading-none text-white">
{data.stats[pubkey].zaps_sent
? compactNumber.format(data.stats[pubkey].zaps_sent.msats / 1000)
: 0}
</span>
<span className="text-sm leading-none text-zinc-400">Zaps sent</span>
<span className="text-sm leading-none text-white/50">Zaps sent</span>
</div>
</div>
);

View File

@ -52,7 +52,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
return (
<>
<div className="h-56 w-full bg-zinc-100">
<div className="h-56 w-full bg-white">
<Image
src={user?.banner}
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
@ -65,7 +65,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}
alt={pubkey}
className="h-14 w-14 rounded-md ring-2 ring-black"
className="h-14 w-14 rounded-md ring-2 ring-white/50"
/>
<div className="mt-2 flex flex-1 flex-col gap-4">
<div className="flex items-center gap-16">
@ -73,7 +73,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
<h5 className="text-lg font-semibold leading-none">
{user?.displayName || user?.name || 'No name'}
</h5>
<span className="max-w-[15rem] truncate text-sm leading-none text-zinc-500">
<span className="max-w-[15rem] truncate text-sm leading-none text-white/50">
{user?.nip05 || shortenKey(pubkey)}
</span>
</div>
@ -81,7 +81,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
{status === 'loading' ? (
<button
type="button"
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
>
Loading...
</button>
@ -89,7 +89,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
<button
type="button"
onClick={() => unfollowUser(pubkey)}
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
>
Unfollow
</button>
@ -97,23 +97,23 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
<button
type="button"
onClick={() => followUser(pubkey)}
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
>
Follow
</button>
)}
<Link
to={`/app/chats/${pubkey}`}
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
to={`/chats/${pubkey}`}
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
>
Message
</Link>
<span className="mx-2 inline-flex h-4 w-px bg-zinc-900" />
<span className="mx-2 inline-flex h-4 w-px bg-white/10" />
{account && account.pubkey === pubkey && <EditProfileModal />}
</div>
</div>
<div className="flex flex-col gap-8">
<p className="mt-2 max-w-[500px] select-text break-words text-zinc-100">
<p className="mt-2 max-w-[500px] select-text break-words text-white">
{user?.about || user?.bio}
</p>
<UserMetadata pubkey={pubkey} />

View File

@ -36,30 +36,30 @@ export function UserScreen() {
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
return (
<div ref={parentRef} className="scrollbar-hide h-full w-full overflow-y-auto">
<div
data-tauri-drag-region
className="flex h-11 w-full items-center border-b border-zinc-900 px-3"
/>
<div
ref={parentRef}
className="scrollbar-hide relative h-full w-full overflow-y-auto bg-white/10"
>
<div data-tauri-drag-region className="absolute left-0 top-0 h-11 w-full" />
<UserProfile pubkey={pubkey} />
<div className="mt-8 h-full w-full border-t border-zinc-900">
<div className="mt-8 h-full w-full border-t border-white/5 px-1.5">
<div className="flex flex-col justify-start gap-1 px-3 pt-4 text-start">
<p className="text-lg font-semibold leading-none text-zinc-200">Latest posts</p>
<span className="text-sm leading-none text-zinc-500">48 hours ago</span>
<p className="text-lg font-semibold leading-none text-white">Latest posts</p>
<span className="text-sm leading-none text-white/50">48 hours ago</span>
</div>
<div className="flex h-full max-w-[400px] flex-col justify-between gap-1.5 pb-4 pt-1.5">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
<div className="shadow-input rounded-xl bg-white/10">
<NoteSkeleton />
</div>
</div>
) : itemsVirtualizer.length === 0 ? (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-6">
<div className="rounded-xl bg-white/10 px-3 py-6">
<div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-zinc-300">
No new posts about this hashtag in 48 hours ago
<p className="text-center text-sm font-medium text-zinc-300">
No new posts in 48 hours ago
</p>
</div>
</div>

View File

@ -15,11 +15,11 @@ button {
}
.markdown {
@apply prose prose-zinc max-w-none select-text hyphens-auto dark:prose-invert prose-p:mb-2 prose-p:mt-0 prose-p:break-words prose-p:[word-break:break-word] prose-p:last:mb-0 prose-a:break-words prose-a:break-all prose-a:font-normal prose-a:leading-tight prose-a:text-fuchsia-400 prose-a:after:content-['_↗'] hover:prose-a:text-fuchsia-500 prose-blockquote:m-0 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:break-all prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-li:leading-tight prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2;
@apply prose prose-white max-w-none select-text hyphens-auto text-white prose-p:mb-2 prose-p:mt-0 prose-p:break-words prose-p:[word-break:break-word] prose-p:last:mb-0 prose-a:break-words prose-a:break-all prose-a:font-normal prose-a:leading-tight prose-a:after:content-['_↗'] hover:prose-a:text-fuchsia-500 prose-blockquote:m-0 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:break-all prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-li:leading-tight prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2;
}
.ProseMirror p.is-empty::before {
@apply text-zinc-500;
@apply text-white/50;
content: attr(data-placeholder);
float: left;
height: 0;

57
src/libs/ndk/cache.tsx Normal file
View File

@ -0,0 +1,57 @@
import { NDKCacheAdapter } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk';
import { Store } from '@tauri-apps/plugin-store';
export default class TauriAdapter implements NDKCacheAdapter {
public store: Store;
readonly locking: boolean;
constructor() {
this.store = new Store('.ndkcache.dat');
this.locking = true;
}
public async query(subscription: NDKSubscription): Promise<void> {
const { filter } = subscription;
if (filter.authors && filter.kinds) {
const promises = [];
for (const author of filter.authors) {
for (const kind of filter.kinds) {
const key = `${author}:${kind}`;
promises.push(this.store.get(key));
}
}
const results = await Promise.all(promises);
for (const result of results) {
if (result) {
const event = await this.store.get(result as string);
if (event) {
const ndkEvent = new NDKEvent(subscription.ndk, JSON.parse(event as string));
subscription.eventReceived(ndkEvent, undefined, true);
}
}
}
}
}
public async setEvent(event: NDKEvent): Promise<void> {
const nostrEvent = await event.toNostrEvent();
const key = `${nostrEvent.pubkey}:${nostrEvent.kind}`;
return new Promise((resolve) => {
Promise.all([
this.store.set(event.id, JSON.stringify(nostrEvent)),
this.store.set(key, event.id),
]).then(() => resolve());
});
}
public save() {
return this.store.save();
}
}

View File

@ -1,9 +1,10 @@
// source: https://github.com/nostr-dev-kit/ndk-react/
import NDK from '@nostr-dev-kit/ndk';
import NDK, { NDKCacheAdapter } from '@nostr-dev-kit/ndk';
import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
import { NostrFetcher, normalizeRelayUrlSet } from 'nostr-fetch';
import { useEffect, useState } from 'react';
import TauriAdapter from '@libs/ndk/cache';
import { getSetting } from '@libs/storage';
const setting = await getSetting('relays');
@ -15,11 +16,16 @@ export const NDKInstance = () => {
const [fetcher, setFetcher] = useState<NostrFetcher>(undefined);
useEffect(() => {
loadNdk(relays);
}, []);
const cacheAdapter = new TauriAdapter();
loadNdk(relays, cacheAdapter);
async function loadNdk(explicitRelayUrls: string[]) {
const ndkInstance = new NDK({ explicitRelayUrls });
return () => {
cacheAdapter.save();
};
}, [relays]);
async function loadNdk(explicitRelayUrls: string[], cacheAdapter: NDKCacheAdapter) {
const ndkInstance = new NDK({ explicitRelayUrls, cacheAdapter });
try {
await ndkInstance.connect();

44
src/libs/nip44.ts Normal file
View File

@ -0,0 +1,44 @@
// source: https://github.com/nbd-wtf/nostr-tools/blob/b1fc8ab401b8074f53e6a05a1a6a13422fb01b2d/nip44.ts
import { xchacha20 } from '@noble/ciphers/chacha';
import { secp256k1 } from '@noble/curves/secp256k1';
import { sha256 } from '@noble/hashes/sha256';
import { randomBytes } from '@noble/hashes/utils';
import { base64 } from '@scure/base';
export function getConversationKey(privkeyA: string, pubkeyB: string) {
const key = secp256k1.getSharedSecret(privkeyA, '02' + pubkeyB);
return sha256(key.slice(1, 33));
}
export function nip44Encrypt(
privkey: string,
pubkey: string,
text: string,
ver = 1
): string {
if (ver !== 1) throw new Error('NIP44: unknown encryption version');
const key = getConversationKey(privkey, pubkey);
const nonce = randomBytes(24);
const plaintext = new TextEncoder().encode(text);
const ciphertext = xchacha20(key, nonce, plaintext, plaintext);
const ctb64 = base64.encode(ciphertext);
const nonceb64 = base64.encode(nonce);
return JSON.stringify({ ciphertext: ctb64, nonce: nonceb64, v: 1 });
}
export function nip44Decrypt(privkey: string, pubkey: string, data: string): string {
const dt = JSON.parse(data);
if (dt.v !== 1) throw new Error('NIP44: unknown encryption version');
let { ciphertext, nonce } = dt;
ciphertext = base64.decode(ciphertext);
nonce = base64.decode(nonce);
const key = getConversationKey(privkey, pubkey);
const plaintext = xchacha20(key, nonce, ciphertext, ciphertext);
const text = new TextDecoder('utf-8').decode(plaintext);
return text;
}

View File

@ -24,6 +24,12 @@ export async function getActiveAccount() {
'SELECT * FROM accounts WHERE is_active = 1;'
);
if (result.length > 0) {
result[0]['follows'] = result[0].follows
? JSON.parse(result[0].follows as unknown as string)
: [];
result[0]['network'] = result[0].network
? JSON.parse(result[0].network as unknown as string)
: [];
return result[0];
} else {
return null;
@ -63,15 +69,12 @@ export async function createAccount(
}
// update account
export async function updateAccount(
column: string,
value: string | string[],
pubkey: string
) {
export async function updateAccount(column: string, value: string | string[]) {
const db = await connect();
return await db.execute(`UPDATE accounts SET ${column} = ? WHERE pubkey = ?;`, [
const account = await getActiveAccount();
return await db.execute(`UPDATE accounts SET ${column} = ? WHERE id = ?;`, [
value,
pubkey,
account.id,
]);
}
@ -305,8 +308,6 @@ export async function getChannelUsers(channel_id: string) {
export async function getChats() {
const db = await connect();
const account = await getActiveAccount();
const follows =
typeof account.follows === 'string' ? JSON.parse(account.follows) : account.follows;
const chats: { follows: Array<Chats> | null; unknowns: Array<Chats> | null } = {
follows: [],
@ -321,7 +322,7 @@ export async function getChats() {
result = result.sort((a, b) => a.new_messages - b.new_messages);
chats.follows = result.filter((el) => {
return follows.some((i) => {
return account.follows.some((i) => {
return i === el.sender_pubkey;
});
});
@ -426,10 +427,19 @@ export async function createBlock(
) {
const db = await connect();
const activeAccount = await getActiveAccount();
return await db.execute(
const insert = await db.execute(
'INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);',
[activeAccount.id, kind, title, content]
);
if (insert) {
const record: Block = await db.select(
'SELECT * FROM blocks ORDER BY id DESC LIMIT 1;'
);
return record[0];
} else {
return null;
}
}
// remove block

View File

@ -16,7 +16,7 @@ import { sendNativeNotification } from '@utils/notification';
const lastLogin = await getLastLogin();
export function ActiveAccount({ data }: { data: any }) {
export function ActiveAccount({ data }: { data: { pubkey: string; npub: string } }) {
const queryClient = useQueryClient();
const { ndk } = useNDK();
@ -88,11 +88,11 @@ export function ActiveAccount({ data }: { data: any }) {
}, []);
if (status === 'loading') {
return <div className="h-9 w-9 animate-pulse rounded-md bg-zinc-800" />;
return <div className="h-9 w-9 animate-pulse rounded-md bg-white/50" />;
}
return (
<Link to={`/app/users/${data.pubkey}`} className="relative inline-block h-9 w-9">
<Link to={`/users/${data.pubkey}`} className="relative inline-block h-9 w-9">
<Image
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}

View File

@ -1,49 +0,0 @@
import { useNavigate } from 'react-router-dom';
import { ArrowLeftIcon, ArrowRightIcon } from '@shared/icons';
export function AppHeader({ reverse }: { reverse?: boolean }) {
const navigate = useNavigate();
const goBack = () => {
navigate(-1);
};
const goForward = () => {
navigate(1);
};
return (
<div
data-tauri-drag-region
className={`flex h-11 w-full shrink-0 items-center border-b border-zinc-900 px-3 ${
reverse ? 'justify-start' : 'justify-end'
}`}
>
<div className="flex gap-2.5">
<button
type="button"
onClick={() => goBack()}
className="group inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-900"
>
<ArrowLeftIcon
width={16}
height={16}
className="text-zinc-500 group-hover:text-zinc-300"
/>
</button>
<button
type="button"
onClick={() => goForward()}
className="group inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-900"
>
<ArrowRightIcon
width={16}
height={16}
className="text-zinc-500 group-hover:text-zinc-300"
/>
</button>
</div>
</div>
);
}

View File

@ -5,12 +5,16 @@ import { Navigation } from '@shared/navigation';
export function AppLayout() {
return (
<div className="flex h-screen w-screen">
<div className="relative flex shrink-0 flex-row">
<div className="shrink-0">
<Navigation />
</div>
<div className="h-full w-full">
<div className="h-full w-full flex-1 bg-black/90">
<Outlet />
<ScrollRestoration />
<ScrollRestoration
getKey={(location) => {
return location.pathname;
}}
/>
</div>
</div>
);

View File

@ -1,66 +1,10 @@
import { platform } from '@tauri-apps/plugin-os';
import { Outlet, useNavigate } from 'react-router-dom';
import { ArrowLeftIcon, ArrowRightIcon } from '@shared/icons';
const platformName = await platform();
import { Outlet } from 'react-router-dom';
export function AuthLayout() {
const navigate = useNavigate();
const goBack = () => {
navigate(-1);
};
const goForward = () => {
navigate(1);
};
return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100">
<div className="flex h-screen w-full flex-col">
<div
data-tauri-drag-region
className="relative h-11 shrink-0 border border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
>
<div
data-tauri-drag-region
className="flex h-full w-full flex-1 items-center px-2"
>
<div
className={`flex h-full items-center gap-2 ${
platformName === 'macos' ? 'pl-[68px]' : ''
}`}
>
<button
type="button"
onClick={() => goBack()}
className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
>
<ArrowLeftIcon
width={16}
height={16}
className="text-zinc-500 group-hover:text-zinc-300"
/>
</button>
<button
type="button"
onClick={() => goForward()}
className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
>
<ArrowRightIcon
width={16}
height={16}
className="text-zinc-500 group-hover:text-zinc-300"
/>
</button>
</div>
</div>
</div>
<div className="relative flex min-h-0 w-full flex-1">
<Outlet />
</div>
</div>
<div className="relative h-screen w-screen bg-black/90">
<div className="absolute left-0 top-0 z-50 h-16 w-full" data-tauri-drag-region />
<Outlet />
</div>
);
}

View File

@ -26,9 +26,9 @@ export function AvatarUploader({ setPicture }: { setPicture: any }) {
className="inline-flex h-full w-full items-center justify-center bg-zinc-900/40"
>
{loading ? (
<LoaderIcon className="h-6 w-6 animate-spin text-zinc-100" />
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
) : (
<PlusIcon className="h-6 w-6 text-zinc-100" />
<PlusIcon className="h-6 w-6 text-white" />
)}
</button>
);

View File

@ -26,9 +26,9 @@ export function BannerUploader({ setBanner }: { setBanner: any }) {
className="inline-flex h-full w-full items-center justify-center bg-zinc-900/40"
>
{loading ? (
<LoaderIcon className="h-8 w-8 animate-spin text-zinc-100" />
<LoaderIcon className="h-8 w-8 animate-spin text-white" />
) : (
<PlusIcon className="h-8 w-8 text-zinc-100" />
<PlusIcon className="h-8 w-8 text-white" />
)}
</button>
);

View File

@ -16,19 +16,19 @@ export function Button({
switch (preset) {
case 'small':
preClass =
'w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600';
'w-min h-9 px-4 bg-white/10 rounded-md text-sm font-medium text-white hover:bg-fuchsia-500';
break;
case 'publish':
preClass =
'w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600';
'w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-white hover:bg-fuchsia-600';
break;
case 'large':
preClass =
'h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600';
'h-11 w-full bg-fuchsia-500 rounded-lg font-medium text-white hover:bg-fuchsia-600';
break;
case 'large-alt':
preClass =
'h-11 w-full bg-zinc-800 rounded-md font-medium text-zinc-300 border-t border-zinc-700/50 hover:bg-zinc-900';
'h-11 w-full bg-white/10 rounded-lg font-medium text-white hover:bg-white/20';
break;
default:
break;
@ -40,7 +40,7 @@ export function Button({
onClick={onClick}
disabled={disabled}
className={twMerge(
'inline-flex transform items-center justify-center gap-1 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50',
'inline-flex transform items-center justify-center gap-1 leading-none focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50',
preClass
)}
>

View File

@ -14,7 +14,6 @@ import { CancelIcon, LoaderIcon, PlusCircleIcon } from '@shared/icons';
import { MentionNote } from '@shared/notes';
import { useComposer } from '@stores/composer';
import { FULL_RELAYS } from '@stores/constants';
import { usePublish } from '@utils/hooks/usePublish';
import { useImageUploader } from '@utils/hooks/useUploader';
@ -45,7 +44,7 @@ export function Composer() {
Image.configure({
HTMLAttributes: {
class:
'rounded-lg w-2/3 h-auto border border-zinc-800 outline outline-2 outline-offset-0 outline-zinc-700 ml-1',
'rounded-lg w-2/3 h-auto border border-white/10 outline outline-2 outline-offset-0 outline-white/20 ml-1',
},
}),
],
@ -54,7 +53,7 @@ export function Composer() {
attributes: {
class: twMerge(
'scrollbar-hide markdown break-all max-h-[500px] overflow-y-auto outline-none pr-2',
`${reply.id ? '!min-h-42' : '!min-h-[100px]'}`
`${reply.id ? '!min-h-42' : '!min-h-[120px]'}`
),
},
},
@ -88,13 +87,13 @@ export function Composer() {
if (reply.id && reply.pubkey) {
if (reply.root && reply.root.length > 1) {
tags = [
['e', reply.root, FULL_RELAYS[0], 'root'],
['e', reply.id, FULL_RELAYS[0], 'reply'],
['e', reply.root, 'wss://relayable.org', 'root'],
['e', reply.id, 'wss://relayable.org', 'reply'],
['p', reply.pubkey],
];
} else {
tags = [
['e', reply.id, FULL_RELAYS[0], 'reply'],
['e', reply.id, 'wss://relayable.org', 'reply'],
['p', reply.pubkey],
];
}
@ -131,7 +130,7 @@ export function Composer() {
<div className="flex h-full flex-col px-4 pb-4">
<div className="flex h-full w-full gap-3">
<div className="flex w-8 shrink-0 items-center justify-center">
<div className="h-full w-[2px] bg-zinc-800" />
<div className="h-full w-[2px] bg-white/10" />
</div>
<div className="w-full">
<EditorContent
@ -147,9 +146,9 @@ export function Composer() {
<button
type="button"
onClick={() => clearReply()}
className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center gap-2 rounded bg-zinc-800 px-2 hover:bg-zinc-700"
className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded bg-white/10 px-2"
>
<CancelIcon className="h-4 w-4 text-zinc-100" />
<CancelIcon className="h-4 w-4 text-white" />
</button>
</div>
)}
@ -159,15 +158,15 @@ export function Composer() {
<button
type="button"
onClick={() => uploadImage()}
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-800"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-white/10"
>
<PlusCircleIcon className="h-5 w-5 text-zinc-500" />
<PlusCircleIcon className="h-5 w-5 text-white/50" />
</button>
<Button onClick={() => submit()} preset="publish">
{status === 'loading' ? (
<LoaderIcon className="h-4 w-4 animate-spin text-zinc-100" />
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
) : (
'Publish'
'Postr'
)}
</Button>
</div>

View File

@ -17,12 +17,12 @@ export function MentionItem({ profile }: { profile: Profile }) {
/>
</div>
<div className="flex flex-col gap-px">
<h5 className="max-w-[15rem] text-sm font-medium leading-none text-zinc-100">
<h5 className="max-w-[15rem] text-sm font-medium leading-none text-white">
{profile.ident || (
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)}
</h5>
<span className="text-sm leading-none text-zinc-400">
<span className="text-sm leading-none text-white/50">
{displayNpub(profile.pubkey, 16)}
</span>
</div>

View File

@ -1,8 +1,6 @@
import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from 'react';
import * as Dialog from '@radix-ui/react-dialog';
import { useHotkeys } from 'react-hotkeys-hook';
import { Button } from '@shared/button';
import { Composer, ComposerUser } from '@shared/composer';
import {
CancelIcon,
@ -20,71 +18,45 @@ export function ComposerModal() {
const { account } = useAccount();
const [toggle, open] = useComposer((state) => [state.toggleModal, state.open]);
const closeModal = () => {
toggle(false);
};
useHotkeys(COMPOSE_SHORTCUT, () => toggle(true));
return (
<>
<Button onClick={() => toggle(true)} preset="small">
<ComposeIcon className="h-4 w-4" />
Compose
</Button>
<Transition appear show={open} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="relative h-min w-full max-w-xl rounded-xl border-t border-zinc-800/50 bg-zinc-900">
<div className="flex items-center justify-between px-4 py-4">
<div className="flex items-center gap-2">
{account && <ComposerUser pubkey={account.pubkey} />}
<span>
<ChevronRightIcon
width={14}
height={14}
className="text-zinc-500"
/>
</span>
<div className="inline-flex h-7 w-max items-center justify-center gap-0.5 rounded bg-zinc-800 pl-3 pr-1.5 text-sm font-medium text-zinc-400">
New Post
<ChevronDownIcon className="h-4 w-4" />
</div>
</div>
<button
onClick={closeModal}
type="button"
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
>
<CancelIcon width={16} height={16} className="text-zinc-500" />
</button>
<Dialog.Root open={open} onOpenChange={toggle}>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-9 w-min items-center justify-center gap-1 rounded-md bg-white/10 px-4 text-sm font-medium text-white hover:bg-fuchsia-500 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
>
<ComposeIcon className="h-4 w-4" />
Postr
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-2xl rounded-xl bg-white/10">
<div className="flex items-center justify-between px-4 py-4">
<div className="flex items-center gap-2">
{account && <ComposerUser pubkey={account.pubkey} />}
<span>
<ChevronRightIcon className="h-4 w-4 text-white/50" />
</span>
<div className="inline-flex h-7 w-max items-center justify-center gap-0.5 rounded bg-white/10 pl-3 pr-1.5 text-sm font-medium text-white">
New Post
<ChevronDownIcon className="h-4 w-4" />
</div>
<Composer />
</Dialog.Panel>
</Transition.Child>
</div>
<Dialog.Close
onClick={() => toggle(false)}
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-800"
>
<CancelIcon className="h-5 w-5 text-white/50" />
</Dialog.Close>
</div>
<Composer />
</div>
</Dialog>
</Transition>
</>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@ -9,15 +9,13 @@ export function ComposerUser({ pubkey }: { pubkey: string }) {
return (
<div className="flex items-center gap-3">
<div className="h-8 w-8 shrink-0 overflow-hidden rounded-md bg-zinc-900">
<Image
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}
alt={pubkey}
className="h-8 w-8 object-cover"
/>
</div>
<h5 className="text-base font-semibold leading-none text-zinc-100">
<Image
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}
alt={pubkey}
className="h-8 w-8 shrink-0 rounded-md object-cover"
/>
<h5 className="text-base font-semibold leading-none text-white">
{user?.nip05 || user?.name || (
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)}

Some files were not shown because too many files have changed in this diff Show More