mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 19:46:34 +00:00
commit
ed759086c9
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/**/node_modules/*
|
||||||
|
node_modules/
|
||||||
|
dist/
|
@ -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>
|
||||||
|
31
package.json
31
package.json
@ -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"
|
||||||
}
|
}
|
||||||
|
979
pnpm-lock.yaml
979
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
BIN
public/clapping_hands.png
Normal file
BIN
public/clapping_hands.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 398 KiB |
BIN
public/clown_face.png
Normal file
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
BIN
public/crying_face.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
BIN
public/face_with_open_mouth.png
Normal file
BIN
public/face_with_open_mouth.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
BIN
public/face_with_tongue.png
Normal file
BIN
public/face_with_tongue.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 986 KiB |
BIN
public/lume.png
Normal file
BIN
public/lume.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
358
src-tauri/Cargo.lock
generated
358
src-tauri/Cargo.lock
generated
@ -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]]
|
||||||
|
@ -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",
|
||||||
] }
|
] }
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE accounts ADD network JSON;
|
@ -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");
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
81
src/app.tsx
81
src/app.tsx
@ -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 }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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 →'
|
||||||
)}
|
)}
|
||||||
|
@ -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 →'
|
||||||
)}
|
)}
|
||||||
|
@ -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 →'
|
||||||
)}
|
)}
|
||||||
|
@ -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 →'
|
||||||
)}
|
)}
|
||||||
|
@ -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 →'
|
||||||
)}
|
)}
|
||||||
|
@ -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 →'
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
|
@ -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're using old Lume version which store your private key as
|
You'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 →'
|
||||||
)}
|
)}
|
||||||
|
@ -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" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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'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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 →'
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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 ? (
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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} />}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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't talk yet, let's send first message
|
You two didn't talk yet, let's send first message
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
51
src/app/events/index.tsx
Normal file
51
src/app/events/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
200
src/app/root.tsx
200
src/app/root.tsx
@ -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't close app.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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're using latest version
|
You'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>
|
||||||
|
@ -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 />
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
212
src/app/space/components/modals/feed.tsx
Normal file
212
src/app/space/components/modals/feed.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
132
src/app/space/components/modals/hashtag.tsx
Normal file
132
src/app/space/components/modals/hashtag.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
179
src/app/space/components/modals/image.tsx
Normal file
179
src/app/space/components/modals/image.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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
58
src/app/splash.tsx
Normal 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't close app.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -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't fetch profile</p>
|
<p>Can'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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
|
@ -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
57
src/libs/ndk/cache.tsx
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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
44
src/libs/nip44.ts
Normal 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;
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user