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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lume</title> <title>Lume</title>
</head> </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> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>

View File

@ -18,13 +18,18 @@
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.7.16", "@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", "@nostr-fetch/adapter-ndk": "^0.11.0",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-tooltip": "^1.0.6", "@radix-ui/react-tooltip": "^1.0.6",
"@tanstack/react-query": "^4.32.0", "@scure/base": "^1.1.1",
"@tanstack/react-query-devtools": "^4.32.0", "@tanstack/react-query": "^4.32.6",
"@tanstack/react-query-devtools": "^4.32.6",
"@tanstack/react-virtual": "3.0.0-beta.54", "@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "2.0.0-alpha.5", "@tauri-apps/api": "2.0.0-alpha.5",
"@tauri-apps/plugin-app": "github:tauri-apps/tauri-plugin-app#v2", "@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-notification": "github:tauri-apps/tauri-plugin-notification#v2",
"@tauri-apps/plugin-os": "github:tauri-apps/tauri-plugin-os#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-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-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-stronghold": "github:tauri-apps/tauri-plugin-stronghold#v2",
"@tauri-apps/plugin-upload": "github:tauri-apps/tauri-plugin-upload#v2", "@tauri-apps/plugin-upload": "github:tauri-apps/tauri-plugin-upload#v2",
"@tauri-apps/plugin-window": "2.0.0-alpha.0", "@tauri-apps/plugin-window": "2.0.0-alpha.0",
@ -55,12 +62,14 @@
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"immer": "^10.0.2", "immer": "^10.0.2",
"light-bolt11-decoder": "^3.0.0", "light-bolt11-decoder": "^3.0.0",
"lru-cache": "^10.0.0",
"million": "2.5.4-beta.2",
"nostr-fetch": "^0.12.2", "nostr-fetch": "^0.12.2",
"nostr-tools": "^1.13.1", "nostr-tools": "^1.14.0",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.45.2", "react-hook-form": "^7.45.4",
"react-hotkeys-hook": "^4.4.1", "react-hotkeys-hook": "^4.4.1",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-player": "^2.12.0", "react-player": "^2.12.0",
@ -70,15 +79,15 @@
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"tauri-controls": "^0.0.5", "tauri-controls": "^0.0.5",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"zustand": "^4.3.9" "zustand": "^4.4.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@tauri-apps/cli": "2.0.0-alpha.10", "@tauri-apps/cli": "2.0.0-alpha.10",
"@trivago/prettier-plugin-sort-imports": "^4.2.0", "@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/html-to-text": "^9.0.1", "@types/html-to-text": "^9.0.1",
"@types/node": "^18.17.1", "@types/node": "^18.17.3",
"@types/react": "^18.2.17", "@types/react": "^18.2.18",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/youtube-player": "^5.5.7", "@types/youtube-player": "^5.5.7",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
@ -90,9 +99,9 @@
"csstype": "^3.1.2", "csstype": "^3.1.2",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"eslint": "^8.46.0", "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-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", "eslint-plugin-simple-import-sort": "^10.0.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"lint-staged": "^13.2.3", "lint-staged": "^13.2.3",
@ -103,7 +112,7 @@
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vite": "^4.4.7", "vite": "^4.4.8",
"vite-plugin-top-level-await": "^1.3.1", "vite-plugin-top-level-await": "^1.3.1",
"vite-tsconfig-paths": "^4.2.0" "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" checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854"
dependencies = [ dependencies = [
"clipboard-win", "clipboard-win",
"core-graphics", "core-graphics 0.22.3",
"image", "image",
"log", "log",
"objc", "objc",
@ -309,7 +309,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -326,7 +326,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -641,9 +641,12 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.79" version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "cesu8" name = "cesu8"
@ -757,7 +760,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -787,8 +790,24 @@ dependencies = [
"block", "block",
"cocoa-foundation", "cocoa-foundation",
"core-foundation", "core-foundation",
"core-graphics", "core-graphics 0.22.3",
"foreign-types", "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", "libc",
"objc", "objc",
] ]
@ -803,7 +822,7 @@ dependencies = [
"block", "block",
"core-foundation", "core-foundation",
"core-graphics-types", "core-graphics-types",
"foreign-types", "foreign-types 0.3.2",
"libc", "libc",
"objc", "objc",
] ]
@ -901,7 +920,20 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"core-graphics-types", "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", "libc",
] ]
@ -1036,7 +1068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -1092,7 +1124,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -1103,7 +1135,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -1117,6 +1149,15 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "deranged"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "derivative" name = "derivative"
version = "2.2.0" version = "2.2.0"
@ -1317,7 +1358,7 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -1328,9 +1369,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.1" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f"
dependencies = [ dependencies = [
"errno-dragonfly", "errno-dragonfly",
"libc", "libc",
@ -1396,7 +1437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"rustix 0.38.4", "rustix 0.38.7",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -1421,13 +1462,13 @@ dependencies = [
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.21" version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall 0.2.16", "redox_syscall 0.3.5",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -1465,7 +1506,28 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [ 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]] [[package]]
@ -1474,6 +1536,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.0" version = "1.2.0"
@ -1575,7 +1643,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -1874,9 +1942,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "globset" name = "globset"
version = "0.4.12" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006" checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"bstr", "bstr",
@ -2224,6 +2292,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.0", "hashbrown 0.14.0",
"serde",
] ]
[[package]] [[package]]
@ -2314,6 +2383,15 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" 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]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.9" version = "0.4.9"
@ -2321,10 +2399,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"rustix 0.38.4", "rustix 0.38.7",
"windows-sys 0.48.0", "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]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.10.5"
@ -2523,9 +2611,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.3" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -2562,7 +2650,7 @@ dependencies = [
name = "lume" name = "lume"
version = "1.1.1" version = "1.1.1"
dependencies = [ dependencies = [
"cocoa", "cocoa 0.24.1",
"objc", "objc",
"rand 0.8.5", "rand 0.8.5",
"rust-argon2", "rust-argon2",
@ -2580,12 +2668,16 @@ dependencies = [
"tauri-plugin-notification", "tauri-plugin-notification",
"tauri-plugin-os", "tauri-plugin-os",
"tauri-plugin-process", "tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-single-instance", "tauri-plugin-single-instance",
"tauri-plugin-sql", "tauri-plugin-sql",
"tauri-plugin-store",
"tauri-plugin-stronghold", "tauri-plugin-stronghold",
"tauri-plugin-updater", "tauri-plugin-updater",
"tauri-plugin-upload", "tauri-plugin-upload",
"tauri-plugin-window", "tauri-plugin-window",
"window-shadows",
"window-vibrancy",
] ]
[[package]] [[package]]
@ -2996,6 +3088,17 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 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]] [[package]]
name = "ordered-stream" name = "ordered-stream"
version = "0.2.0" version = "0.2.0"
@ -3017,6 +3120,16 @@ dependencies = [
"winapi", "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]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -3084,6 +3197,12 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]] [[package]]
name = "pbkdf2" name = "pbkdf2"
version = "0.11.0" version = "0.11.0"
@ -3223,7 +3342,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -3589,13 +3708,13 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.9.1" version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata 0.3.4", "regex-automata 0.3.6",
"regex-syntax 0.7.4", "regex-syntax 0.7.4",
] ]
@ -3610,9 +3729,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.3.4" version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -3785,22 +3904,22 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.4" version = "0.38.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399"
dependencies = [ dependencies = [
"bitflags 2.3.3", "bitflags 2.3.3",
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.3", "linux-raw-sys 0.4.5",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.21.5" version = "0.21.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36" checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb"
dependencies = [ dependencies = [
"ring", "ring",
"rustls-webpki", "rustls-webpki",
@ -3930,22 +4049,22 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.178" version = "1.0.181"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60363bdd39a7be0266a520dab25fdc9241d2f987b08a01e01f0ec6d06a981348" checksum = "6d3e73c93c3240c0bda063c239298e633114c69a888c3e37ca8bb33f343e9890"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.178" version = "1.0.181"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28482318d6641454cb273da158647922d1be6b5a2fcc6165cd89ebdd7ed576b" checksum = "be02f6cb0cd3a5ec20bbcfbcbd749f57daddb1a0882dc2e46a6c236c90b977ed"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -3967,7 +4086,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -3993,14 +4112,15 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.1.0" version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3" checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49"
dependencies = [ dependencies = [
"base64 0.21.2", "base64 0.21.2",
"chrono", "chrono",
"hex", "hex",
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.0.0",
"serde", "serde",
"serde_json", "serde_json",
"serde_with_macros", "serde_with_macros",
@ -4009,14 +4129,14 @@ dependencies = [
[[package]] [[package]]
name = "serde_with_macros" name = "serde_with_macros"
version = "3.1.0" version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65" checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -4095,6 +4215,16 @@ dependencies = [
"lazy_static", "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]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.17" version = "0.3.17"
@ -4610,9 +4740,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.27" version = "2.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4651,9 +4781,9 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"cairo-rs", "cairo-rs",
"cc", "cc",
"cocoa", "cocoa 0.24.1",
"core-foundation", "core-foundation",
"core-graphics", "core-graphics 0.22.3",
"crossbeam-channel", "crossbeam-channel",
"dirs-next", "dirs-next",
"dispatch", "dispatch",
@ -4715,9 +4845,9 @@ dependencies = [
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.10" version = "0.12.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e" checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a"
[[package]] [[package]]
name = "tauri" name = "tauri"
@ -4727,7 +4857,7 @@ checksum = "2e18377a75e314aa1d476896af881ed63f57a78ca84889fe63e69067f0de158d"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
"cocoa", "cocoa 0.24.1",
"dirs-next", "dirs-next",
"embed_plist", "embed_plist",
"futures-util", "futures-util",
@ -4829,7 +4959,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-app" name = "tauri-plugin-app"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"tauri", "tauri",
] ]
@ -4837,7 +4967,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-autostart" name = "tauri-plugin-autostart"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"auto-launch", "auto-launch",
"log", "log",
@ -4850,7 +4980,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-clipboard-manager" name = "tauri-plugin-clipboard-manager"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"arboard", "arboard",
"log", "log",
@ -4864,7 +4994,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"glib", "glib",
"log", "log",
@ -4881,7 +5011,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs" name = "tauri-plugin-fs"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",
@ -4894,7 +5024,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-http" name = "tauri-plugin-http"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"bytes", "bytes",
"glob", "glob",
@ -4912,7 +5042,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-notification" name = "tauri-plugin-notification"
version = "2.0.0-alpha.1" 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 = [ dependencies = [
"log", "log",
"notify-rust", "notify-rust",
@ -4930,7 +5060,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-os" name = "tauri-plugin-os"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"gethostname 0.4.3", "gethostname 0.4.3",
"log", "log",
@ -4946,15 +5076,32 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-process" name = "tauri-plugin-process"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"tauri", "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]] [[package]]
name = "tauri-plugin-single-instance" name = "tauri-plugin-single-instance"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"log", "log",
"serde", "serde",
@ -4968,7 +5115,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-sql" name = "tauri-plugin-sql"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"futures-core", "futures-core",
"log", "log",
@ -4981,10 +5128,22 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tauri-plugin-stronghold" name = "tauri-plugin-stronghold"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"hex", "hex",
"iota-crypto 0.23.0", "iota-crypto 0.23.0",
@ -5000,7 +5159,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-updater" name = "tauri-plugin-updater"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"base64 0.21.2", "base64 0.21.2",
"dirs-next", "dirs-next",
@ -5027,7 +5186,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-upload" name = "tauri-plugin-upload"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
@ -5044,7 +5203,7 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-window" name = "tauri-plugin-window"
version = "2.0.0-alpha.0" 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 = [ dependencies = [
"serde", "serde",
"tauri", "tauri",
@ -5078,7 +5237,7 @@ version = "0.13.0-alpha.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1231be42085f3a8b150e615601f8a070bd16bf579771a5dafe2931970a05b518" checksum = "1231be42085f3a8b150e615601f8a070bd16bf579771a5dafe2931970a05b518"
dependencies = [ dependencies = [
"cocoa", "cocoa 0.24.1",
"gtk", "gtk",
"jni", "jni",
"percent-encoding", "percent-encoding",
@ -5151,7 +5310,7 @@ dependencies = [
"cfg-if", "cfg-if",
"fastrand 2.0.0", "fastrand 2.0.0",
"redox_syscall 0.3.5", "redox_syscall 0.3.5",
"rustix 0.38.4", "rustix 0.38.7",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -5189,7 +5348,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -5215,10 +5374,11 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.23" version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea"
dependencies = [ dependencies = [
"deranged",
"itoa 1.0.9", "itoa 1.0.9",
"serde", "serde",
"time-core", "time-core",
@ -5233,9 +5393,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.10" version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd"
dependencies = [ dependencies = [
"time-core", "time-core",
] ]
@ -5281,7 +5441,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -5370,7 +5530,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]
@ -5633,7 +5793,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -5667,7 +5827,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -5844,6 +6004,28 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "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]] [[package]]
name = "windows" name = "windows"
version = "0.36.1" version = "0.36.1"
@ -6128,9 +6310,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.5.1" version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25b5872fa2e10bd067ae946f927e726d7d603eaeb6e02fa6a350e0722d2b8c11" checksum = "acaaa1190073b2b101e15083c38ee8ec891b5e05cbee516521e94ec008f61e64"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -6162,8 +6344,8 @@ checksum = "7d15f9f827d537cefe6d047be3930f5d89b238dfb85e08ba6a319153217635aa"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"block", "block",
"cocoa", "cocoa 0.24.1",
"core-graphics", "core-graphics 0.22.3",
"crossbeam-channel", "crossbeam-channel",
"dunce", "dunce",
"gdk", "gdk",
@ -6349,7 +6531,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.27", "syn 2.0.28",
] ]
[[package]] [[package]]

View File

@ -16,7 +16,10 @@ tauri-build = { version = "2.0.0-alpha.6", features = [] }
[dependencies] [dependencies]
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = { version = "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-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-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-stronghold = { 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-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-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-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 = [ sqlx-cli = { version = "0.7.0", default-features = false, features = [
"sqlite", "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" windows_subsystem = "windows"
)] )]
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
// use rand::distributions::{Alphanumeric, DistString}; // use rand::distributions::{Alphanumeric, DistString};
use tauri::{Manager, WindowEvent}; use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_sql::{Migration, MigrationKind}; use tauri_plugin_sql::{Migration, MigrationKind};
use window_shadows::set_shadow;
#[cfg(target_os = "macos")] use window_vibrancy::{apply_mica, apply_vibrancy, NSVisualEffectMaterial};
use window_ext::WindowExt;
#[cfg(target_os = "macos")]
mod window_ext;
#[derive(Clone, serde::Serialize)] #[derive(Clone, serde::Serialize)]
struct Payload { struct Payload {
@ -23,6 +16,16 @@ struct Payload {
cwd: String, 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() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.plugin( .plugin(
@ -96,6 +99,12 @@ fn main() {
sql: include_str!("../migrations/20230725010250_update_default_relays.sql"), sql: include_str!("../migrations/20230725010250_update_default_relays.sql"),
kind: MigrationKind::Up, 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(), .build(),
@ -144,29 +153,25 @@ fn main() {
.plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_window::init()) .plugin(tauri_plugin_window::init())
.plugin(tauri_plugin_store::Builder::default().build())
.plugin(tauri_plugin_shell::init())
.setup(|app| { .setup(|app| {
#[cfg(target_os = "macos")] let window = app.get_window("main").unwrap();
let main_window = app.get_window("main").unwrap();
// native shadow
set_shadow(&window, true).expect("Unsupported platform!");
#[cfg(target_os = "macos")] #[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(()) Ok(())
}) })
.on_window_event(|e| { .invoke_handler(tauri::generate_handler![close_splashscreen])
#[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(),
_ => {}
}
})
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .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": [ "windows": [
{ {
"fullscreen": false, "width": 1080,
"height": 800, "height": 800,
"hiddenTitle": true,
"minHeight": 720,
"minWidth": 1080, "minWidth": 1080,
"minHeight": 800,
"resizable": true, "resizable": true,
"theme": "Dark", "theme": "Dark",
"title": "Lume", "title": "Lume",
"titleBarStyle": "Overlay", "titleBarStyle": "Overlay",
"transparent": false, "transparent": true,
"width": 1080 "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 { AuthCreateScreen } from '@app/auth/create';
import { CreateStep1Screen } from '@app/auth/create/step-1'; 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 { ResetScreen } from '@app/auth/reset';
import { UnlockScreen } from '@app/auth/unlock'; import { UnlockScreen } from '@app/auth/unlock';
import { WelcomeScreen } from '@app/auth/welcome'; import { WelcomeScreen } from '@app/auth/welcome';
import { ChannelScreen } from '@app/channel';
import { ChatScreen } from '@app/chats'; import { ChatScreen } from '@app/chats';
import { ErrorScreen } from '@app/error'; import { ErrorScreen } from '@app/error';
import { NoteScreen } from '@app/note'; import { EventScreen } from '@app/events';
import { Root } from '@app/root';
import { AccountSettingsScreen } from '@app/settings/account'; import { AccountSettingsScreen } from '@app/settings/account';
import { GeneralSettingsScreen } from '@app/settings/general'; import { GeneralSettingsScreen } from '@app/settings/general';
import { ShortcutsSettingsScreen } from '@app/settings/shortcuts'; import { ShortcutsSettingsScreen } from '@app/settings/shortcuts';
import { SpaceScreen } from '@app/space'; import { SpaceScreen } from '@app/space';
import { SplashScreen } from '@app/splash';
import { TrendingScreen } from '@app/trending'; import { TrendingScreen } from '@app/trending';
import { UserScreen } from '@app/users'; import { UserScreen } from '@app/users';
import { getActiveAccount } from '@libs/storage';
import { AppLayout } from '@shared/appLayout'; import { AppLayout } from '@shared/appLayout';
import { AuthLayout } from '@shared/authLayout'; import { AuthLayout } from '@shared/authLayout';
import { Protected } from '@shared/protected'; import { LoaderIcon } from '@shared/icons';
import { SettingsLayout } from '@shared/settingsLayout'; import { SettingsLayout } from '@shared/settingsLayout';
import './index.css'; 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([ const router = createBrowserRouter([
{ {
path: '/', path: '/',
element: ( element: <AppLayout />,
// @ts-expect-error, todo errorElement: <ErrorScreen />,
<Protected> loader: appLoader,
<Root /> children: [
</Protected> { 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 />, errorElement: <ErrorScreen />,
}, },
{ {
@ -76,31 +105,9 @@ const router = createBrowserRouter([
{ path: 'reset', element: <ResetScreen /> }, { 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', path: '/settings',
element: ( element: <SettingsLayout />,
// @ts-expect-error, todo
<Protected>
<SettingsLayout />
</Protected>
),
children: [ children: [
{ path: 'general', element: <GeneralSettingsScreen /> }, { path: 'general', element: <GeneralSettingsScreen /> },
{ path: 'shortcuts', element: <ShortcutsSettingsScreen /> }, { path: 'shortcuts', element: <ShortcutsSettingsScreen /> },
@ -113,7 +120,11 @@ export default function App() {
return ( return (
<RouterProvider <RouterProvider
router={router} 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 }} future={{ v7_startTransition: true }}
/> />
); );

View File

@ -11,10 +11,10 @@ export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }
if (status === 'loading') { if (status === 'loading') {
return ( return (
<div className="flex items-center gap-2"> <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"> <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-4 w-1/2 animate-pulse rounded bg-white/10" />
<span className="h-3 w-1/3 animate-pulse rounded bg-zinc-800" /> <span className="h-3 w-1/3 animate-pulse rounded bg-white/10" />
</div> </div>
</div> </div>
); );
@ -31,10 +31,10 @@ export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }
/> />
</div> </div>
<div className="flex w-full flex-1 flex-col items-start text-start"> <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} {user?.name || user?.displayName || user?.display_name}
</span> </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)} {user?.nip05?.toLowerCase() || shortenKey(pubkey)}
</span> </span>
</div> </div>

View File

@ -37,13 +37,9 @@ export function CreateStep1Screen() {
}; };
const download = async () => { const download = async () => {
await writeTextFile( await writeTextFile('lume-keys.txt', `Public key: ${npub}\nPrivate key: ${nsec}`, {
'lume-keys.txt',
`Public key: ${pubkey}\nPrivate key: ${privkey}`,
{
dir: BaseDirectory.Download, dir: BaseDirectory.Download,
} });
);
setDownloaded(true); setDownloaded(true);
}; };
@ -81,47 +77,39 @@ export function CreateStep1Screen() {
return ( return (
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <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>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-1"> <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 <input
readOnly readOnly
value={npub} 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>
<div className="flex flex-col gap-1"> <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"> <div className="relative">
<input <input
readOnly readOnly
type={privkeyInput} type={privkeyInput}
value={nsec} 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 <button
type="button" type="button"
onClick={() => showPrivateKey()} onClick={() => showPrivateKey()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700" className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-white/10"
> >
{privkeyInput === 'password' ? ( {privkeyInput === 'password' ? (
<EyeOffIcon <EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
) : ( ) : (
<EyeOnIcon <EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
)} )}
</button> </button>
</div> </div>
<div className="mt-2 text-sm text-zinc-500"> <div className="mt-2 text-sm text-white/50">
<p> <p>
Your private key is your password. If you lose this key, you will lose 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 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"> <div className="flex flex-col gap-2">
<Button preset="large" onClick={() => submit()}> <Button preset="large" onClick={() => submit()}>
{loading ? ( {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 →' 'I have saved my key, continue →'
)} )}
</Button> </Button>
{downloaded ? ( {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()}> <Button preset="large-alt" onClick={() => download()}>
Download Download

View File

@ -74,7 +74,7 @@ export function CreateStep2Screen() {
return ( return (
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <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 Set password to secure your key
</h1> </h1>
</div> </div>
@ -85,29 +85,21 @@ export function CreateStep2Screen() {
<input <input
{...register('password', { required: true })} {...register('password', { required: true })}
type={passwordInput} 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 <button
type="button" type="button"
onClick={() => showPassword()} 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' ? ( {passwordInput === 'password' ? (
<EyeOffIcon <EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
) : ( ) : (
<EyeOnIcon <EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
)} )}
</button> </button>
</div> </div>
<div className="text-sm text-zinc-500"> <div className="text-sm text-white/50">
<p> <p>
Password is use to secure your key store in local machine, when you move 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 to other clients, you just need to copy your private key as nsec or
@ -122,10 +114,10 @@ export function CreateStep2Screen() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} 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 ? ( {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 →' 'Continue →'
)} )}

View File

@ -45,22 +45,12 @@ export function CreateStep3Screen() {
return ( return (
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <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>
<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"> <form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
<input <input type={'hidden'} {...register('picture')} value={picture} />
type={'hidden'} <input type={'hidden'} {...register('banner')} value={banner} />
{...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"
/>
<div className="relative"> <div className="relative">
<div className="relative h-44 w-full bg-zinc-800"> <div className="relative h-44 w-full bg-zinc-800">
<Image <Image
@ -79,7 +69,7 @@ export function CreateStep3Screen() {
src={picture} src={picture}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
alt="user's 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"> <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} /> <AvatarUploader setPicture={setPicture} />
@ -91,7 +81,7 @@ export function CreateStep3Screen() {
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label <label
htmlFor="name" 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 * Name *
</label> </label>
@ -102,26 +92,26 @@ export function CreateStep3Screen() {
minLength: 4, minLength: 4,
})} })}
spellCheck={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> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label <label
htmlFor="about" 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 Bio
</label> </label>
<textarea <textarea
{...register('about')} {...register('about')}
spellCheck={false} 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>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label <label
htmlFor="website" 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 Website
</label> </label>
@ -131,16 +121,16 @@ export function CreateStep3Screen() {
required: false, required: false,
})} })}
spellCheck={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> </div>
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} 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 ? ( {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 →' 'Continue →'
)} )}

View File

@ -53,10 +53,10 @@ export function CreateStep4Screen() {
return ( return (
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <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>
<div className="flex w-full flex-col items-center justify-center gap-4"> <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 <input
type="text" type="text"
value={username} value={username}
@ -65,7 +65,7 @@ export function CreateStep4Screen() {
autoCorrect="none" autoCorrect="none"
spellCheck="false" spellCheck="false"
placeholder="satoshi" 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> <span className="pr-3.5 font-semibold text-fuchsia-500">@lume.nu</span>
</div> </div>
@ -75,7 +75,7 @@ export function CreateStep4Screen() {
disabled={username.length === 0} disabled={username.length === 0}
> >
{loading ? ( {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 →' 'Continue →'
)} )}

View File

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

View File

@ -98,17 +98,17 @@ export function ImportStep1Screen() {
return ( return (
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <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>
<div className="flex flex-col gap-4"> <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"> <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 <input
{...register('privkey', { required: true, minLength: 32 })} {...register('privkey', { required: true, minLength: 32 })}
type={'password'} type={'password'}
placeholder="nsec or hexstring" 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"> <span className="text-sm text-red-400">
{errors.privkey && <p>{errors.privkey.message}</p>} {errors.privkey && <p>{errors.privkey.message}</p>}
@ -118,10 +118,10 @@ export function ImportStep1Screen() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} 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 ? ( {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 →' 'Continue →'
)} )}

View File

@ -74,7 +74,7 @@ export function ImportStep2Screen() {
return ( return (
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <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 Set password to secure your key
</h1> </h1>
</div> </div>
@ -85,29 +85,21 @@ export function ImportStep2Screen() {
<input <input
{...register('password', { required: true })} {...register('password', { required: true })}
type={passwordInput} 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 <button
type="button" type="button"
onClick={() => showPassword()} 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' ? ( {passwordInput === 'password' ? (
<EyeOffIcon <EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
) : ( ) : (
<EyeOnIcon <EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
)} )}
</button> </button>
</div> </div>
<div className="text-sm text-zinc-500"> <div className="text-sm text-white/50">
<p> <p>
Password is use to unlock app and secure your key store in local machine. 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 When you move to other clients, you just need to copy your private key as
@ -122,10 +114,10 @@ export function ImportStep2Screen() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} 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 ? ( {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 →' '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 { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user'; import { User } from '@app/auth/components/user';
import { useNDK } from '@libs/ndk/provider'; import { updateLastLogin } from '@libs/storage';
import { updateAccount } from '@libs/storage';
import { Button } from '@shared/button';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { useAccount } from '@utils/hooks/useAccount'; import { useAccount } from '@utils/hooks/useAccount';
import { setToArray } from '@utils/transform'; import { useNostr } from '@utils/hooks/useNostr';
export function ImportStep3Screen() { export function ImportStep3Screen() {
const queryClient = useQueryClient();
const navigate = useNavigate(); const navigate = useNavigate();
const queryClient = useQueryClient();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { ndk } = useNDK();
const { status, account } = useAccount(); const { status, account } = useAccount();
const { fetchNotes, fetchChats } = useNostr();
const update = useMutation({
mutationFn: (follows: string[]) => {
return updateAccount('follows', follows, account.pubkey);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
},
});
const submit = async () => { const submit = async () => {
try { try {
// show loading indicator // show loading indicator
setLoading(true); setLoading(true);
const user = ndk.getUser({ hexpubkey: account.pubkey }); const now = Math.floor(Date.now() / 1000);
const follows = await user.follows(); await fetchNotes();
await fetchChats();
await updateLastLogin(now);
// follows as list queryClient.invalidateQueries(['currentAccount']);
const followsList = setToArray(follows);
// update navigate('/', { replace: true });
update.mutate([...followsList, account.pubkey]); } catch (e) {
console.log('error: ', e);
// redirect to next step setLoading(false);
setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1200);
} catch {
console.log('error');
} }
}; };
@ -56,30 +44,42 @@ export function ImportStep3Screen() {
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-xl font-semibold"> <h1 className="text-xl font-semibold">
{loading ? 'Creating...' : 'Continue with'} {loading ? 'Prefetching data...' : 'Continue with'}
</h1> </h1>
</div> </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' ? ( {status === 'loading' ? (
<div className="w-full"> <div className="w-full">
<div className="flex items-center gap-2"> <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>
<div className="mb-1 h-4 w-16 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-zinc-800" /> <div className="h-3 w-36 animate-pulse rounded bg-white/10" />
</div> </div>
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<User pubkey={account.pubkey} /> <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 ? ( {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>
)} )}
</div> </div>

View File

@ -92,7 +92,7 @@ export function MigrateScreen() {
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <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 Upgrade security for your account
</h1> </h1>
</div> </div>
@ -100,15 +100,15 @@ export function MigrateScreen() {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div> <div>
<div className="mt-1"> <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 You&apos;re using old Lume version which store your private key as
plaintext in database, this is huge security risk. plaintext in database, this is huge security risk.
</p> </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 To secure your private key, please set a password and Lume will put your
private key in secure storage. private key in secure storage.
</p> </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 It is not possible to start the app without applying this step, it is
easy and fast! easy and fast!
</p> </p>
@ -124,7 +124,7 @@ export function MigrateScreen() {
{...register('password', { required: true })} {...register('password', { required: true })}
type={passwordInput} type={passwordInput}
placeholder="min. 4 characters" 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 <button
type="button" type="button"
@ -135,13 +135,13 @@ export function MigrateScreen() {
<EyeOffIcon <EyeOffIcon
width={20} width={20}
height={20} height={20}
className="text-zinc-500 group-hover:text-zinc-100" className="text-white/50 group-hover:text-white"
/> />
) : ( ) : (
<EyeOnIcon <EyeOnIcon
width={20} width={20}
height={20} height={20}
className="text-zinc-500 group-hover:text-zinc-100" className="text-white/50 group-hover:text-white"
/> />
)} )}
</button> </button>
@ -154,10 +154,10 @@ export function MigrateScreen() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} 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 ? ( {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 →' 'Continue →'
)} )}

View File

@ -38,7 +38,7 @@ export function OnboardingScreen() {
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-4 text-center"> <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 👋 Hello, welcome you to Lume
</h1> </h1>
<p className="text-sm text-zinc-300"> <p className="text-sm text-zinc-300">
@ -54,7 +54,7 @@ export function OnboardingScreen() {
{status === 'success' && ( {status === 'success' && (
<User pubkey={account.pubkey} time={Math.floor(Date.now() / 1000)} /> <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> <p>Running Lume, join with me #nostr #lume</p>
<a <a
href="https://lume.nu" href="https://lume.nu"
@ -71,12 +71,12 @@ export function OnboardingScreen() {
<button <button
type="button" type="button"
onClick={() => submit()} 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 ? ( {loading ? (
<> <>
<span className="w-5" /> <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" /> <span className="w-5" />
</> </>
) : ( ) : (

View File

@ -102,13 +102,12 @@ export function ResetScreen() {
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <div className="mb-6 text-center">
<h1 className="text-xl font-semibold text-zinc-100">Reset unlock password</h1> <h1 className="text-2xl font-semibold text-white">Reset unlock password</h1>
</div> </div>
<div className="flex flex-col gap-4">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3"> <form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label htmlFor="privkey" className="font-medium text-zinc-200"> <label htmlFor="privkey" className="font-medium text-white/50">
Private key Private key
</label> </label>
<div className="relative"> <div className="relative">
@ -116,12 +115,12 @@ export function ResetScreen() {
{...register('privkey', { required: true })} {...register('privkey', { required: true })}
type="text" type="text"
placeholder="nsec..." 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" 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>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label htmlFor="password" className="font-medium text-zinc-200"> <label htmlFor="password" className="font-medium text-white/50">
Set a new password to protect your key Set a new password to protect your key
</label> </label>
<div className="relative"> <div className="relative">
@ -129,25 +128,17 @@ export function ResetScreen() {
{...register('password', { required: true })} {...register('password', { required: true })}
type={passwordInput} type={passwordInput}
placeholder="min. 4 characters" 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 h-12 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none placeholder:text-white/10"
/> />
<button <button
type="button" type="button"
onClick={() => showPassword()} 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' ? ( {passwordInput === 'password' ? (
<EyeOffIcon <EyeOffIcon className="h-5 w-5 text-white/50 group-hover:text-white" />
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
) : ( ) : (
<EyeOnIcon <EyeOnIcon className="h-5 w-5 text-white/50 group-hover:text-white" />
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
)} )}
</button> </button>
</div> </div>
@ -159,10 +150,10 @@ export function ResetScreen() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} 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="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 ? ( {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 →' 'Continue →'
)} )}
@ -171,6 +162,5 @@ export function ResetScreen() {
</form> </form>
</div> </div>
</div> </div>
</div>
); );
} }

View File

@ -81,37 +81,26 @@ export function UnlockScreen() {
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<div className="mx-auto w-full max-w-md"> <div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center"> <div className="mb-6 text-center">
<h1 className="text-xl font-semibold text-zinc-100"> <h1 className="text-2xl font-semibold text-white">Enter password to unlock</h1>
Enter password to unlock
</h1>
</div> </div>
<div className="flex flex-col gap-4"> <form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="relative"> <div className="relative">
<input <input
{...register('password', { required: true })} {...register('password', { required: true })}
type={passwordInput} type={passwordInput}
className="relative w-full rounded-lg bg-zinc-800 py-3 text-center text-zinc-100 !outline-none placeholder:text-zinc-400" className="relative h-12 w-full rounded-lg bg-white/10 py-1 text-center text-white !outline-none placeholder:text-white/10"
/> />
<button <button
type="button" type="button"
onClick={() => showPassword()} 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' ? ( {passwordInput === 'password' ? (
<EyeOffIcon <EyeOffIcon className="h-5 w-5 text-white/50 group-hover:text-white" />
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
) : ( ) : (
<EyeOnIcon <EyeOnIcon className="h-5 w-5 text-white/50 group-hover:text-white" />
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
)} )}
</button> </button>
</div> </div>
@ -123,17 +112,17 @@ export function UnlockScreen() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} 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-12 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600"
> >
{loading ? ( {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 →' 'Continue →'
)} )}
</button> </button>
<Link <Link
to="/auth/reset" to="/auth/reset"
className="inline-flex h-12 items-center justify-center text-center text-sm text-zinc-400" className="inline-flex h-14 items-center justify-center text-center text-white/50"
> >
Reset password Reset password
</Link> </Link>
@ -141,6 +130,5 @@ export function UnlockScreen() {
</form> </form>
</div> </div>
</div> </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 { Link } from 'react-router-dom';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle'; import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
export function WelcomeScreen() { 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 ( return (
<div className="grid h-full w-full grid-cols-12 gap-4 px-4 py-4"> <div className="flex h-screen w-full flex-col justify-between bg-white/10">
<div className="col-span-5 flex flex-col rounded-xl border-t border-zinc-800/50 bg-zinc-900"> <div className="flex flex-col gap-10 pt-16">
<div className="flex h-full w-full flex-col justify-center gap-2 px-4 py-4"> <div className="flex flex-col gap-2 text-center">
<h1 className="text-4xl font-bold leading-none text-transparent text-zinc-700"> <h1 className="text-3xl font-medium text-white">Welcome to Lume</h1>
Preserve your <span className="text-fuchsia-300">freedom</span> <h3 className="mx-auto w-2/3 text-white/50">
</h1> Let&apos;s get you up and connecting with all peoples around the world on
<h2 className="text-4xl font-bold leading-none text-transparent text-zinc-700"> Nostr
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>
</h3> </h3>
</div> </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 <Link
to="/auth/import" 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 className="w-5" />
<span>Login with private key</span> <span>Login with private key</span>
@ -31,24 +41,15 @@ export function WelcomeScreen() {
</Link> </Link>
<Link <Link
to="/auth/create" 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 Create new key
</Link> </Link>
</div> </div>
</div> </div>
<div <div className="flex flex-1 items-end justify-center pb-10">
className="col-span-5 rounded-xl bg-zinc-900 bg-cover bg-center" <img src="/lume.png" alt="lume" className="h-auto w-1/3" />
style={{ </div>
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> </div>
); );
} }

View File

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

View File

@ -93,7 +93,7 @@ export function ChannelCreateModal() {
// close modal // close modal
setIsOpen(false); setIsOpen(false);
// redirect to channel page // redirect to channel page
navigate(`/app/channel/${event.id}`); navigate(`/channel/${event.id}`);
}, 1000); }, 1000);
} catch (e) { } catch (e) {
console.log('error: ', 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" 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"> <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>
<div> <div>
<h5 className="font-medium text-zinc-400">Create channel</h5> <h5 className="font-medium text-white/50">Create channel</h5>
</div> </div>
</button> </button>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
@ -147,7 +147,7 @@ export function ChannelCreateModal() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-lg font-semibold leading-none text-zinc-100" className="text-lg font-semibold leading-none text-white"
> >
Create channel Create channel
</Dialog.Title> </Dialog.Title>
@ -159,7 +159,7 @@ export function ChannelCreateModal() {
<CancelIcon width={20} height={20} className="text-zinc-300" /> <CancelIcon width={20} height={20} className="text-zinc-300" />
</button> </button>
</div> </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 Channels are freedom square, everyone can speech freely, no one can
stop you or deceive what to speech stop you or deceive what to speech
</Dialog.Description> </Dialog.Description>
@ -174,10 +174,10 @@ export function ChannelCreateModal() {
type={'hidden'} type={'hidden'}
{...register('picture')} {...register('picture')}
value={image} 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"> <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 Picture
</span> </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"> <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"> <div className="flex flex-col gap-1">
<label <label
htmlFor="name" 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 * Channel name *
</label> </label>
@ -206,28 +206,28 @@ export function ChannelCreateModal() {
minLength: 4, minLength: 4,
})} })}
spellCheck={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-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label <label
htmlFor="about" 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 Description
</label> </label>
<textarea <textarea
{...register('about')} {...register('about')}
spellCheck={false} 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>
<div className="flex h-20 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2"> <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"> <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 Encrypted
</span> </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 All messages are encrypted and only invited members can view and
send message send message
</p> </p>
@ -248,10 +248,10 @@ export function ChannelCreateModal() {
<button <button
type="submit" type="submit"
disabled={!isDirty || !isValid} 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 ? ( {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 →' 'Create channel →'
)} )}

View File

@ -7,17 +7,17 @@ export function ChannelsListItem({ data }: { data: any }) {
const channel = useChannelProfile(data.event_id); const channel = useChannelProfile(data.event_id);
return ( return (
<NavLink <NavLink
to={`/app/channel/${data.event_id}`} to={`/channel/${data.event_id}`}
preventScrollReset={true} preventScrollReset={true}
className={({ isActive }) => className={({ isActive }) =>
twMerge( twMerge(
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5', '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"> <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>
<div className="inline-flex w-full items-center justify-between"> <div className="inline-flex w-full items-center justify-between">
<h5 className="truncate font-medium text-zinc-200">{channel?.name}</h5> <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"> <div className="flex w-full flex-col">
<UserReply pubkey={replyTo.pubkey} /> <UserReply pubkey={replyTo.pubkey} />
<div className="-mt-5 pl-[38px]"> <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>
</div> </div>
<button <button
@ -82,7 +82,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
onClick={() => stopReply()} onClick={() => stopReply()}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800" 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> </button>
</div> </div>
</div> </div>
@ -95,10 +95,10 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
placeholder="Message" placeholder="Message"
className={`relative ${ className={`relative ${
replyTo.id ? 'h-36 pt-16' : 'h-24 pt-3' 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="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} /> <MediaUploader setState={setValue} />
<button <button
type="button" type="button"

View File

@ -99,7 +99,7 @@ export function MessageHideButton({ id }: { id: string }) {
<CancelIcon width={20} height={20} className="text-zinc-300" /> <CancelIcon width={20} height={20} className="text-zinc-300" />
</button> </button>
</div> </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. This message will be hidden from your feed.
</Dialog.Description> </Dialog.Description>
</div> </div>
@ -109,14 +109,14 @@ export function MessageHideButton({ id }: { id: string }) {
<button <button
type="button" type="button"
onClick={closeModal} 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 Cancel
</button> </button>
<button <button
type="button" type="button"
onClick={() => hideMessage()} 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 Confirm
</button> </button>

View File

@ -19,7 +19,7 @@ export function ChannelMessageItem({ data }: { data: LumeEvent }) {
<div className="flex flex-col"> <div className="flex flex-col">
<User pubkey={data.pubkey} time={data.created_at} isChat={true} /> <User pubkey={data.pubkey} time={data.created_at} isChat={true} />
<div className="-mt-[20px] pl-[49px]"> <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} {content.parsed}
</p> </p>
{Array.isArray(content.images) && content.images.length ? ( {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" /> <CancelIcon width={20} height={20} className="text-zinc-300" />
</button> </button>
</div> </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. You will no longer see messages from this user.
</Dialog.Description> </Dialog.Description>
</div> </div>
@ -109,14 +109,14 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
<button <button
type="button" type="button"
onClick={closeModal} 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 Cancel
</button> </button>
<button <button
type="button" type="button"
onClick={() => muteUser()} 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 Confirm
</button> </button>

View File

@ -13,7 +13,7 @@ export function UserReply({ pubkey }: { pubkey: string }) {
{isError || isLoading ? ( {isError || isLoading ? (
<> <>
<div className="relative h-9 w-9 shrink animate-pulse overflow-hidden rounded bg-zinc-800" /> <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" className="h-9 w-9 rounded object-cover"
/> />
</div> </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)} Replying to {user?.name || shortenKey(pubkey)}
</span> </span>
</> </>

View File

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

View File

@ -51,10 +51,10 @@ export function MutedItem({ data }: { data: any }) {
/> />
</div> </div>
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start"> <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'} {user?.displayName || user?.name || 'Pleb'}
</span> </span>
<span className="text-base leading-none text-zinc-400"> <span className="text-base leading-none text-white/50">
{shortenKey(data.content)} {shortenKey(data.content)}
</span> </span>
</div> </div>
@ -64,7 +64,7 @@ export function MutedItem({ data }: { data: any }) {
<button <button
type="button" type="button"
onClick={() => unmute()} 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 Unmute
</button> </button>
@ -72,7 +72,7 @@ export function MutedItem({ data }: { data: any }) {
<button <button
type="button" type="button"
onClick={() => mute()} 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 Mute
</button> </button>

View File

@ -23,7 +23,7 @@ const Header = (
<div className="w-full border-t border-zinc-800" /> <div className="w-full border-t border-zinc-800" />
</div> </div>
<div className="relative flex justify-center"> <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', { {getHourAgo(24, now).toLocaleDateString('en-US', {
weekday: 'long', weekday: 'long',
year: 'numeric', year: 'numeric',
@ -40,7 +40,7 @@ const Empty = (
<h3 className="text-base font-semibold leading-none text-white"> <h3 className="text-base font-semibold leading-none text-white">
Nothing to see here yet Nothing to see here yet
</h3> </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. Be the first to share a message in this channel.
</p> </p>
</div> </div>
@ -102,7 +102,7 @@ export function ChannelScreen() {
data-tauri-drag-region data-tauri-drag-region
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900" 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>
<div className="h-full w-full flex-1 p-3"> <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 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 { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile'; 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); const { status, user } = useProfile(data.sender_pubkey);
if (status === 'loading') { if (status === 'loading') {
return ( return (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <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-zinc-800" /> <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-zinc-800" /> <div className="h-2.5 w-2/3 animate-pulse rounded bg-white/10" />
</div> </div>
); );
} }
return ( return (
<NavLink <NavLink
to={`/app/chats/${data.sender_pubkey}`} to={`/chats/${data.sender_pubkey}`}
preventScrollReset={true} preventScrollReset={true}
className={({ isActive }) => className={({ isActive }) =>
twMerge( twMerge(
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5', 'inline-flex h-9 items-center gap-2.5 rounded-md px-2',
isActive ? 'bg-zinc-900/50 text-zinc-100' : '' 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 <Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
alt={data.sender_pubkey} alt={data.sender_pubkey}
className="h-6 w-6 rounded object-cover" className="h-6 w-6 shrink-0 rounded object-cover"
/> />
</div> <div className="inline-flex w-full flex-1 items-center justify-between">
<div className="inline-flex w-full items-center justify-between"> <h5 className="max-w-[10rem] truncate">
<div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[10rem] truncate font-medium text-zinc-200">
{user?.nip05 || {user?.nip05 ||
user?.name || user?.name ||
user?.displayName || user?.display_name ||
shortenKey(data.sender_pubkey)} displayNpub(data.sender_pubkey, 16)}
</h5> </h5>
</div>
<div className="flex items-center"> <div className="flex items-center">
{data.new_messages > 0 && ( {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"> <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( const renderItem = useCallback(
(item: Chats) => { (item: Chats) => {
if (account.pubkey !== item.sender_pubkey) { if (account?.pubkey !== item.sender_pubkey) {
return <ChatsListItem key={item.sender_pubkey} data={item} />; return <ChatsListItem key={item.sender_pubkey} data={item} />;
} }
}, },
@ -34,12 +34,12 @@ export function ChatsList() {
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <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="relative h-6 w-6 shrink-0 animate-pulse rounded bg-white/10" />
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" /> <div className="h-3 w-full animate-pulse rounded-sm bg-white/10" />
</div> </div>
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <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="relative h-6 w-6 shrink-0 animate-pulse rounded bg-white/10" />
<div className="h-3 w-full animate-pulse rounded-sm bg-zinc-800" /> <div className="h-3 w-full animate-pulse rounded-sm bg-white/10" />
</div> </div>
</div> </div>
); );
@ -50,20 +50,20 @@ export function ChatsList() {
{account ? ( {account ? (
<ChatsListSelfItem data={account} /> <ChatsListSelfItem data={account} />
) : ( ) : (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <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-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-zinc-800" /> <div className="h-3 w-full animate-pulse rounded-sm bg-white/10" />
</div> </div>
)} )}
{chats.follows.map((item) => renderItem(item))} {chats.follows.map((item) => renderItem(item))}
{chats.unknowns.length > 0 && <UnknownsModal data={chats.unknowns} />}
<NewMessageModal />
{isFetching && ( {isFetching && (
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"> <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-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-zinc-800" /> <div className="h-3 w-full animate-pulse rounded-sm bg-white/10" />
</div> </div>
)} )}
{chats.unknowns.length > 0 && <UnknownsModal data={chats.unknowns} />}
<NewMessageModal />
</div> </div>
); );
} }

View File

@ -51,10 +51,10 @@ export function ChatMessageForm({
onKeyDown={handleEnterPress} onKeyDown={handleEnterPress}
spellCheck={false} spellCheck={false}
placeholder="Message" 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="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} /> <MediaUploader setState={setValue} />
<button <button
type="button" type="button"

View File

@ -26,11 +26,11 @@ export function ChatMessageItem({
const content = parser(data); const content = parser(data);
return ( 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"> <div className="flex flex-col">
<User pubkey={data.sender_pubkey} time={data.created_at} isChat={true} /> <User pubkey={data.sender_pubkey} time={data.created_at} isChat={true} />
<div className="-mt-[20px] pl-[49px]"> <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} {content.parsed}
</p> </p>
{content.images.length > 0 && <ImagePreview urls={content.images} />} {content.images.length > 0 && <ImagePreview urls={content.images} />}

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants'; import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey'; import { displayNpub } from '@utils/shortenKey';
export function ChatSidebar({ pubkey }: { pubkey: string }) { export function ChatSidebar({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey); 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-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h3 className="text-lg font-semibold leading-none"> <h3 className="text-lg font-semibold leading-none">
{user?.displayName || user?.name} {user?.display_name || user?.name}
</h3> </h3>
<h5 className="leading-none text-zinc-400"> <h5 className="leading-none text-white/50">
{user?.nip05 || shortenKey(pubkey)} {user?.nip05 || displayNpub(pubkey, 16)}
</h5> </h5>
</div> </div>
<div> <div>
<p className="leading-tight">{user?.bio || user?.about}</p> <p className="leading-tight">{user?.bio || user?.about}</p>
<Link <Link
to={`/app/users/${pubkey}`} to={`/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" 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 View full profile
</Link> </Link>

View File

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

View File

@ -1,7 +1,9 @@
import { nip04 } from 'nostr-tools'; import { nip04 } from 'nostr-tools';
import { useEffect, useState } from 'react'; 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); const [content, setContent] = useState(data.content);
useEffect(() => { useEffect(() => {

View File

@ -14,6 +14,7 @@ import { createChat, getChatMessages } from '@libs/storage';
import { useStronghold } from '@stores/stronghold'; import { useStronghold } from '@stores/stronghold';
import { useAccount } from '@utils/hooks/useAccount'; import { useAccount } from '@utils/hooks/useAccount';
import { Chats } from '@utils/types';
export function ChatScreen() { export function ChatScreen() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -34,7 +35,7 @@ export function ChatScreen() {
const userPrivkey = useStronghold((state) => state.privkey); const userPrivkey = useStronghold((state) => state.privkey);
const itemContent: any = useCallback( const itemContent = useCallback(
(index: string | number) => { (index: string | number) => {
return ( return (
<ChatMessageItem <ChatMessageItem
@ -55,7 +56,7 @@ export function ChatScreen() {
); );
const chat = useMutation({ const chat = useMutation({
mutationFn: (data: any) => { mutationFn: (data: Chats) => {
return createChat( return createChat(
data.id, data.id,
data.receiver_pubkey, data.receiver_pubkey,
@ -100,16 +101,10 @@ export function ChatScreen() {
}, [pubkey]); }, [pubkey]);
return ( return (
<div className="grid h-full w-full grid-cols-3"> <div className="grid h-full w-full grid-cols-3 bg-white/10">
<div className="col-span-2 flex flex-col justify-between border-r border-zinc-900"> <div className="col-span-2 border-r border-white/5">
<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="h-full w-full flex-1 p-3"> <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"> <div className="h-full w-full flex-1">
{status === 'loading' ? ( {status === 'loading' ? (
<p>Loading...</p> <p>Loading...</p>
@ -131,7 +126,7 @@ export function ChatScreen() {
/> />
)} )}
</div> </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 <ChatMessageForm
receiverPubkey={pubkey} receiverPubkey={pubkey}
userPubkey={account.pubkey} userPubkey={account.pubkey}
@ -141,11 +136,7 @@ export function ChatScreen() {
</div> </div>
</div> </div>
</div> </div>
<div className="col-span-1"> <div className="col-span-1 pt-3">
<div
data-tauri-drag-region
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
/>
<ChatSidebar pubkey={pubkey} /> <ChatSidebar pubkey={pubkey} />
</div> </div>
</div> </div>
@ -155,7 +146,7 @@ export function ChatScreen() {
const Empty = ( 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"> <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> <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 You two didn&apos;t talk yet, let&apos;s send first message
</p> </p>
</div> </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 ( return (
<div className="h-full w-full px-3 pt-12"> <div className="h-full w-full px-3 pt-12">
<div className="flex flex-col gap-2"> <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=""> <div className="">
{status === 'loading' ? ( {status === 'loading' ? (
<p>Loading...</p> <p>Loading...</p>
) : ( ) : (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-1"> <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 Public Key
</label> </label>
<input <input
readOnly readOnly
value={account.pubkey} 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>
<div className="flex flex-col gap-1"> <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 Npub
</label> </label>
<input <input
readOnly readOnly
value={account.npub} 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>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label <label
htmlFor="privkey" htmlFor="privkey"
className="text-base font-semibold text-zinc-400" className="text-base font-semibold text-white/50"
> >
Private Key Private Key
</label> </label>
@ -61,7 +61,7 @@ export function AccountSettingsScreen() {
readOnly readOnly
type={type} type={type}
value={privkey} 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 <button
type="button" type="button"
@ -72,13 +72,13 @@ export function AccountSettingsScreen() {
<EyeOffIcon <EyeOffIcon
width={20} width={20}
height={20} height={20}
className="text-zinc-500 group-hover:text-zinc-100" className="text-white/50 group-hover:text-white"
/> />
) : ( ) : (
<EyeOnIcon <EyeOnIcon
width={20} width={20}
height={20} height={20}
className="text-zinc-500 group-hover:text-zinc-100" className="text-white/50 group-hover:text-white"
/> />
)} )}
</button> </button>

View File

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

View File

@ -20,7 +20,7 @@ export function CacheTimeSetting() {
<span className="font-medium leading-none text-zinc-200"> <span className="font-medium leading-none text-zinc-200">
Cache time (milliseconds) Cache time (milliseconds)
</span> </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 The length of time before inactive data gets removed from the cache
</span> </span>
</div> </div>
@ -37,7 +37,7 @@ export function CacheTimeSetting() {
onClick={() => update()} onClick={() => update()}
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 font-medium hover:bg-fuchsia-500" 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> </button>
</div> </div>
</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="inline-flex items-center justify-between px-5 py-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="font-medium leading-none text-zinc-200">Version</span> <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 You&apos;re using latest version
</span> </span>
</div> </div>
@ -19,7 +19,7 @@ export function VersionSetting() {
type="button" type="button"
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 font-medium hover:bg-fuchsia-500" 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> </button>
</div> </div>
</div> </div>

View File

@ -6,7 +6,7 @@ export function GeneralSettingsScreen() {
return ( return (
<div className="h-full w-full px-3 pt-12"> <div className="h-full w-full px-3 pt-12">
<div className="flex flex-col gap-2"> <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="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="flex h-full w-full flex-col divide-y divide-zinc-800">
<AutoStartSetting /> <AutoStartSetting />

View File

@ -4,7 +4,7 @@ export function ShortcutsSettingsScreen() {
return ( return (
<div className="h-full w-full px-3 pt-12"> <div className="h-full w-full px-3 pt-12">
<div className="flex flex-col gap-2"> <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="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="flex h-full w-full flex-col divide-y divide-zinc-800">
<div className="inline-flex items-center justify-between px-5 py-4"> <div className="inline-flex items-center justify-between px-5 py-4">
@ -15,10 +15,10 @@ export function ShortcutsSettingsScreen() {
</div> </div>
<div className="flex items-center gap-2"> <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"> <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>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <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> </div>
</div> </div>
@ -30,10 +30,10 @@ export function ShortcutsSettingsScreen() {
</div> </div>
<div className="flex items-center gap-2"> <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"> <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>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <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> </div>
</div> </div>
@ -45,10 +45,10 @@ export function ShortcutsSettingsScreen() {
</div> </div>
<div className="flex items-center gap-2"> <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"> <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>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <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> </div>
</div> </div>
@ -60,10 +60,10 @@ export function ShortcutsSettingsScreen() {
</div> </div>
<div className="flex items-center gap-2"> <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"> <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>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <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> </div>
</div> </div>
@ -75,10 +75,10 @@ export function ShortcutsSettingsScreen() {
</div> </div>
<div className="flex items-center gap-2"> <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"> <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>
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800"> <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> </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 itemsVirtualizer = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();
useEffect(() => { useEffect(() => {
const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse(); const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse();
@ -113,24 +114,20 @@ export function FeedBlock({ params }: { params: Block }) {
); );
return ( 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} /> <TitleBar id={params.id} title={params.title} />
<div <div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
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' }}
>
{status === 'loading' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <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 /> <NoteSkeleton />
</div> </div>
</div> </div>
) : itemsVirtualizer.length === 0 ? ( ) : itemsVirtualizer.length === 0 ? (
<div className="px-3 py-1.5"> <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"> <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 Not found any posts from last 48 hours
</p> </p>
</div> </div>
@ -140,7 +137,7 @@ export function FeedBlock({ params }: { params: Block }) {
<div <div
className="relative w-full" className="relative w-full"
style={{ style={{
height: `${rowVirtualizer.getTotalSize()}px`, height: `${totalSize}px`,
}} }}
> >
<div <div
@ -157,7 +154,7 @@ export function FeedBlock({ params }: { params: Block }) {
)} )}
{isFetchingNextPage && ( {isFetchingNextPage && (
<div className="px-3 py-1.5"> <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 /> <NoteSkeleton />
</div> </div>
</div> </div>

View File

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

View File

@ -1,23 +1,23 @@
import { CancelIcon } from '@shared/icons'; import { CancelIcon } from '@shared/icons';
import { Image } from '@shared/image'; import { Image } from '@shared/image';
import { useBlocks } from '@stores/blocks';
import { DEFAULT_AVATAR } from '@stores/constants'; import { DEFAULT_AVATAR } from '@stores/constants';
import { useBlock } from '@utils/hooks/useBlock';
import { Block } from '@utils/types'; import { Block } from '@utils/types';
export function ImageBlock({ params }: { params: Block }) { export function ImageBlock({ params }: { params: Block }) {
const { remove } = useBlock(); const removeBlock = useBlocks((state) => state.removeBlock);
return ( 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="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="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"> <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> <h3 className="font-medium text-white drop-shadow-lg">{params.title}</h3>
<button <button
type="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" 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" /> <CancelIcon width={16} height={16} className="text-white" />
@ -28,7 +28,7 @@ export function ImageBlock({ params }: { params: Block }) {
src={params.content} src={params.content}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
alt={params.title} 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>
</div> </div>

View File

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

View File

@ -22,31 +22,35 @@ export function ThreadBlock({ params }: { params: Block }) {
// useLiveThread(params.content); // useLiveThread(params.content);
return ( 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} /> <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' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <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 /> <NoteSkeleton />
</div> </div>
</div> </div>
) : ( ) : (
<div className="h-min w-full px-3 pt-1.5"> <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} /> <ThreadUser pubkey={data.pubkey} time={data.created_at} />
<div className="mt-2"> <div className="mt-2">
<NoteContent content={data.content} /> <NoteContent content={data.content} />
</div> </div>
<div> <div>
<NoteActions id={data.id} pubkey={data.pubkey} noOpenThread={true} /> <NoteActions
<NoteStats id={data.id} /> id={params.content}
pubkey={data.pubkey}
noOpenThread={true}
/>
<NoteStats id={params.content} />
</div> </div>
</div> </div>
</div> </div>
)} )}
<div className="px-3"> <div className="px-3">
<NoteReplyForm id={params.content} pubkey={account.pubkey} /> {account && <NoteReplyForm id={params.content} pubkey={account.pubkey} />}
<RepliesList id={params.content} /> <RepliesList id={params.content} />
</div> </div>
</div> </div>

View File

@ -35,31 +35,28 @@ export function UserBlock({ params }: { params: Block }) {
const itemsVirtualizer = rowVirtualizer.getVirtualItems(); const itemsVirtualizer = rowVirtualizer.getVirtualItems();
return ( 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} /> <TitleBar id={params.id} title={params.title} />
<div <div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
ref={parentRef}
className="scrollbar-hide flex h-full flex-1 flex-col gap-1.5 overflow-y-auto pt-1.5"
>
<div className="px-3 pt-1.5"> <div className="px-3 pt-1.5">
<UserProfile pubkey={params.content} /> <UserProfile pubkey={params.content} />
</div> </div>
<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 Latest activities
</h3> </h3>
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10"> <div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
{status === 'loading' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <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 /> <NoteSkeleton />
</div> </div>
</div> </div>
) : itemsVirtualizer.length === 0 ? ( ) : itemsVirtualizer.length === 0 ? (
<div className="px-3 py-1.5"> <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"> <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 No new posts about this hashtag in 48 hours ago
</p> </p>
</div> </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(() => { useEffect(() => {
if (status === 'success' && account) { if (status === 'success' && account) {
const follows = account ? JSON.parse(account.follows as string) : [];
const filter: NDKFilter = { const filter: NDKFilter = {
kinds: [1, 6], kinds: [1, 6],
authors: follows, authors: account.follows,
since: now.current, since: now.current,
}; };

View File

@ -1,37 +1,23 @@
import { useQuery } from '@tanstack/react-query'; import { useCallback, useEffect } from 'react';
import { useCallback } from 'react';
import { AddBlock } from '@app/space/components/add';
import { FeedBlock } from '@app/space/components/blocks/feed'; 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 { HashtagBlock } from '@app/space/components/blocks/hashtag';
import { ImageBlock } from '@app/space/components/blocks/image'; 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 { ThreadBlock } from '@app/space/components/blocks/thread';
import { UserBlock } from '@app/space/components/blocks/user'; import { UserBlock } from '@app/space/components/blocks/user';
import { FeedModal } from '@app/space/components/modals/feed';
import { getBlocks } from '@libs/storage'; import { HashtagModal } from '@app/space/components/modals/hashtag';
import { ImageModal } from '@app/space/components/modals/image';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { useBlocks } from '@stores/blocks';
import { Block } from '@utils/types'; import { Block } from '@utils/types';
export function SpaceScreen() { export function SpaceScreen() {
const { const [blocks, fetchBlocks] = useBlocks((state) => [state.blocks, state.fetchBlocks]);
status,
data: blocks,
isFetching,
} = useQuery(
['blocks'],
async () => {
return await getBlocks();
},
{
staleTime: Infinity,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
}
);
const renderBlock = useCallback( const renderBlock = useCallback(
(block: Block) => { (block: Block) => {
@ -53,41 +39,30 @@ export function SpaceScreen() {
[blocks] [blocks]
); );
return ( useEffect(() => {
<div className="scrollbar-hide flex h-full w-full flex-nowrap overflow-x-auto overflow-y-hidden"> fetchBlocks();
<FollowingBlock /> }, [fetchBlocks]);
{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"
/>
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"> <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>
</div> </div>
) : ( ) : (
blocks.map((block: Block) => renderBlock(block)) blocks.map((block) => renderBlock(block))
)} )}
{isFetching && ( <div className="flex w-[350px] shrink-0 flex-col">
<div className="flex w-[350px] shrink-0 flex-col border-r border-zinc-900"> <div className="inline-flex h-full w-full flex-col items-center justify-center gap-1">
<div <FeedModal />
data-tauri-drag-region <ImageModal />
className="group flex h-11 w-full items-center justify-between overflow-hidden border-b border-zinc-900 px-3" <HashtagModal />
/>
<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> </div>
)} <div className="w-[250px] shrink-0" />
<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>
</div>
<div className="w-[350px] shrink-0" />
</div> </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'; import { shortenKey } from '@utils/shortenKey';
export function Profile({ data }: { data: any }) { export function Profile({ data }: { data: any }) {
const { status, data: userStats } = useQuery(['user-stats', data.pubkey], async () => { 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}`); const res = await fetch(`https://api.nostr.band/v0/stats/profile/${data.pubkey}`);
return res.json(); return res.json();
}); },
{
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
}
);
const embedProfile = data.profile ? JSON.parse(data.profile.content) : null; const embedProfile = data.profile ? JSON.parse(data.profile.content) : null;
const profile = embedProfile; const profile = embedProfile;
const { status: socialStatus, userFollows, follow, unfollow } = useSocial();
const [followed, setFollowed] = useState(false); const [followed, setFollowed] = useState(false);
@ -50,29 +59,28 @@ export function Profile({ data }: { data: any }) {
} }
}, [status]); }, [status]);
if (!profile) if (!profile) {
return ( 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> <p>Can&apos;t fetch profile</p>
</div> </div>
); );
}
return ( 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="flex items-center justify-between">
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
<div className="h-11 w-11 shrink-0">
<Image <Image
src={profile.picture} src={profile.picture}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
className="h-11 w-11 rounded-lg object-cover" className="h-11 w-11 shrink-0 rounded-lg object-cover"
/> />
</div>
<div className="inline-flex flex-col gap-1"> <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} {profile.display_name || profile.name}
</h3> </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)} {profile.nip05 || shortenKey(data.pubkey)}
</p> </p>
</div> </div>
@ -81,15 +89,15 @@ export function Profile({ data }: { data: any }) {
{socialStatus === 'loading' ? ( {socialStatus === 'loading' ? (
<button <button
type="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> </button>
) : followed ? ( ) : followed ? (
<button <button
type="button" type="button"
onClick={() => unfollowUser(data.pubkey)} 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" /> <UnfollowIcon className="h-4 w-4" />
</button> </button>
@ -97,7 +105,7 @@ export function Profile({ data }: { data: any }) {
<button <button
type="button" type="button"
onClick={() => followUser(data.pubkey)} 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" /> <FollowIcon className="h-4 w-4" />
</button> </button>
@ -105,7 +113,7 @@ export function Profile({ data }: { data: any }) {
</div> </div>
</div> </div>
<div className="mt-2"> <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} {profile.about || profile.bio}
</p> </p>
</div> </div>
@ -115,26 +123,26 @@ export function Profile({ data }: { data: any }) {
) : ( ) : (
<div className="flex w-full items-center gap-8"> <div className="flex w-full items-center gap-8">
<div className="inline-flex flex-col gap-1"> <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} {userStats.stats[data.pubkey].followers_pubkey_count ?? 0}
</span> </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>
<div className="inline-flex flex-col gap-1"> <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} {userStats.stats[data.pubkey].pub_following_pubkey_count ?? 0}
</span> </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>
<div className="inline-flex flex-col gap-1"> <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 {userStats.stats[data.pubkey].zaps_received
? compactNumber.format( ? compactNumber.format(
userStats.stats[data.pubkey].zaps_received.msats / 1000 userStats.stats[data.pubkey].zaps_received.msats / 1000
) )
: 0} : 0}
</span> </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>
</div> </div>
)} )}

View File

@ -1,33 +1,52 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { fetch } from '@tauri-apps/plugin-http';
import { NoteKind_1 } from '@shared/notes'; import { NoteKind_1 } from '@shared/notes';
import { NoteSkeleton } from '@shared/notes/skeleton'; import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar'; import { TitleBar } from '@shared/titleBar';
import { LumeEvent } from '@utils/types';
interface Response {
ok: boolean;
data: {
notes: Array<{ event: LumeEvent }>;
};
}
export function TrendingNotes() { export function TrendingNotes() {
const { status, data, error } = useQuery(['trending-notes'], async () => { const { status, data, error } = useQuery(
const res = await fetch('https://api.nostr.band/v0/trending/notes'); ['trending-notes'],
async () => {
const res: Response = await fetch('https://api.nostr.band/v0/trending/notes');
if (!res.ok) { if (!res.ok) {
throw new Error('Error'); throw new Error('Error');
} }
return res.json(); return res.data?.notes;
}); },
{
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
}
);
return ( 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" /> <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>} {error && <p>Failed to fetch</p>}
{status === 'loading' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <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 /> <NoteSkeleton />
</div> </div>
</div> </div>
) : ( ) : (
<div className="relative flex w-full flex-col pt-1.5"> <div className="relative flex w-full flex-col">
{data.notes.map((item) => ( {data.map((item) => (
<NoteKind_1 key={item.id} event={item.event} skipMetadata={true} /> <NoteKind_1 key={item.event.id} event={item.event} skipMetadata={true} />
))} ))}
</div> </div>
)} )}

View File

@ -1,33 +1,50 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { fetch } from '@tauri-apps/plugin-http';
import { Profile } from '@app/trending/components/profile'; import { Profile } from '@app/trending/components/profile';
import { NoteSkeleton } from '@shared/notes/skeleton'; import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar'; import { TitleBar } from '@shared/titleBar';
interface Response {
ok: boolean;
data: {
profiles: Array<{ pubkey: string }>;
};
}
export function TrendingProfiles() { export function TrendingProfiles() {
const { status, data, error } = useQuery(['trending-profiles'], async () => { const { status, data, error } = useQuery(
const res = await fetch('https://api.nostr.band/v0/trending/profiles'); ['trending-profiles'],
async () => {
const res: Response = await fetch('https://api.nostr.band/v0/trending/profiles');
if (!res.ok) { if (!res.ok) {
throw new Error('Error'); throw new Error('Error');
} }
return res.json(); return res.data?.profiles;
}); },
{
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
}
);
return ( 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" /> <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>} {error && <p>Failed to fetch</p>}
{status === 'loading' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <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 /> <NoteSkeleton />
</div> </div>
</div> </div>
) : ( ) : (
<div className="relative flex w-full flex-col gap-3 px-3 pt-3"> <div className="relative flex w-full flex-col gap-3 px-3 pt-1.5">
{data.profiles.map((item) => ( {data.map((item) => (
<Profile key={item.pubkey} data={item} /> <Profile key={item.pubkey} data={item} />
))} ))}
</div> </div>

View File

@ -3,7 +3,7 @@ import { TrendingProfiles } from '@app/trending/components/trendingProfiles';
export function TrendingScreen() { export function TrendingScreen() {
return ( 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 /> <TrendingProfiles />
<TrendingNotes /> <TrendingNotes />
</div> </div>

View File

@ -18,32 +18,32 @@ export function UserMetadata({ pubkey }: { pubkey: string }) {
return ( return (
<div className="flex w-full items-center gap-10"> <div className="flex w-full items-center gap-10">
<div className="inline-flex flex-col gap-1"> <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} {compactNumber.format(data.stats[pubkey].followers_pubkey_count) ?? 0}
</span> </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>
<div className="inline-flex flex-col gap-1"> <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} {compactNumber.format(data.stats[pubkey].pub_following_pubkey_count) ?? 0}
</span> </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>
<div className="inline-flex flex-col gap-1"> <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 {data.stats[pubkey].zaps_received
? compactNumber.format(data.stats[pubkey].zaps_received.msats / 1000) ? compactNumber.format(data.stats[pubkey].zaps_received.msats / 1000)
: 0} : 0}
</span> </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>
<div className="inline-flex flex-col gap-1"> <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 {data.stats[pubkey].zaps_sent
? compactNumber.format(data.stats[pubkey].zaps_sent.msats / 1000) ? compactNumber.format(data.stats[pubkey].zaps_sent.msats / 1000)
: 0} : 0}
</span> </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>
</div> </div>
); );

View File

@ -52,7 +52,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
return ( return (
<> <>
<div className="h-56 w-full bg-zinc-100"> <div className="h-56 w-full bg-white">
<Image <Image
src={user?.banner} src={user?.banner}
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg" fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
@ -65,7 +65,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
src={user?.picture || user?.image} src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
alt={pubkey} 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="mt-2 flex flex-1 flex-col gap-4">
<div className="flex items-center gap-16"> <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"> <h5 className="text-lg font-semibold leading-none">
{user?.displayName || user?.name || 'No name'} {user?.displayName || user?.name || 'No name'}
</h5> </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)} {user?.nip05 || shortenKey(pubkey)}
</span> </span>
</div> </div>
@ -81,7 +81,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
{status === 'loading' ? ( {status === 'loading' ? (
<button <button
type="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... Loading...
</button> </button>
@ -89,7 +89,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
<button <button
type="button" type="button"
onClick={() => unfollowUser(pubkey)} 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 Unfollow
</button> </button>
@ -97,23 +97,23 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
<button <button
type="button" type="button"
onClick={() => followUser(pubkey)} 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 Follow
</button> </button>
)} )}
<Link <Link
to={`/app/chats/${pubkey}`} to={`/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" 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 Message
</Link> </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 />} {account && account.pubkey === pubkey && <EditProfileModal />}
</div> </div>
</div> </div>
<div className="flex flex-col gap-8"> <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} {user?.about || user?.bio}
</p> </p>
<UserMetadata pubkey={pubkey} /> <UserMetadata pubkey={pubkey} />

View File

@ -36,30 +36,30 @@ export function UserScreen() {
const itemsVirtualizer = rowVirtualizer.getVirtualItems(); const itemsVirtualizer = rowVirtualizer.getVirtualItems();
return ( return (
<div ref={parentRef} className="scrollbar-hide h-full w-full overflow-y-auto">
<div <div
data-tauri-drag-region ref={parentRef}
className="flex h-11 w-full items-center border-b border-zinc-900 px-3" 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} /> <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"> <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> <p className="text-lg font-semibold leading-none text-white">Latest posts</p>
<span className="text-sm leading-none text-zinc-500">48 hours ago</span> <span className="text-sm leading-none text-white/50">48 hours ago</span>
</div> </div>
<div className="flex h-full max-w-[400px] flex-col justify-between gap-1.5 pb-4 pt-1.5"> <div className="flex h-full max-w-[400px] flex-col justify-between gap-1.5 pb-4 pt-1.5">
{status === 'loading' ? ( {status === 'loading' ? (
<div className="px-3 py-1.5"> <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 /> <NoteSkeleton />
</div> </div>
</div> </div>
) : itemsVirtualizer.length === 0 ? ( ) : itemsVirtualizer.length === 0 ? (
<div className="px-3 py-1.5"> <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"> <div className="flex flex-col items-center gap-4">
<p className="text-center text-sm text-zinc-300"> <p className="text-center text-sm font-medium text-zinc-300">
No new posts about this hashtag in 48 hours ago No new posts in 48 hours ago
</p> </p>
</div> </div>
</div> </div>

View File

@ -15,11 +15,11 @@ button {
} }
.markdown { .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 { .ProseMirror p.is-empty::before {
@apply text-zinc-500; @apply text-white/50;
content: attr(data-placeholder); content: attr(data-placeholder);
float: left; float: left;
height: 0; 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/ // 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 { ndkAdapter } from '@nostr-fetch/adapter-ndk';
import { NostrFetcher, normalizeRelayUrlSet } from 'nostr-fetch'; import { NostrFetcher, normalizeRelayUrlSet } from 'nostr-fetch';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import TauriAdapter from '@libs/ndk/cache';
import { getSetting } from '@libs/storage'; import { getSetting } from '@libs/storage';
const setting = await getSetting('relays'); const setting = await getSetting('relays');
@ -15,11 +16,16 @@ export const NDKInstance = () => {
const [fetcher, setFetcher] = useState<NostrFetcher>(undefined); const [fetcher, setFetcher] = useState<NostrFetcher>(undefined);
useEffect(() => { useEffect(() => {
loadNdk(relays); const cacheAdapter = new TauriAdapter();
}, []); loadNdk(relays, cacheAdapter);
async function loadNdk(explicitRelayUrls: string[]) { return () => {
const ndkInstance = new NDK({ explicitRelayUrls }); cacheAdapter.save();
};
}, [relays]);
async function loadNdk(explicitRelayUrls: string[], cacheAdapter: NDKCacheAdapter) {
const ndkInstance = new NDK({ explicitRelayUrls, cacheAdapter });
try { try {
await ndkInstance.connect(); 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;' 'SELECT * FROM accounts WHERE is_active = 1;'
); );
if (result.length > 0) { 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]; return result[0];
} else { } else {
return null; return null;
@ -63,15 +69,12 @@ export async function createAccount(
} }
// update account // update account
export async function updateAccount( export async function updateAccount(column: string, value: string | string[]) {
column: string,
value: string | string[],
pubkey: string
) {
const db = await connect(); 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, value,
pubkey, account.id,
]); ]);
} }
@ -305,8 +308,6 @@ export async function getChannelUsers(channel_id: string) {
export async function getChats() { export async function getChats() {
const db = await connect(); const db = await connect();
const account = await getActiveAccount(); 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 } = { const chats: { follows: Array<Chats> | null; unknowns: Array<Chats> | null } = {
follows: [], follows: [],
@ -321,7 +322,7 @@ export async function getChats() {
result = result.sort((a, b) => a.new_messages - b.new_messages); result = result.sort((a, b) => a.new_messages - b.new_messages);
chats.follows = result.filter((el) => { chats.follows = result.filter((el) => {
return follows.some((i) => { return account.follows.some((i) => {
return i === el.sender_pubkey; return i === el.sender_pubkey;
}); });
}); });
@ -426,10 +427,19 @@ export async function createBlock(
) { ) {
const db = await connect(); const db = await connect();
const activeAccount = await getActiveAccount(); const activeAccount = await getActiveAccount();
return await db.execute( const insert = await db.execute(
'INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);', 'INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);',
[activeAccount.id, kind, title, content] [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 // remove block

View File

@ -16,7 +16,7 @@ import { sendNativeNotification } from '@utils/notification';
const lastLogin = await getLastLogin(); const lastLogin = await getLastLogin();
export function ActiveAccount({ data }: { data: any }) { export function ActiveAccount({ data }: { data: { pubkey: string; npub: string } }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { ndk } = useNDK(); const { ndk } = useNDK();
@ -88,11 +88,11 @@ export function ActiveAccount({ data }: { data: any }) {
}, []); }, []);
if (status === 'loading') { 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 ( 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 <Image
src={user?.picture || user?.image} src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR} 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() { export function AppLayout() {
return ( return (
<div className="flex h-screen w-screen"> <div className="flex h-screen w-screen">
<div className="relative flex shrink-0 flex-row"> <div className="shrink-0">
<Navigation /> <Navigation />
</div> </div>
<div className="h-full w-full"> <div className="h-full w-full flex-1 bg-black/90">
<Outlet /> <Outlet />
<ScrollRestoration /> <ScrollRestoration
getKey={(location) => {
return location.pathname;
}}
/>
</div> </div>
</div> </div>
); );

View File

@ -1,66 +1,10 @@
import { platform } from '@tauri-apps/plugin-os'; import { Outlet } from 'react-router-dom';
import { Outlet, useNavigate } from 'react-router-dom';
import { ArrowLeftIcon, ArrowRightIcon } from '@shared/icons';
const platformName = await platform();
export function AuthLayout() { export function AuthLayout() {
const navigate = useNavigate();
const goBack = () => {
navigate(-1);
};
const goForward = () => {
navigate(1);
};
return ( return (
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100"> <div className="relative h-screen w-screen bg-black/90">
<div className="flex h-screen w-full flex-col"> <div className="absolute left-0 top-0 z-50 h-16 w-full" data-tauri-drag-region />
<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 /> <Outlet />
</div> </div>
</div>
</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" className="inline-flex h-full w-full items-center justify-center bg-zinc-900/40"
> >
{loading ? ( {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> </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" className="inline-flex h-full w-full items-center justify-center bg-zinc-900/40"
> >
{loading ? ( {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> </button>
); );

View File

@ -16,19 +16,19 @@ export function Button({
switch (preset) { switch (preset) {
case 'small': case 'small':
preClass = 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; break;
case 'publish': case 'publish':
preClass = 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; break;
case 'large': case 'large':
preClass = 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; break;
case 'large-alt': case 'large-alt':
preClass = 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; break;
default: default:
break; break;
@ -40,7 +40,7 @@ export function Button({
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
className={twMerge( 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 preClass
)} )}
> >

View File

@ -14,7 +14,6 @@ import { CancelIcon, LoaderIcon, PlusCircleIcon } from '@shared/icons';
import { MentionNote } from '@shared/notes'; import { MentionNote } from '@shared/notes';
import { useComposer } from '@stores/composer'; import { useComposer } from '@stores/composer';
import { FULL_RELAYS } from '@stores/constants';
import { usePublish } from '@utils/hooks/usePublish'; import { usePublish } from '@utils/hooks/usePublish';
import { useImageUploader } from '@utils/hooks/useUploader'; import { useImageUploader } from '@utils/hooks/useUploader';
@ -45,7 +44,7 @@ export function Composer() {
Image.configure({ Image.configure({
HTMLAttributes: { HTMLAttributes: {
class: 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: { attributes: {
class: twMerge( class: twMerge(
'scrollbar-hide markdown break-all max-h-[500px] overflow-y-auto outline-none pr-2', '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.id && reply.pubkey) {
if (reply.root && reply.root.length > 1) { if (reply.root && reply.root.length > 1) {
tags = [ tags = [
['e', reply.root, FULL_RELAYS[0], 'root'], ['e', reply.root, 'wss://relayable.org', 'root'],
['e', reply.id, FULL_RELAYS[0], 'reply'], ['e', reply.id, 'wss://relayable.org', 'reply'],
['p', reply.pubkey], ['p', reply.pubkey],
]; ];
} else { } else {
tags = [ tags = [
['e', reply.id, FULL_RELAYS[0], 'reply'], ['e', reply.id, 'wss://relayable.org', 'reply'],
['p', reply.pubkey], ['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 flex-col px-4 pb-4">
<div className="flex h-full w-full gap-3"> <div className="flex h-full w-full gap-3">
<div className="flex w-8 shrink-0 items-center justify-center"> <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>
<div className="w-full"> <div className="w-full">
<EditorContent <EditorContent
@ -147,9 +146,9 @@ export function Composer() {
<button <button
type="button" type="button"
onClick={() => clearReply()} 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> </button>
</div> </div>
)} )}
@ -159,15 +158,15 @@ export function Composer() {
<button <button
type="button" type="button"
onClick={() => uploadImage()} 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>
<Button onClick={() => submit()} preset="publish"> <Button onClick={() => submit()} preset="publish">
{status === 'loading' ? ( {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> </Button>
</div> </div>

View File

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

View File

@ -1,8 +1,6 @@
import { Dialog, Transition } from '@headlessui/react'; import * as Dialog from '@radix-ui/react-dialog';
import { Fragment } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { Button } from '@shared/button';
import { Composer, ComposerUser } from '@shared/composer'; import { Composer, ComposerUser } from '@shared/composer';
import { import {
CancelIcon, CancelIcon,
@ -20,71 +18,45 @@ export function ComposerModal() {
const { account } = useAccount(); const { account } = useAccount();
const [toggle, open] = useComposer((state) => [state.toggleModal, state.open]); const [toggle, open] = useComposer((state) => [state.toggleModal, state.open]);
const closeModal = () => {
toggle(false);
};
useHotkeys(COMPOSE_SHORTCUT, () => toggle(true)); useHotkeys(COMPOSE_SHORTCUT, () => toggle(true));
return ( return (
<> <Dialog.Root open={open} onOpenChange={toggle}>
<Button onClick={() => toggle(true)} preset="small"> <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" /> <ComposeIcon className="h-4 w-4" />
Compose Postr
</Button> </button>
<Transition appear show={open} as={Fragment}> </Dialog.Trigger>
<Dialog as="div" className="relative z-10" onClose={closeModal}> <Dialog.Portal className="relative z-10">
<Transition.Child <Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
as={Fragment} <Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
enter="ease-out duration-300" <div className="relative h-min w-full max-w-2xl rounded-xl bg-white/10">
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 justify-between px-4 py-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{account && <ComposerUser pubkey={account.pubkey} />} {account && <ComposerUser pubkey={account.pubkey} />}
<span> <span>
<ChevronRightIcon <ChevronRightIcon className="h-4 w-4 text-white/50" />
width={14}
height={14}
className="text-zinc-500"
/>
</span> </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"> <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 New Post
<ChevronDownIcon className="h-4 w-4" /> <ChevronDownIcon className="h-4 w-4" />
</div> </div>
</div> </div>
<button <Dialog.Close
onClick={closeModal} onClick={() => toggle(false)}
type="button" className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-800"
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" /> <CancelIcon className="h-5 w-5 text-white/50" />
</button> </Dialog.Close>
</div> </div>
<Composer /> <Composer />
</Dialog.Panel>
</Transition.Child>
</div> </div>
</Dialog> </Dialog.Content>
</Transition> </Dialog.Portal>
</> </Dialog.Root>
); );
} }

View File

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