Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8398ae80d3 | ||
|
|
fe60f75e96 | ||
|
|
e098743d43 | ||
|
|
0c19ada1ab | ||
|
|
09db39fce1 | ||
|
|
f0fc89724d | ||
|
|
afa9327bb7 | ||
|
|
5c3644f977 | ||
|
|
0a8eed9a46 | ||
|
|
bacfaed48a | ||
|
|
3d5085785b | ||
|
|
9152c3e122 | ||
|
|
a5574bef6c | ||
|
|
2c7f3685b6 | ||
|
|
be0abc4075 | ||
|
|
dafe35cd1f | ||
|
|
b23903240b | ||
|
|
872a6cee36 | ||
|
|
ac7ce726c5 | ||
|
|
e5e290c0c3 | ||
|
|
2eab6f04c7 | ||
|
|
e06b0334a5 | ||
|
|
74d8bf2ead |
+2
-1
@@ -23,4 +23,5 @@ dist-ssr
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
src/router.gen.ts
|
src/routes.gen.ts
|
||||||
|
src/commands.gen.ts
|
||||||
|
|||||||
+44
-45
@@ -12,73 +12,72 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getalby/bitcoin-connect-react": "^3.6.2",
|
"@getalby/bitcoin-connect-react": "^3.6.2",
|
||||||
"@phosphor-icons/react": "^2.1.7",
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-popover": "^1.1.1",
|
"@radix-ui/react-popover": "^1.1.2",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||||
"@radix-ui/react-switch": "^1.1.0",
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
"@tanstack/query-persist-client-core": "^5.51.21",
|
"@tanstack/query-persist-client-core": "^5.59.0",
|
||||||
"@tanstack/react-query": "^5.51.23",
|
"@tanstack/react-query": "^5.59.0",
|
||||||
"@tanstack/react-router": "^1.48.1",
|
"@tanstack/react-router": "^1.58.16",
|
||||||
"@tanstack/react-store": "^0.5.5",
|
"@tanstack/react-store": "^0.5.5",
|
||||||
"@tanstack/store": "^0.5.5",
|
"@tanstack/store": "^0.5.5",
|
||||||
"@tauri-apps/api": "2.0.0-rc.1",
|
"@tauri-apps/api": "^2.0.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": "2.0.0-rc.0",
|
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||||
"@tauri-apps/plugin-fs": "2.0.0-rc.1",
|
"@tauri-apps/plugin-fs": "^2.0.0",
|
||||||
"@tauri-apps/plugin-http": "2.0.0-rc.1",
|
"@tauri-apps/plugin-http": "^2.0.0",
|
||||||
"@tauri-apps/plugin-os": "2.0.0-rc.0",
|
"@tauri-apps/plugin-os": "^2.0.0",
|
||||||
"@tauri-apps/plugin-process": "2.0.0-rc.0",
|
"@tauri-apps/plugin-process": "^2.0.0",
|
||||||
"@tauri-apps/plugin-shell": "2.0.0-rc.0",
|
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||||
"@tauri-apps/plugin-store": "2.0.0-rc.0",
|
"@tauri-apps/plugin-store": "^2.0.0",
|
||||||
"@tauri-apps/plugin-updater": "2.0.0-rc.0",
|
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||||
"@tauri-apps/plugin-upload": "2.0.0-rc.0",
|
"@tauri-apps/plugin-upload": "^2.0.0",
|
||||||
"@tauri-apps/plugin-window-state": "2.0.0-rc.0",
|
"@tauri-apps/plugin-window-state": "^2.0.0",
|
||||||
"bitcoin-units": "^1.0.0",
|
"bitcoin-units": "^1.0.0",
|
||||||
"boring-avatars": "^1.10.2",
|
"boring-avatars": "^1.11.2",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.13",
|
||||||
"embla-carousel-react": "^8.1.8",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"i18next": "^23.13.0",
|
"i18next": "^23.15.1",
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
"i18next-resources-to-backend": "^1.2.1",
|
||||||
"light-bolt11-decoder": "^3.1.1",
|
"light-bolt11-decoder": "^3.2.0",
|
||||||
"minidenticons": "^4.2.1",
|
"minidenticons": "^4.2.1",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"react": "19.0.0-rc-d025ddd3-20240722",
|
"react": "19.0.0-rc-d025ddd3-20240722",
|
||||||
"react-currency-input-field": "^3.8.0",
|
"react-currency-input-field": "^3.8.0",
|
||||||
"react-dom": "19.0.0-rc-d025ddd3-20240722",
|
"react-dom": "19.0.0-rc-d025ddd3-20240722",
|
||||||
"react-hook-form": "^7.52.2",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.2",
|
||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
"slate": "^0.103.0",
|
"rich-textarea": "^0.26.3",
|
||||||
"slate-react": "^0.107.1",
|
|
||||||
"use-debounce": "^10.0.3",
|
"use-debounce": "^10.0.3",
|
||||||
"virtua": "^0.33.7"
|
"virtua": "^0.34.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.8.3",
|
"@biomejs/biome": "^1.9.3",
|
||||||
"@evilmartians/harmony": "^1.2.0",
|
"@evilmartians/harmony": "^1.2.0",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.14",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tanstack/router-devtools": "^1.48.1",
|
"@tanstack/router-devtools": "^1.58.16",
|
||||||
"@tanstack/router-plugin": "^1.47.0",
|
"@tanstack/router-plugin": "^1.58.12",
|
||||||
"@tauri-apps/cli": "2.0.0-rc.4",
|
"@tauri-apps/cli": "^2.0.0",
|
||||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625",
|
"babel-plugin-react-compiler": "0.0.0-experimental-b4db8c3-20241001",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.47",
|
||||||
"tailwind-gradient-mask-image": "^1.2.0",
|
"tailwind-gradient-mask-image": "^1.2.0",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwind-scrollbar": "^3.1.0",
|
"tailwind-scrollbar": "^3.1.0",
|
||||||
"tailwindcss": "^3.4.10",
|
"tailwindcss": "^3.4.13",
|
||||||
"tailwindcss-content-visibility": "^0.2.0",
|
"tailwindcss-content-visibility": "^1.0.0",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.6.2",
|
||||||
"vite": "^5.4.1",
|
"vite": "^5.4.8",
|
||||||
"vite-tsconfig-paths": "^5.0.1"
|
"vite-tsconfig-paths": "^5.0.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
Generated
+1066
-823
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="32" height="36" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M28.2711 21.2958C27.1084 21.2958 26.0749 21.8372 25.4037 22.6797L21.8702 20.6405C22.2577 19.8106 22.4755 18.8846 22.4755 17.908C22.4755 16.9314 22.2577 16.0053 21.8702 15.1755L25.3404 13.1742C26.0091 14.0648 27.0704 14.6442 28.2711 14.6442C30.2949 14.6442 31.9363 13.0047 31.9363 10.9831C31.9363 8.96158 30.2949 7.32208 28.2711 7.32208C26.2472 7.32208 24.6058 8.96158 24.6058 10.9831C24.6058 11.4006 24.6793 11.8003 24.8084 12.1748L21.3028 14.1963C20.2338 12.6732 18.5241 11.6333 16.5635 11.4638V7.274C18.3189 7.00076 19.6639 5.49029 19.6639 3.66104C19.6639 1.6395 18.0225 0 15.9987 0C13.9748 0 12.3334 1.6395 12.3334 3.66104C12.3334 5.49029 13.6784 7.00329 15.4338 7.274V11.4638C13.4733 11.6333 11.7635 12.6732 10.6946 14.1963L7.1889 12.1748C7.31808 11.8003 7.39154 11.4006 7.39154 10.9831C7.39154 8.96158 5.75015 7.32208 3.72629 7.32208C1.70242 7.32208 0.0610352 8.96158 0.0610352 10.9831C0.0610352 13.0047 1.70242 14.6442 3.72629 14.6442C4.92693 14.6442 5.98825 14.0648 6.65697 13.1742L10.1272 15.1755C9.73963 16.0053 9.52179 16.9314 9.52179 17.908C9.52179 18.8846 9.73963 19.8106 10.1272 20.643L6.59364 22.6822C5.9224 21.8397 4.88893 21.2983 3.72629 21.2983C1.70242 21.2983 0.0610352 22.9378 0.0610352 24.9593C0.0610352 26.9809 1.70242 28.6204 3.72629 28.6204C5.75015 28.6204 7.39154 26.9809 7.39154 24.9593C7.39154 24.5039 7.30542 24.0687 7.1509 23.6639L10.6946 21.6196C11.3329 22.5279 12.1992 23.2667 13.2098 23.7499V32.3497C13.2098 32.9721 13.5569 33.8551 13.9799 34.3131L15.2286 35.6565C15.6516 36.1145 16.3457 36.1145 16.7712 35.6565L18.02 34.3131C18.443 33.8551 18.79 32.9721 18.79 32.3497V23.7499C19.8007 23.2667 20.667 22.5304 21.3053 21.6196L24.849 23.6639C24.697 24.0662 24.6083 24.5014 24.6083 24.9593C24.6083 26.9809 26.2497 28.6204 28.2736 28.6204C30.2975 28.6204 31.9388 26.9809 31.9388 24.9593C31.9388 22.9378 30.2975 21.2983 28.2736 21.2983L28.2711 21.2958Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -5,3 +5,6 @@
|
|||||||
# Generated by Tauri
|
# Generated by Tauri
|
||||||
# will have schema files for capabilities auto-completion
|
# will have schema files for capabilities auto-completion
|
||||||
/gen/schemas
|
/gen/schemas
|
||||||
|
|
||||||
|
# Config
|
||||||
|
.cargo
|
||||||
|
|||||||
Generated
+1063
-950
File diff suppressed because it is too large
Load Diff
+24
-23
@@ -8,39 +8,39 @@ edition = "2021"
|
|||||||
rust-version = "1.70"
|
rust-version = "1.70"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
tauri-build = { version = "2.0.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
|
tauri = { version = "2.0.0", features = [
|
||||||
"sqlite",
|
|
||||||
] }
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
tauri = { version = "2.0.0-rc", features = [
|
|
||||||
"unstable",
|
"unstable",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"macos-private-api",
|
"macos-private-api",
|
||||||
"protocol-asset",
|
"protocol-asset",
|
||||||
] }
|
] }
|
||||||
tauri-plugin-window-state = "2.0.0-rc"
|
tauri-plugin-window-state = "2.0.0"
|
||||||
tauri-plugin-clipboard-manager = "2.0.0-rc"
|
tauri-plugin-clipboard-manager = "2.0.0"
|
||||||
tauri-plugin-dialog = "2.0.0-rc"
|
tauri-plugin-dialog = "2.0.0"
|
||||||
tauri-plugin-fs = "2.0.0-rc"
|
tauri-plugin-fs = "2.0.0"
|
||||||
tauri-plugin-http = "2.0.0-rc"
|
tauri-plugin-http = "2.0.0"
|
||||||
tauri-plugin-notification = "2.0.0-rc"
|
tauri-plugin-notification = "2.0.0"
|
||||||
tauri-plugin-os = "2.0.0-rc"
|
tauri-plugin-os = "2.0.0"
|
||||||
tauri-plugin-process = "2.0.0-rc"
|
tauri-plugin-process = "2.0.0"
|
||||||
tauri-plugin-shell = "2.0.0-rc"
|
tauri-plugin-shell = "2.0.0"
|
||||||
tauri-plugin-updater = "2.0.0-rc"
|
tauri-plugin-updater = "2.0.0"
|
||||||
tauri-plugin-upload = "2.0.0-rc"
|
tauri-plugin-upload = "2.0.0"
|
||||||
tauri-plugin-store = "2.0.0-rc"
|
tauri-plugin-store = "2.0.0"
|
||||||
tauri-plugin-theme = "0.4.1"
|
tauri-plugin-decorum = { git = "https://github.com/clearlysid/tauri-plugin-decorum.git" }
|
||||||
tauri-plugin-decorum = "1.0.0"
|
tauri-plugin-prevent-default = "0.6"
|
||||||
tauri-plugin-prevent-default = "0.4"
|
|
||||||
tauri-specta = { version = "2.0.0-rc.15", features = ["derive", "typescript"] }
|
tauri-specta = { version = "2.0.0-rc.15", features = ["derive", "typescript"] }
|
||||||
|
|
||||||
|
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = ["lmdb"] }
|
||||||
|
nostr-relay-builder = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
|
|
||||||
specta = "^2.0.0-rc.20"
|
specta = "^2.0.0-rc.20"
|
||||||
specta-typescript = "0.0.7"
|
specta-typescript = "0.0.7"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
reqwest = "0.12.4"
|
reqwest = "0.12.4"
|
||||||
url = "2.5.0"
|
url = "2.5.0"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
@@ -48,6 +48,7 @@ linkify = "0.10.0"
|
|||||||
regex = "1.10.4"
|
regex = "1.10.4"
|
||||||
keyring = { version = "3", features = ["apple-native", "windows-native"] }
|
keyring = { version = "3", features = ["apple-native", "windows-native"] }
|
||||||
keyring-search = "1.2.0"
|
keyring-search = "1.2.0"
|
||||||
|
tracing-subscriber = "0.3.18"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
cocoa = "0.25.0"
|
cocoa = "0.25.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "desktop-capability",
|
"identifier": "column",
|
||||||
"description": "Capability for the column",
|
"description": "Capability for the column",
|
||||||
"platforms": [
|
"platforms": [
|
||||||
"linux",
|
"linux",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "desktop-capability",
|
"identifier": "window",
|
||||||
"description": "Capability for the desktop",
|
"description": "Capability for the desktop",
|
||||||
"platforms": [
|
"platforms": [
|
||||||
"macOS",
|
"macOS",
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
"core:window:allow-minimize",
|
"core:window:allow-minimize",
|
||||||
"core:window:allow-maximize",
|
"core:window:allow-maximize",
|
||||||
"core:window:allow-set-size",
|
"core:window:allow-set-size",
|
||||||
"core:window:allow-set-focus",
|
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-toggle-maximize",
|
||||||
"decorum:allow-show-snap-overlay",
|
"decorum:allow-show-snap-overlay",
|
||||||
"clipboard-manager:allow-write-text",
|
"clipboard-manager:allow-write-text",
|
||||||
"clipboard-manager:allow-read-text",
|
"clipboard-manager:allow-read-text",
|
||||||
@@ -57,14 +57,10 @@
|
|||||||
"process:allow-restart",
|
"process:allow-restart",
|
||||||
"process:allow-exit",
|
"process:allow-exit",
|
||||||
"fs:allow-read-file",
|
"fs:allow-read-file",
|
||||||
"theme:allow-set-theme",
|
|
||||||
"theme:allow-get-theme",
|
|
||||||
"core:menu:allow-new",
|
"core:menu:allow-new",
|
||||||
"core:menu:allow-popup",
|
"core:menu:allow-popup",
|
||||||
"shell:allow-open",
|
"shell:allow-open",
|
||||||
"store:allow-get",
|
"store:default",
|
||||||
"store:allow-set",
|
|
||||||
"store:allow-delete",
|
|
||||||
"prevent-default:default",
|
"prevent-default:default",
|
||||||
{
|
{
|
||||||
"identifier": "http:default",
|
"identifier": "http:default",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","panel","settings","search-*","zap-*","event-*","user-*","editor-*"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","core:window:allow-create","core:window:allow-close","core:window:allow-destroy","core:window:allow-set-focus","core:window:allow-center","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-set-size","core:window:allow-set-focus","core:window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-set-webview-size","core:webview:allow-set-webview-position","core:webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","process:allow-exit","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","core:menu:allow-new","core:menu:allow-popup","shell:allow-open","store:allow-get","store:allow-set","store:allow-delete","prevent-default:default",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["macOS","windows"]}}
|
{"column":{"identifier":"column","description":"Capability for the column","local":true,"windows":["column-*"],"permissions":["core:resources:default","core:tray:default","os:allow-locale","os:allow-os-type","clipboard-manager:allow-write-text","dialog:allow-open","dialog:allow-ask","dialog:allow-message","fs:allow-read-file","core:menu:default","core:menu:allow-new","core:menu:allow-popup","http:default","shell:allow-open","store:allow-get","store:allow-set","store:allow-delete",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]},"window":{"identifier":"window","description":"Capability for the desktop","local":true,"windows":["main","panel","settings","search-*","zap-*","event-*","user-*","editor-*"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","core:window:allow-create","core:window:allow-close","core:window:allow-destroy","core:window:allow-set-focus","core:window:allow-center","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-set-size","core:window:allow-start-dragging","core:window:allow-toggle-maximize","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-set-webview-size","core:webview:allow-set-webview-position","core:webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","process:allow-exit","fs:allow-read-file","core:menu:allow-new","core:menu:allow-popup","shell:allow-open","store:default","prevent-default:default",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["macOS","windows"]}}
|
||||||
+3328
-5211
File diff suppressed because it is too large
Load Diff
+3328
-5211
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,2 @@
|
|||||||
wss://relay.damus.io,
|
wss://relay.damus.io,
|
||||||
wss://relay.nostr.net,
|
wss://relay.nostr.net,
|
||||||
wss://purplepag.es/,
|
|
||||||
wss://directory.yabu.me/,
|
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ use keyring_search::{Limit, List, Search};
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
use std::{collections::HashSet, str::FromStr, time::Duration};
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
fs::{self, File},
|
||||||
|
str::FromStr,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
use tauri::{Emitter, Manager, State};
|
use tauri::{Emitter, Manager, State};
|
||||||
|
|
||||||
use crate::{
|
use crate::{common::init_nip65, Nostr, NOTIFICATION_SUB_ID};
|
||||||
common::{get_user_settings, init_nip65},
|
|
||||||
Nostr, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT, NOTIFICATION_SUB_ID,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||||
struct Account {
|
struct Account {
|
||||||
@@ -44,7 +46,7 @@ pub async fn create_account(
|
|||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let keys = Keys::generate();
|
let keys = Keys::generate();
|
||||||
let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?;
|
let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?;
|
||||||
let secret_key = keys.secret_key().map_err(|e| e.to_string())?;
|
let secret_key = keys.secret_key();
|
||||||
let enc = EncryptedSecretKey::new(secret_key, password, 16, KeySecurity::Medium)
|
let enc = EncryptedSecretKey::new(secret_key, password, 16, KeySecurity::Medium)
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||||
@@ -80,38 +82,40 @@ pub async fn create_account(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn import_account(
|
pub async fn import_account(key: String, password: String) -> Result<String, String> {
|
||||||
key: String,
|
let (npub, enc_bech32) = match key.starts_with("ncryptsec") {
|
||||||
password: Option<String>,
|
true => {
|
||||||
state: State<'_, Nostr>,
|
let enc = EncryptedSecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||||
) -> Result<String, String> {
|
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||||
let client = &state.client;
|
let secret_key = enc.to_secret_key(password).map_err(|err| err.to_string())?;
|
||||||
let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
let keys = Keys::new(secret_key);
|
||||||
let keys = Keys::new(secret_key.clone());
|
let npub = keys.public_key().to_bech32().unwrap();
|
||||||
let npub = keys.public_key().to_bech32().unwrap();
|
|
||||||
|
|
||||||
let enc_bech32 = match password {
|
(npub, enc_bech32)
|
||||||
Some(pw) => {
|
}
|
||||||
let enc = EncryptedSecretKey::new(&secret_key, pw, 16, KeySecurity::Medium)
|
false => {
|
||||||
|
let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||||
|
let keys = Keys::new(secret_key.clone());
|
||||||
|
let npub = keys.public_key().to_bech32().unwrap();
|
||||||
|
|
||||||
|
let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium)
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
enc.to_bech32().map_err(|err| err.to_string())?
|
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
(npub, enc_bech32)
|
||||||
}
|
}
|
||||||
None => secret_key.to_bech32().map_err(|err| err.to_string())?,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let keyring = Entry::new("Lume Secret Storage", &npub).map_err(|e| e.to_string())?;
|
let keyring = Entry::new("Lume Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let account = Account {
|
let account = Account {
|
||||||
password: enc_bech32,
|
password: enc_bech32,
|
||||||
nostr_connect: None,
|
nostr_connect: None,
|
||||||
};
|
};
|
||||||
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
|
||||||
let _ = keyring.set_password(&j);
|
|
||||||
|
|
||||||
let signer = NostrSigner::Keys(keys);
|
let pwd = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||||
|
keyring.set_password(&pwd).map_err(|e| e.to_string())?;
|
||||||
// Update client's signer
|
|
||||||
client.set_signer(Some(signer)).await;
|
|
||||||
|
|
||||||
Ok(npub)
|
Ok(npub)
|
||||||
}
|
}
|
||||||
@@ -125,13 +129,13 @@ pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result<Str
|
|||||||
Ok(bunker_uri) => {
|
Ok(bunker_uri) => {
|
||||||
// Local user
|
// Local user
|
||||||
let app_keys = Keys::generate();
|
let app_keys = Keys::generate();
|
||||||
let app_secret = app_keys.secret_key().unwrap().to_string();
|
let app_secret = app_keys.secret_key().to_secret_hex();
|
||||||
|
|
||||||
// Get remote user
|
// Get remote user
|
||||||
let remote_user = bunker_uri.signer_public_key().unwrap();
|
let remote_user = bunker_uri.signer_public_key().unwrap();
|
||||||
let remote_npub = remote_user.to_bech32().unwrap();
|
let remote_npub = remote_user.to_bech32().unwrap();
|
||||||
|
|
||||||
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await {
|
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None) {
|
||||||
Ok(signer) => {
|
Ok(signer) => {
|
||||||
let mut url = Url::parse(&uri).unwrap();
|
let mut url = Url::parse(&uri).unwrap();
|
||||||
let query: Vec<(String, String)> = url
|
let query: Vec<(String, String)> = url
|
||||||
@@ -202,6 +206,17 @@ pub fn delete_account(id: String) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn is_account_sync(id: String, handle: tauri::AppHandle) -> bool {
|
||||||
|
let config_dir = handle
|
||||||
|
.path()
|
||||||
|
.app_config_dir()
|
||||||
|
.expect("Error: app config directory not found.");
|
||||||
|
|
||||||
|
fs::metadata(config_dir.join(id)).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
@@ -242,7 +257,7 @@ pub async fn login(
|
|||||||
let public_key = uri.signer_public_key().unwrap().to_bech32().unwrap();
|
let public_key = uri.signer_public_key().unwrap().to_bech32().unwrap();
|
||||||
let app_keys = Keys::from_str(&account.password).map_err(|e| e.to_string())?;
|
let app_keys = Keys::from_str(&account.password).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
match Nip46Signer::new(uri, app_keys, Duration::from_secs(120), None).await {
|
match Nip46Signer::new(uri, app_keys, Duration::from_secs(120), None) {
|
||||||
Ok(signer) => {
|
Ok(signer) => {
|
||||||
// Update signer
|
// Update signer
|
||||||
client.set_signer(Some(signer.into())).await;
|
client.set_signer(Some(signer.into())).await;
|
||||||
@@ -253,84 +268,169 @@ pub async fn login(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect to user's relay (NIP-65)
|
// NIP-65: Connect to user's relay list
|
||||||
init_nip65(client).await;
|
init_nip65(client, &public_key).await;
|
||||||
|
|
||||||
// Get user's contact list
|
|
||||||
if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await {
|
|
||||||
let mut contacts_state = state.contact_list.lock().await;
|
|
||||||
*contacts_state = contacts;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get user's settings
|
|
||||||
if let Ok(settings) = get_user_settings(client).await {
|
|
||||||
let mut settings_state = state.settings.lock().await;
|
|
||||||
*settings_state = settings;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Run seperate thread for syncing data
|
||||||
|
let pk = public_key.clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let config_dir = handle.path().app_config_dir().unwrap();
|
||||||
let state = handle.state::<Nostr>();
|
let state = handle.state::<Nostr>();
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let contact_list = state.contact_list.lock().await;
|
|
||||||
|
|
||||||
let signer = client.signer().await.unwrap();
|
// Convert current user to PublicKey
|
||||||
let public_key = signer.public_key().await.unwrap();
|
let author = PublicKey::from_str(&pk).unwrap();
|
||||||
|
|
||||||
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
// Fetching user's metadata
|
||||||
|
if let Ok(report) = client
|
||||||
if !contact_list.is_empty() {
|
.reconcile(
|
||||||
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
|
Filter::new()
|
||||||
let sync = Filter::new()
|
.author(author)
|
||||||
.authors(authors.clone())
|
.kinds(vec![
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
Kind::Metadata,
|
||||||
.limit(NEWSFEED_NEG_LIMIT);
|
Kind::ContactList,
|
||||||
|
Kind::MuteList,
|
||||||
if client
|
Kind::Bookmarks,
|
||||||
.reconcile(sync, NegentropyOptions::default())
|
Kind::Interests,
|
||||||
.await
|
Kind::PinList,
|
||||||
.is_ok()
|
])
|
||||||
{
|
.limit(1000),
|
||||||
handle.emit("newsfeed_synchronized", ()).unwrap();
|
NegentropyOptions::default(),
|
||||||
}
|
)
|
||||||
};
|
|
||||||
|
|
||||||
drop(contact_list);
|
|
||||||
|
|
||||||
let sync = Filter::new()
|
|
||||||
.pubkey(public_key)
|
|
||||||
.kinds(vec![
|
|
||||||
Kind::TextNote,
|
|
||||||
Kind::Repost,
|
|
||||||
Kind::Reaction,
|
|
||||||
Kind::ZapReceipt,
|
|
||||||
])
|
|
||||||
.limit(NOTIFICATION_NEG_LIMIT);
|
|
||||||
|
|
||||||
// Sync notification with negentropy
|
|
||||||
if client
|
|
||||||
.reconcile(sync, NegentropyOptions::default())
|
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
|
||||||
{
|
{
|
||||||
handle.emit("notification_synchronized", ()).unwrap();
|
println!("Received: {}", report.received.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
let notification = Filter::new()
|
// Fetching user's events
|
||||||
.pubkey(public_key)
|
if let Ok(report) = client
|
||||||
.kinds(vec![
|
.reconcile(
|
||||||
Kind::TextNote,
|
Filter::new()
|
||||||
Kind::Repost,
|
.author(author)
|
||||||
Kind::Reaction,
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
Kind::ZapReceipt,
|
.limit(200),
|
||||||
])
|
NegentropyOptions::default(),
|
||||||
.since(Timestamp::now());
|
)
|
||||||
|
|
||||||
// Subscribing for new notification...
|
|
||||||
if let Err(e) = client
|
|
||||||
.subscribe_with_id(notification_id, vec![notification], None)
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
println!("Error: {}", e)
|
println!("Received: {}", report.received.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetching user's notification
|
||||||
|
if let Ok(report) = client
|
||||||
|
.reconcile(
|
||||||
|
Filter::new()
|
||||||
|
.pubkey(author)
|
||||||
|
.kinds(vec![
|
||||||
|
Kind::TextNote,
|
||||||
|
Kind::Repost,
|
||||||
|
Kind::Reaction,
|
||||||
|
Kind::ZapReceipt,
|
||||||
|
])
|
||||||
|
.limit(200),
|
||||||
|
NegentropyOptions::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
println!("Received: {}", report.received.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe for new notification
|
||||||
|
if let Ok(e) = client
|
||||||
|
.subscribe_with_id(
|
||||||
|
SubscriptionId::new(NOTIFICATION_SUB_ID),
|
||||||
|
vec![Filter::new()
|
||||||
|
.pubkey(author)
|
||||||
|
.kinds(vec![
|
||||||
|
Kind::TextNote,
|
||||||
|
Kind::Repost,
|
||||||
|
Kind::Reaction,
|
||||||
|
Kind::ZapReceipt,
|
||||||
|
])
|
||||||
|
.since(Timestamp::now())],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
println!("Subscribed: {}", e.success.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's contact list
|
||||||
|
let contact_list = {
|
||||||
|
let contacts = client.get_contact_list(None).await.unwrap();
|
||||||
|
// Update app's state
|
||||||
|
state.contact_list.lock().await.clone_from(&contacts);
|
||||||
|
// Return
|
||||||
|
contacts
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get events from contact list
|
||||||
|
if !contact_list.is_empty() {
|
||||||
|
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
|
||||||
|
|
||||||
|
// Fetching contact's metadata
|
||||||
|
if let Ok(report) = client
|
||||||
|
.reconcile(
|
||||||
|
Filter::new()
|
||||||
|
.authors(authors.clone())
|
||||||
|
.kinds(vec![Kind::Metadata, Kind::ContactList, Kind::MuteList])
|
||||||
|
.limit(3000),
|
||||||
|
NegentropyOptions::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
println!("Received: {}", report.received.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetching contact's events
|
||||||
|
if let Ok(report) = client
|
||||||
|
.reconcile(
|
||||||
|
Filter::new()
|
||||||
|
.authors(authors.clone())
|
||||||
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
|
.limit(1000),
|
||||||
|
NegentropyOptions::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
println!("Received: {}", report.received.len());
|
||||||
|
|
||||||
|
// Save the process status
|
||||||
|
let _ = File::create(config_dir.join(author.to_bech32().unwrap()));
|
||||||
|
// Update frontend
|
||||||
|
handle.emit("synchronized", ()).unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
for author in authors.into_iter() {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.author(author)
|
||||||
|
.kind(Kind::ContactList)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let mut circles = state.circles.lock().await;
|
||||||
|
let mut list: Vec<PublicKey> = Vec::new();
|
||||||
|
|
||||||
|
if let Ok(events) = client.database().query(vec![filter]).await {
|
||||||
|
if let Some(event) = events.into_iter().next() {
|
||||||
|
for tag in event.tags.into_iter() {
|
||||||
|
if let Some(TagStandard::PublicKey {
|
||||||
|
public_key,
|
||||||
|
uppercase: false,
|
||||||
|
..
|
||||||
|
}) = tag.to_standardized()
|
||||||
|
{
|
||||||
|
list.push(public_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !list.is_empty() {
|
||||||
|
circles.insert(author, list);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handle.emit("synchronized", ()).unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::{str::FromStr, time::Duration};
|
|||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
use crate::common::{create_event_tags, filter_converstation, parse_event, Meta};
|
use crate::common::{create_event_tags, filter_converstation, parse_event, Meta};
|
||||||
use crate::{Nostr, FETCH_LIMIT};
|
use crate::{Nostr, DEFAULT_DIFFICULTY, FETCH_LIMIT};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Type)]
|
#[derive(Debug, Clone, Serialize, Type)]
|
||||||
pub struct RichEvent {
|
pub struct RichEvent {
|
||||||
@@ -28,13 +28,7 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent,
|
|||||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||||
let filter = Filter::new().id(event_id);
|
let filter = Filter::new().id(event_id);
|
||||||
|
|
||||||
match client
|
match client.database().query(vec![filter.clone()]).await {
|
||||||
.get_events_of(
|
|
||||||
vec![filter],
|
|
||||||
EventSource::both(Some(Duration::from_secs(5))),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
if let Some(event) = events.first() {
|
if let Some(event) = events.first() {
|
||||||
let raw = event.as_json();
|
let raw = event.as_json();
|
||||||
@@ -46,7 +40,29 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent,
|
|||||||
|
|
||||||
Ok(RichEvent { raw, parsed })
|
Ok(RichEvent { raw, parsed })
|
||||||
} else {
|
} else {
|
||||||
Err("Cannot found this event with current relay list".into())
|
match client
|
||||||
|
.get_events_of(
|
||||||
|
vec![filter],
|
||||||
|
EventSource::relays(Some(Duration::from_secs(5))),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(events) => {
|
||||||
|
if let Some(event) = events.first() {
|
||||||
|
let raw = event.as_json();
|
||||||
|
let parsed = if event.kind == Kind::TextNote {
|
||||||
|
Some(parse_event(&event.content).await)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(RichEvent { raw, parsed })
|
||||||
|
} else {
|
||||||
|
Err("Cannot found this event with current relay list".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
@@ -191,15 +207,12 @@ pub async fn get_events_by(
|
|||||||
let author = PublicKey::parse(&public_key).map_err(|err| err.to_string())?;
|
let author = PublicKey::parse(&public_key).map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kinds(vec![Kind::TextNote])
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
.author(author)
|
.author(author)
|
||||||
.limit(limit as usize);
|
.limit(limit as usize);
|
||||||
|
|
||||||
match client
|
match client
|
||||||
.get_events_of(
|
.get_events_of(vec![filter], EventSource::Database)
|
||||||
vec![filter],
|
|
||||||
EventSource::both(Some(Duration::from_secs(5))),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
@@ -224,17 +237,11 @@ pub async fn get_events_by(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_events_from_contacts(
|
pub async fn get_local_events(
|
||||||
until: Option<&str>,
|
until: Option<&str>,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<Vec<RichEvent>, String> {
|
) -> Result<Vec<RichEvent>, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let contact_list = state.contact_list.lock().await;
|
|
||||||
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
|
|
||||||
|
|
||||||
if authors.is_empty() {
|
|
||||||
return Err("Contact List is empty.".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let as_of = match until {
|
let as_of = match until {
|
||||||
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
||||||
@@ -244,10 +251,9 @@ pub async fn get_events_from_contacts(
|
|||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
.limit(FETCH_LIMIT)
|
.limit(FETCH_LIMIT)
|
||||||
.until(as_of)
|
.until(as_of);
|
||||||
.authors(authors);
|
|
||||||
|
|
||||||
match client.database().query(vec![filter], Order::Desc).await {
|
match client.database().query(vec![filter]).await {
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
let fils = filter_converstation(events);
|
let fils = filter_converstation(events);
|
||||||
let futures = fils.iter().map(|ev| async move {
|
let futures = fils.iter().map(|ev| async move {
|
||||||
@@ -295,7 +301,7 @@ pub async fn get_group_events(
|
|||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
.limit(FETCH_LIMIT)
|
.limit(20)
|
||||||
.until(as_of)
|
.until(as_of)
|
||||||
.authors(authors);
|
.authors(authors);
|
||||||
|
|
||||||
@@ -341,7 +347,7 @@ pub async fn get_global_events(
|
|||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
.limit(FETCH_LIMIT)
|
.limit(20)
|
||||||
.until(as_of);
|
.until(as_of);
|
||||||
|
|
||||||
match client
|
match client
|
||||||
@@ -385,7 +391,7 @@ pub async fn get_hashtag_events(
|
|||||||
};
|
};
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
.limit(FETCH_LIMIT)
|
.limit(20)
|
||||||
.until(as_of)
|
.until(as_of)
|
||||||
.hashtags(hashtags);
|
.hashtags(hashtags);
|
||||||
|
|
||||||
@@ -429,36 +435,26 @@ pub async fn publish(
|
|||||||
// Create tags from content
|
// Create tags from content
|
||||||
let mut tags = create_event_tags(&content);
|
let mut tags = create_event_tags(&content);
|
||||||
|
|
||||||
|
// Add client tag
|
||||||
|
// TODO: allow user config this setting
|
||||||
|
tags.push(Tag::custom(TagKind::custom("client"), vec!["Lume"]));
|
||||||
|
|
||||||
// Add content-warning tag if present
|
// Add content-warning tag if present
|
||||||
if let Some(reason) = warning {
|
if let Some(reason) = warning {
|
||||||
let t = TagStandard::ContentWarning {
|
let t = TagStandard::ContentWarning {
|
||||||
reason: Some(reason),
|
reason: Some(reason),
|
||||||
};
|
};
|
||||||
let tag = Tag::from(t);
|
let tag = Tag::from_standardized(t);
|
||||||
tags.push(tag)
|
tags.push(tag)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get signer
|
|
||||||
let signer = match client.signer().await {
|
|
||||||
Ok(signer) => signer,
|
|
||||||
Err(_) => return Err("Signer is required.".into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get public key
|
|
||||||
let public_key = signer.public_key().await.map_err(|err| err.to_string())?;
|
|
||||||
|
|
||||||
// Create unsigned event
|
// Create unsigned event
|
||||||
let unsigned_event = match difficulty {
|
let builder =
|
||||||
Some(num) => EventBuilder::text_note(content, tags).to_unsigned_pow_event(public_key, num),
|
EventBuilder::text_note(content, tags).pow(difficulty.unwrap_or(DEFAULT_DIFFICULTY));
|
||||||
None => EventBuilder::text_note(content, tags).to_unsigned_event(public_key),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Publish
|
// Publish
|
||||||
match signer.sign_event(unsigned_event).await {
|
match client.send_event_builder(builder).await {
|
||||||
Ok(event) => match client.send_event(event).await {
|
Ok(event_id) => Ok(event_id.to_bech32().unwrap()),
|
||||||
Ok(event_id) => Ok(event_id.to_bech32().map_err(|err| err.to_string())?),
|
|
||||||
Err(err) => Err(err.to_string()),
|
|
||||||
},
|
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,14 +475,11 @@ pub async fn reply(
|
|||||||
|
|
||||||
let reply_id = EventId::parse(&to).map_err(|err| err.to_string())?;
|
let reply_id = EventId::parse(&to).map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
match database
|
match database.query(vec![Filter::new().id(reply_id)]).await {
|
||||||
.query(vec![Filter::new().id(reply_id)], Order::Desc)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
if let Some(event) = events.first() {
|
if let Some(event) = events.first() {
|
||||||
let relay_hint = if let Some(relays) = database
|
let relay_hint = if let Some(relays) = database
|
||||||
.event_seen_on_relays(event.id)
|
.event_seen_on_relays(&event.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| err.to_string())?
|
.map_err(|err| err.to_string())?
|
||||||
{
|
{
|
||||||
@@ -515,13 +508,10 @@ pub async fn reply(
|
|||||||
Err(_) => return Err("Event is not valid.".into()),
|
Err(_) => return Err("Event is not valid.".into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(events) = database
|
if let Ok(events) = database.query(vec![Filter::new().id(root_id)]).await {
|
||||||
.query(vec![Filter::new().id(root_id)], Order::Desc)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
if let Some(event) = events.first() {
|
if let Some(event) = events.first() {
|
||||||
let relay_hint = if let Some(relays) = database
|
let relay_hint = if let Some(relays) = database
|
||||||
.event_seen_on_relays(event.id)
|
.event_seen_on_relays(&event.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| err.to_string())?
|
.map_err(|err| err.to_string())?
|
||||||
{
|
{
|
||||||
@@ -579,7 +569,7 @@ pub async fn event_to_bech32(id: String, state: State<'_, Nostr>) -> Result<Stri
|
|||||||
|
|
||||||
let seens = client
|
let seens = client
|
||||||
.database()
|
.database()
|
||||||
.event_seen_on_relays(event_id)
|
.event_seen_on_relays(&event_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,25 @@ pub struct Profile {
|
|||||||
website: Option<String>,
|
website: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||||
|
pub struct Mention {
|
||||||
|
pubkey: String,
|
||||||
|
avatar: String,
|
||||||
|
display_name: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<String, String> {
|
pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
|
||||||
let public_key: PublicKey = match id {
|
let public_key: PublicKey = match id {
|
||||||
Some(user_id) => PublicKey::parse(&user_id).map_err(|e| e.to_string())?,
|
Some(user_id) => PublicKey::parse(&user_id).map_err(|e| e.to_string())?,
|
||||||
None => client.signer().await.unwrap().public_key().await.unwrap(),
|
None => {
|
||||||
|
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||||
|
signer.public_key().await.map_err(|e| e.to_string())?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
@@ -34,13 +46,7 @@ pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<
|
|||||||
.kind(Kind::Metadata)
|
.kind(Kind::Metadata)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
match client
|
match client.database().query(vec![filter.clone()]).await {
|
||||||
.get_events_of(
|
|
||||||
vec![filter],
|
|
||||||
EventSource::both(Some(Duration::from_secs(3))),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
if let Some(event) = events.first() {
|
if let Some(event) = events.first() {
|
||||||
if let Ok(metadata) = Metadata::from_json(&event.content) {
|
if let Ok(metadata) = Metadata::from_json(&event.content) {
|
||||||
@@ -49,7 +55,26 @@ pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<
|
|||||||
Err("Parse metadata failed".into())
|
Err("Parse metadata failed".into())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(Metadata::new().as_json())
|
match client
|
||||||
|
.get_events_of(
|
||||||
|
vec![filter],
|
||||||
|
EventSource::relays(Some(Duration::from_secs(5))),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(events) => {
|
||||||
|
if let Some(event) = events.first() {
|
||||||
|
if let Ok(metadata) = Metadata::from_json(&event.content) {
|
||||||
|
Ok(metadata.as_json())
|
||||||
|
} else {
|
||||||
|
Err("Parse metadata failed".into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Metadata::new().as_json())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
@@ -71,9 +96,6 @@ pub async fn set_contact_list(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Update local state
|
|
||||||
state.contact_list.lock().await.clone_from(&contact_list);
|
|
||||||
|
|
||||||
match client.set_contact_list(contact_list).await {
|
match client.set_contact_list(contact_list).await {
|
||||||
Ok(_) => Ok(true),
|
Ok(_) => Ok(true),
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
@@ -85,18 +107,13 @@ pub async fn set_contact_list(
|
|||||||
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
|
||||||
match client.get_contact_list(Some(Duration::from_secs(10))).await {
|
match client.get_contact_list(Some(Duration::from_secs(5))).await {
|
||||||
Ok(contact_list) => {
|
Ok(contact_list) => {
|
||||||
if !contact_list.is_empty() {
|
let list = contact_list
|
||||||
let list = contact_list
|
.into_iter()
|
||||||
.into_iter()
|
.map(|f| f.public_key.to_hex())
|
||||||
.map(|f| f.public_key.to_hex())
|
.collect();
|
||||||
.collect();
|
Ok(list)
|
||||||
|
|
||||||
Ok(list)
|
|
||||||
} else {
|
|
||||||
Err("Empty.".into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
}
|
}
|
||||||
@@ -137,21 +154,13 @@ pub async fn set_profile(profile: Profile, state: State<'_, Nostr>) -> Result<St
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn is_contact_list_empty(state: State<'_, Nostr>) -> Result<bool, ()> {
|
pub async fn check_contact(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||||
Ok(state.contact_list.lock().await.is_empty())
|
let contact_list = &state.contact_list.lock().await;
|
||||||
}
|
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
#[tauri::command]
|
match contact_list.iter().position(|x| x.public_key == public_key) {
|
||||||
#[specta::specta]
|
Some(_) => Ok(true),
|
||||||
pub async fn check_contact(hex: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
None => Ok(false),
|
||||||
let contact_list = state.contact_list.lock().await;
|
|
||||||
|
|
||||||
match PublicKey::parse(&hex) {
|
|
||||||
Ok(public_key) => match contact_list.iter().position(|x| x.public_key == public_key) {
|
|
||||||
Some(_) => Ok(true),
|
|
||||||
None => Ok(false),
|
|
||||||
},
|
|
||||||
Err(e) => Err(e.to_string()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,9 +190,6 @@ pub async fn toggle_contact(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update local state
|
|
||||||
state.contact_list.lock().await.clone_from(&contact_list);
|
|
||||||
|
|
||||||
// Publish
|
// Publish
|
||||||
match client.set_contact_list(contact_list).await {
|
match client.set_contact_list(contact_list).await {
|
||||||
Ok(event_id) => Ok(event_id.to_string()),
|
Ok(event_id) => Ok(event_id.to_string()),
|
||||||
@@ -194,6 +200,36 @@ pub async fn toggle_contact(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_mention_list(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
let filter = Filter::new().kind(Kind::Metadata);
|
||||||
|
|
||||||
|
let events = client
|
||||||
|
.database()
|
||||||
|
.query(vec![filter])
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let data: Vec<Mention> = events
|
||||||
|
.iter()
|
||||||
|
.map(|event| {
|
||||||
|
let pubkey = event.pubkey.to_bech32().unwrap();
|
||||||
|
let metadata = Metadata::from_json(&event.content).unwrap_or(Metadata::new());
|
||||||
|
|
||||||
|
Mention {
|
||||||
|
pubkey,
|
||||||
|
avatar: metadata.picture.unwrap_or_else(|| "".to_string()),
|
||||||
|
display_name: metadata.display_name.unwrap_or_else(|| "".to_string()),
|
||||||
|
name: metadata.name.unwrap_or_else(|| "".to_string()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn set_lume_store(
|
pub async fn set_lume_store(
|
||||||
@@ -206,7 +242,7 @@ pub async fn set_lume_store(
|
|||||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let encrypted = signer
|
let encrypted = signer
|
||||||
.nip44_encrypt(public_key, content)
|
.nip44_encrypt(&public_key, content)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let tag = Tag::identifier(key);
|
let tag = Tag::identifier(key);
|
||||||
@@ -237,7 +273,7 @@ pub async fn get_lume_store(key: String, state: State<'_, Nostr>) -> Result<Stri
|
|||||||
{
|
{
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
if let Some(event) = get_latest_event(&events) {
|
if let Some(event) = get_latest_event(&events) {
|
||||||
match signer.nip44_decrypt(public_key, event.content()).await {
|
match signer.nip44_decrypt(&public_key, &event.content).await {
|
||||||
Ok(decrypted) => Ok(decrypted),
|
Ok(decrypted) => Ok(decrypted),
|
||||||
Err(_) => Err(event.content.to_string()),
|
Err(_) => Err(event.content.to_string()),
|
||||||
}
|
}
|
||||||
@@ -382,7 +418,7 @@ pub async fn copy_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, St
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
for event in contact_list_events.into_iter() {
|
for event in contact_list_events.into_iter() {
|
||||||
for tag in event.into_iter_tags() {
|
for tag in event.tags.into_iter() {
|
||||||
if let Some(TagStandard::PublicKey {
|
if let Some(TagStandard::PublicKey {
|
||||||
public_key,
|
public_key,
|
||||||
relay_url,
|
relay_url,
|
||||||
@@ -405,74 +441,6 @@ pub async fn copy_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, St
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_following(
|
|
||||||
state: State<'_, Nostr>,
|
|
||||||
public_key: &str,
|
|
||||||
) -> Result<Vec<String>, String> {
|
|
||||||
let client = &state.client;
|
|
||||||
let public_key = PublicKey::parse(public_key).map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let filter = Filter::new().kind(Kind::ContactList).author(public_key);
|
|
||||||
let events = match client
|
|
||||||
.get_events_of(
|
|
||||||
vec![filter],
|
|
||||||
EventSource::both(Some(Duration::from_secs(5))),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(events) => events,
|
|
||||||
Err(err) => return Err(err.to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut ret: Vec<String> = vec![];
|
|
||||||
if let Some(latest_event) = events.iter().max_by_key(|event| event.created_at()) {
|
|
||||||
ret.extend(latest_event.tags().iter().filter_map(|tag| {
|
|
||||||
if let Some(TagStandard::PublicKey {
|
|
||||||
uppercase: false, ..
|
|
||||||
}) = <nostr_sdk::Tag as Clone>::clone(tag).to_standardized()
|
|
||||||
{
|
|
||||||
tag.content().map(String::from)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_followers(
|
|
||||||
state: State<'_, Nostr>,
|
|
||||||
public_key: &str,
|
|
||||||
) -> Result<Vec<String>, String> {
|
|
||||||
let client = &state.client;
|
|
||||||
let public_key = PublicKey::parse(public_key).map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let filter = Filter::new().kind(Kind::ContactList).custom_tag(
|
|
||||||
SingleLetterTag::lowercase(Alphabet::P),
|
|
||||||
vec![public_key.to_hex()],
|
|
||||||
);
|
|
||||||
|
|
||||||
let events = match client
|
|
||||||
.get_events_of(
|
|
||||||
vec![filter],
|
|
||||||
EventSource::both(Some(Duration::from_secs(5))),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(events) => events,
|
|
||||||
Err(err) => return Err(err.to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let ret: Vec<String> = events
|
|
||||||
.into_iter()
|
|
||||||
.map(|event| event.author().to_hex())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(ret)
|
|
||||||
// TODO: get more than 500 events
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_notifications(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
pub async fn get_notifications(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||||
@@ -491,11 +459,7 @@ pub async fn get_notifications(state: State<'_, Nostr>) -> Result<Vec<String>, S
|
|||||||
])
|
])
|
||||||
.limit(200);
|
.limit(200);
|
||||||
|
|
||||||
match client
|
match client.database().query(vec![filter]).await {
|
||||||
.database()
|
|
||||||
.query(vec![filter], Order::default())
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
|
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
}
|
}
|
||||||
@@ -522,7 +486,7 @@ pub async fn set_settings(
|
|||||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
||||||
let encrypted = signer
|
let encrypted = signer
|
||||||
.nip44_encrypt(public_key, settings)
|
.nip44_encrypt(&public_key, settings)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let tag = Tag::identifier(ident);
|
let tag = Tag::identifier(ident);
|
||||||
@@ -555,3 +519,13 @@ pub async fn verify_nip05(id: String, nip05: &str) -> Result<bool, String> {
|
|||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn is_trusted_user(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||||
|
let circles = &state.circles.lock().await;
|
||||||
|
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
|
||||||
|
let trusted = circles.values().any(|v| v.contains(&public_key));
|
||||||
|
|
||||||
|
Ok(trusted)
|
||||||
|
}
|
||||||
|
|||||||
+8
-37
@@ -13,7 +13,6 @@ use crate::Settings;
|
|||||||
pub struct Meta {
|
pub struct Meta {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub images: Vec<String>,
|
pub images: Vec<String>,
|
||||||
pub videos: Vec<String>,
|
|
||||||
pub events: Vec<String>,
|
pub events: Vec<String>,
|
||||||
pub mentions: Vec<String>,
|
pub mentions: Vec<String>,
|
||||||
pub hashtags: Vec<String>,
|
pub hashtags: Vec<String>,
|
||||||
@@ -44,7 +43,6 @@ const NOSTR_MENTIONS: [&str; 10] = [
|
|||||||
"Nostr:naddr1",
|
"Nostr:naddr1",
|
||||||
];
|
];
|
||||||
const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
||||||
const VIDEOS: [&str; 5] = ["mp4", "mov", "avi", "webm", "mkv"];
|
|
||||||
|
|
||||||
pub fn get_latest_event(events: &[Event]) -> Option<&Event> {
|
pub fn get_latest_event(events: &[Event]) -> Option<&Event> {
|
||||||
events.iter().next()
|
events.iter().next()
|
||||||
@@ -115,7 +113,6 @@ pub async fn parse_event(content: &str) -> Meta {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut images = Vec::new();
|
let mut images = Vec::new();
|
||||||
let mut videos = Vec::new();
|
|
||||||
let mut text = content.to_string();
|
let mut text = content.to_string();
|
||||||
|
|
||||||
if !urls.is_empty() {
|
if !urls.is_empty() {
|
||||||
@@ -135,12 +132,6 @@ pub async fn parse_event(content: &str) -> Meta {
|
|||||||
// Process the next item.
|
// Process the next item.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if VIDEOS.contains(&ext) {
|
|
||||||
text = text.replace(url_str, "");
|
|
||||||
videos.push(url_str.to_string());
|
|
||||||
// Process the next item.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the content type of URL via HEAD request
|
// Check the content type of URL via HEAD request
|
||||||
@@ -167,7 +158,6 @@ pub async fn parse_event(content: &str) -> Meta {
|
|||||||
mentions,
|
mentions,
|
||||||
hashtags,
|
hashtags,
|
||||||
images,
|
images,
|
||||||
videos,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,31 +242,17 @@ pub fn create_event_tags(content: &str) -> Vec<Tag> {
|
|||||||
tags
|
tags
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn init_nip65(client: &Client) {
|
pub async fn init_nip65(client: &Client, public_key: &str) {
|
||||||
let signer = match client.signer().await {
|
let author = PublicKey::from_str(public_key).unwrap();
|
||||||
Ok(signer) => signer,
|
let filter = Filter::new().author(author).kind(Kind::RelayList).limit(1);
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to get signer: {:?}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let public_key = match signer.public_key().await {
|
|
||||||
Ok(public_key) => public_key,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to get public key: {:?}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
// client.add_relay("ws://127.0.0.1:1984").await.unwrap();
|
||||||
.author(public_key)
|
// client.connect_relay("ws://127.0.0.1:1984").await.unwrap();
|
||||||
.kind(Kind::RelayList)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Ok(events) = client
|
if let Ok(events) = client
|
||||||
.get_events_of(
|
.get_events_of(
|
||||||
vec![filter],
|
vec![filter],
|
||||||
EventSource::both(Some(Duration::from_secs(5))),
|
EventSource::relays(Some(Duration::from_secs(5))),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -288,7 +264,7 @@ pub async fn init_nip65(client: &Client) {
|
|||||||
Some(_) => RelayOptions::new().write(true).read(false),
|
Some(_) => RelayOptions::new().write(true).read(false),
|
||||||
None => RelayOptions::default(),
|
None => RelayOptions::default(),
|
||||||
};
|
};
|
||||||
if let Err(e) = client.add_relay_with_opts(&url.to_string(), opts).await {
|
if let Err(e) = client.pool().add_relay(&url.to_string(), opts).await {
|
||||||
eprintln!("Failed to add relay {}: {:?}", url, e);
|
eprintln!("Failed to add relay {}: {:?}", url, e);
|
||||||
}
|
}
|
||||||
if let Err(e) = client.connect_relay(url.to_string()).await {
|
if let Err(e) = client.connect_relay(url.to_string()).await {
|
||||||
@@ -298,8 +274,6 @@ pub async fn init_nip65(client: &Client) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
eprintln!("Failed to get events for RelayList.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,8 +303,7 @@ pub async fn get_user_settings(client: &Client) -> Result<Settings, String> {
|
|||||||
{
|
{
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
if let Some(event) = events.first() {
|
if let Some(event) = events.first() {
|
||||||
let content = event.content();
|
match signer.nip44_decrypt(&public_key, &event.content).await {
|
||||||
match signer.nip44_decrypt(public_key, content).await {
|
|
||||||
Ok(decrypted) => match serde_json::from_str(&decrypted) {
|
Ok(decrypted) => match serde_json::from_str(&decrypted) {
|
||||||
Ok(parsed) => Ok(parsed),
|
Ok(parsed) => Ok(parsed),
|
||||||
Err(_) => Err("Could not parse settings payload".into()),
|
Err(_) => Err("Could not parse settings payload".into()),
|
||||||
@@ -359,7 +332,6 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(meta.content, "Check this image: #cool @npub1");
|
assert_eq!(meta.content, "Check this image: #cool @npub1");
|
||||||
assert_eq!(meta.images, vec!["https://example.com/image.jpg"]);
|
assert_eq!(meta.images, vec!["https://example.com/image.jpg"]);
|
||||||
assert_eq!(meta.videos, Vec::<String>::new());
|
|
||||||
assert_eq!(meta.hashtags, vec!["#cool"]);
|
assert_eq!(meta.hashtags, vec!["#cool"]);
|
||||||
assert_eq!(meta.mentions, vec!["@npub1"]);
|
assert_eq!(meta.mentions, vec!["@npub1"]);
|
||||||
}
|
}
|
||||||
@@ -371,7 +343,6 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(meta.content, "Check this video: #cool @npub1");
|
assert_eq!(meta.content, "Check this video: #cool @npub1");
|
||||||
assert_eq!(meta.images, Vec::<String>::new());
|
assert_eq!(meta.images, Vec::<String>::new());
|
||||||
assert_eq!(meta.videos, vec!["https://example.com/video.mp4"]);
|
|
||||||
assert_eq!(meta.hashtags, vec!["#cool"]);
|
assert_eq!(meta.hashtags, vec!["#cool"]);
|
||||||
assert_eq!(meta.mentions, vec!["@npub1"]);
|
assert_eq!(meta.mentions, vec!["@npub1"]);
|
||||||
}
|
}
|
||||||
|
|||||||
+189
-55
@@ -7,11 +7,13 @@
|
|||||||
use border::WebviewWindowExt as BorderWebviewWindowExt;
|
use border::WebviewWindowExt as BorderWebviewWindowExt;
|
||||||
use commands::{account::*, event::*, metadata::*, relay::*, window::*};
|
use commands::{account::*, event::*, metadata::*, relay::*, window::*};
|
||||||
use common::parse_event;
|
use common::parse_event;
|
||||||
|
use nostr_relay_builder::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
use specta_typescript::Typescript;
|
use specta_typescript::Typescript;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
fs,
|
fs,
|
||||||
io::{self, BufRead},
|
io::{self, BufRead},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
@@ -28,8 +30,9 @@ pub mod common;
|
|||||||
|
|
||||||
pub struct Nostr {
|
pub struct Nostr {
|
||||||
client: Client,
|
client: Client,
|
||||||
contact_list: Mutex<Vec<Contact>>,
|
|
||||||
settings: Mutex<Settings>,
|
settings: Mutex<Settings>,
|
||||||
|
contact_list: Mutex<Vec<Contact>>,
|
||||||
|
circles: Mutex<HashMap<PublicKey, Vec<PublicKey>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||||
@@ -38,6 +41,7 @@ pub struct Settings {
|
|||||||
image_resize_service: Option<String>,
|
image_resize_service: Option<String>,
|
||||||
use_relay_hint: bool,
|
use_relay_hint: bool,
|
||||||
content_warning: bool,
|
content_warning: bool,
|
||||||
|
trusted_only: bool,
|
||||||
display_avatar: bool,
|
display_avatar: bool,
|
||||||
display_zap_button: bool,
|
display_zap_button: bool,
|
||||||
display_repost_button: bool,
|
display_repost_button: bool,
|
||||||
@@ -52,6 +56,7 @@ impl Default for Settings {
|
|||||||
image_resize_service: Some("https://wsrv.nl".to_string()),
|
image_resize_service: Some("https://wsrv.nl".to_string()),
|
||||||
use_relay_hint: true,
|
use_relay_hint: true,
|
||||||
content_warning: true,
|
content_warning: true,
|
||||||
|
trusted_only: false,
|
||||||
display_avatar: true,
|
display_avatar: true,
|
||||||
display_zap_button: true,
|
display_zap_button: true,
|
||||||
display_repost_button: true,
|
display_repost_button: true,
|
||||||
@@ -77,12 +82,16 @@ struct Subscription {
|
|||||||
#[derive(Serialize, Deserialize, Type, Clone, TauriEvent)]
|
#[derive(Serialize, Deserialize, Type, Clone, TauriEvent)]
|
||||||
struct NewSettings(Settings);
|
struct NewSettings(Settings);
|
||||||
|
|
||||||
pub const FETCH_LIMIT: usize = 44;
|
pub const DEFAULT_DIFFICULTY: u8 = 21;
|
||||||
pub const NEWSFEED_NEG_LIMIT: usize = 256;
|
pub const FETCH_LIMIT: usize = 100;
|
||||||
|
pub const NEWSFEED_NEG_LIMIT: usize = 512;
|
||||||
pub const NOTIFICATION_NEG_LIMIT: usize = 64;
|
pub const NOTIFICATION_NEG_LIMIT: usize = 64;
|
||||||
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
|
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let builder = Builder::<tauri::Wry>::new()
|
let builder = Builder::<tauri::Wry>::new()
|
||||||
// Then register them (separated by a comma)
|
// Then register them (separated by a comma)
|
||||||
.commands(collect_commands![
|
.commands(collect_commands![
|
||||||
@@ -98,14 +107,15 @@ fn main() {
|
|||||||
get_private_key,
|
get_private_key,
|
||||||
delete_account,
|
delete_account,
|
||||||
reset_password,
|
reset_password,
|
||||||
|
is_account_sync,
|
||||||
login,
|
login,
|
||||||
get_profile,
|
get_profile,
|
||||||
set_profile,
|
set_profile,
|
||||||
get_contact_list,
|
get_contact_list,
|
||||||
set_contact_list,
|
set_contact_list,
|
||||||
is_contact_list_empty,
|
|
||||||
check_contact,
|
check_contact,
|
||||||
toggle_contact,
|
toggle_contact,
|
||||||
|
get_mention_list,
|
||||||
get_lume_store,
|
get_lume_store,
|
||||||
set_lume_store,
|
set_lume_store,
|
||||||
set_wallet,
|
set_wallet,
|
||||||
@@ -118,13 +128,14 @@ fn main() {
|
|||||||
get_settings,
|
get_settings,
|
||||||
set_settings,
|
set_settings,
|
||||||
verify_nip05,
|
verify_nip05,
|
||||||
|
is_trusted_user,
|
||||||
get_event_meta,
|
get_event_meta,
|
||||||
get_event,
|
get_event,
|
||||||
get_event_from,
|
get_event_from,
|
||||||
get_replies,
|
get_replies,
|
||||||
subscribe_to,
|
subscribe_to,
|
||||||
get_events_by,
|
get_events_by,
|
||||||
get_events_from_contacts,
|
get_local_events,
|
||||||
get_group_events,
|
get_group_events,
|
||||||
get_global_events,
|
get_global_events,
|
||||||
get_hashtag_events,
|
get_hashtag_events,
|
||||||
@@ -156,8 +167,6 @@ fn main() {
|
|||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
let tauri_builder = tauri::Builder::default();
|
let tauri_builder = tauri::Builder::default();
|
||||||
|
|
||||||
let mut ctx = tauri::generate_context!();
|
|
||||||
|
|
||||||
tauri_builder
|
tauri_builder
|
||||||
.invoke_handler(builder.invoke_handler())
|
.invoke_handler(builder.invoke_handler())
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
@@ -168,6 +177,19 @@ fn main() {
|
|||||||
let handle_clone_child = handle_clone.clone();
|
let handle_clone_child = handle_clone.clone();
|
||||||
let main_window = app.get_webview_window("main").unwrap();
|
let main_window = app.get_webview_window("main").unwrap();
|
||||||
|
|
||||||
|
let config_dir = handle
|
||||||
|
.path()
|
||||||
|
.app_config_dir()
|
||||||
|
.expect("Error: app config directory not found.");
|
||||||
|
|
||||||
|
let data_dir = handle
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.expect("Error: app data directory not found.");
|
||||||
|
|
||||||
|
let _ = fs::create_dir_all(&config_dir);
|
||||||
|
let _ = fs::create_dir_all(&data_dir);
|
||||||
|
|
||||||
// Set custom decoration for Windows
|
// Set custom decoration for Windows
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
main_window.create_overlay_titlebar().unwrap();
|
main_window.create_overlay_titlebar().unwrap();
|
||||||
@@ -191,23 +213,17 @@ fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let client = tauri::async_runtime::block_on(async move {
|
let client = tauri::async_runtime::block_on(async move {
|
||||||
// Create data folder if not exist
|
|
||||||
let dir = handle
|
|
||||||
.path()
|
|
||||||
.app_config_dir()
|
|
||||||
.expect("App config directory not found.");
|
|
||||||
let _ = fs::create_dir_all(dir.clone());
|
|
||||||
|
|
||||||
// Setup database
|
// Setup database
|
||||||
let database = SQLiteDatabase::open(dir.join("nostr.db"))
|
let database = NostrLMDB::open(config_dir.join("nostr-lmdb"))
|
||||||
.await
|
.expect("Error: cannot create database.");
|
||||||
.expect("Database error.");
|
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
let opts = Options::new()
|
let opts = Options::new()
|
||||||
|
.gossip(true)
|
||||||
.max_avg_latency(Duration::from_millis(500))
|
.max_avg_latency(Duration::from_millis(500))
|
||||||
.automatic_authentication(true)
|
.automatic_authentication(false)
|
||||||
.connection_timeout(Some(Duration::from_secs(5)))
|
.connection_timeout(Some(Duration::from_secs(20)))
|
||||||
|
.send_timeout(Some(Duration::from_secs(10)))
|
||||||
.timeout(Duration::from_secs(20));
|
.timeout(Duration::from_secs(20));
|
||||||
|
|
||||||
// Setup nostr client
|
// Setup nostr client
|
||||||
@@ -234,7 +250,7 @@ fn main() {
|
|||||||
} else {
|
} else {
|
||||||
RelayOptions::new().write(true).read(false)
|
RelayOptions::new().write(true).read(false)
|
||||||
};
|
};
|
||||||
let _ = client.add_relay_with_opts(relay, opts).await;
|
let _ = client.pool().add_relay(relay, opts).await;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let _ = client.add_relay(relay).await;
|
let _ = client.add_relay(relay).await;
|
||||||
@@ -244,6 +260,14 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Err(e) = client.add_discovery_relay("wss://purplepag.es/").await {
|
||||||
|
println!("Add discovery relay failed: {}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = client.add_discovery_relay("wss://directory.yabu.me/").await {
|
||||||
|
println!("Add discovery relay failed: {}", e)
|
||||||
|
}
|
||||||
|
|
||||||
// Connect
|
// Connect
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
|
|
||||||
@@ -253,8 +277,9 @@ fn main() {
|
|||||||
// Create global state
|
// Create global state
|
||||||
app.manage(Nostr {
|
app.manage(Nostr {
|
||||||
client,
|
client,
|
||||||
contact_list: Mutex::new(vec![]),
|
|
||||||
settings: Mutex::new(Settings::default()),
|
settings: Mutex::new(Settings::default()),
|
||||||
|
contact_list: Mutex::new(Vec::new()),
|
||||||
|
circles: Mutex::new(HashMap::new()),
|
||||||
});
|
});
|
||||||
|
|
||||||
Subscription::listen_any(app, move |event| {
|
Subscription::listen_any(app, move |event| {
|
||||||
@@ -269,27 +294,43 @@ fn main() {
|
|||||||
SubKind::Subscribe => {
|
SubKind::Subscribe => {
|
||||||
let subscription_id = SubscriptionId::new(payload.label);
|
let subscription_id = SubscriptionId::new(payload.label);
|
||||||
|
|
||||||
let filter = if let Some(id) = payload.event_id {
|
match payload.event_id {
|
||||||
let event_id = EventId::from_str(&id).unwrap();
|
Some(id) => {
|
||||||
|
let event_id = EventId::from_str(&id).unwrap();
|
||||||
|
let filter =
|
||||||
|
Filter::new().event(event_id).since(Timestamp::now());
|
||||||
|
|
||||||
Filter::new().event(event_id).since(Timestamp::now())
|
if let Err(e) = client
|
||||||
} else {
|
.subscribe_with_id(subscription_id, vec![filter], None)
|
||||||
let contact_list = state.contact_list.lock().await;
|
.await
|
||||||
let authors: Vec<PublicKey> =
|
{
|
||||||
contact_list.iter().map(|f| f.public_key).collect();
|
println!("Subscription error: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let contact_list = client
|
||||||
|
.get_contact_list(Some(Duration::from_secs(5)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
Filter::new()
|
if !contact_list.is_empty() {
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
let authors: Vec<PublicKey> =
|
||||||
.authors(authors)
|
contact_list.iter().map(|f| f.public_key).collect();
|
||||||
.since(Timestamp::now())
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
|
.authors(authors)
|
||||||
|
.since(Timestamp::now());
|
||||||
|
|
||||||
|
if let Err(e) = client
|
||||||
|
.subscribe_with_id(subscription_id, vec![filter], None)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
println!("Subscription error: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = client
|
|
||||||
.subscribe_with_id(subscription_id, vec![filter], None)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
println!("Subscription error: {}", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
SubKind::Unsubscribe => {
|
SubKind::Unsubscribe => {
|
||||||
let subscription_id = SubscriptionId::new(payload.label);
|
let subscription_id = SubscriptionId::new(payload.label);
|
||||||
@@ -299,6 +340,22 @@ fn main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Run local relay thread
|
||||||
|
//tauri::async_runtime::spawn(async move {
|
||||||
|
// let database = NostrLMDB::open(data_dir.join("local-relay"))
|
||||||
|
// .expect("Error: cannot create database.");
|
||||||
|
// let builder = RelayBuilder::default().database(database).port(1984);
|
||||||
|
//
|
||||||
|
// if let Ok(relay) = LocalRelay::run(builder).await {
|
||||||
|
// println!("Running local relay: {}", relay.url())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// loop {
|
||||||
|
// tokio::time::sleep(Duration::from_secs(60)).await;
|
||||||
|
// }
|
||||||
|
//});
|
||||||
|
|
||||||
|
// Run notification thread
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let state = handle_clone.state::<Nostr>();
|
let state = handle_clone.state::<Nostr>();
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
@@ -315,11 +372,52 @@ fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
||||||
|
let mut notifications = client.pool().notifications();
|
||||||
|
|
||||||
client
|
while let Ok(notification) = notifications.recv().await {
|
||||||
.handle_notifications(|notification| async {
|
match notification {
|
||||||
if let RelayPoolNotification::Message { message, .. } = notification {
|
RelayPoolNotification::Message { relay_url, message } => {
|
||||||
if let RelayMessage::Event {
|
if let RelayMessage::Auth { challenge } = message {
|
||||||
|
match client.auth(challenge, relay_url.clone()).await {
|
||||||
|
Ok(..) => {
|
||||||
|
if let Ok(relay) = client.relay(&relay_url).await {
|
||||||
|
let msg =
|
||||||
|
format!("Authenticated to {} relay.", relay_url);
|
||||||
|
let opts = RelaySendOptions::new()
|
||||||
|
.skip_send_confirmation(true);
|
||||||
|
|
||||||
|
if let Err(e) = relay.resubscribe(opts).await {
|
||||||
|
println!("Error: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if allow_notification {
|
||||||
|
if let Err(e) = &handle_clone
|
||||||
|
.notification()
|
||||||
|
.builder()
|
||||||
|
.body(&msg)
|
||||||
|
.title("Lume")
|
||||||
|
.show()
|
||||||
|
{
|
||||||
|
println!("Error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if allow_notification {
|
||||||
|
if let Err(e) = &handle_clone
|
||||||
|
.notification()
|
||||||
|
.builder()
|
||||||
|
.body(e.to_string())
|
||||||
|
.title("Lume")
|
||||||
|
.show()
|
||||||
|
{
|
||||||
|
println!("Error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let RelayMessage::Event {
|
||||||
subscription_id,
|
subscription_id,
|
||||||
event,
|
event,
|
||||||
} = message
|
} = message
|
||||||
@@ -329,11 +427,14 @@ fn main() {
|
|||||||
// Send native notification
|
// Send native notification
|
||||||
if allow_notification {
|
if allow_notification {
|
||||||
let author = client
|
let author = client
|
||||||
.metadata(event.pubkey)
|
.fetch_metadata(
|
||||||
|
event.pubkey,
|
||||||
|
Some(Duration::from_secs(3)),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|_| Metadata::new());
|
.unwrap_or_else(|_| Metadata::new());
|
||||||
|
|
||||||
send_notification(&event, author, &handle_clone);
|
send_event_notification(&event, author, &handle_clone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,19 +453,52 @@ fn main() {
|
|||||||
RichEvent { raw, parsed },
|
RichEvent { raw, parsed },
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
} else {
|
};
|
||||||
println!("new message: {}", message.as_json())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(false)
|
RelayPoolNotification::Shutdown => break,
|
||||||
})
|
_ => (),
|
||||||
.await
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
.on_window_event(|window, event| {
|
||||||
|
if let tauri::WindowEvent::Focused(focused) = event {
|
||||||
|
if !focused {
|
||||||
|
let handle = window.app_handle().to_owned();
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let state = handle.state::<Nostr>();
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
|
if client.signer().await.is_ok() {
|
||||||
|
if let Ok(contact_list) =
|
||||||
|
client.get_contact_list(Some(Duration::from_secs(5))).await
|
||||||
|
{
|
||||||
|
let authors: Vec<PublicKey> =
|
||||||
|
contact_list.iter().map(|f| f.public_key).collect();
|
||||||
|
|
||||||
|
if client
|
||||||
|
.reconcile(
|
||||||
|
Filter::new()
|
||||||
|
.authors(authors)
|
||||||
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
|
.limit(NEWSFEED_NEG_LIMIT),
|
||||||
|
NegentropyOptions::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
handle.emit("synchronized", ()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.plugin(prevent_default())
|
.plugin(prevent_default())
|
||||||
.plugin(tauri_plugin_theme::init(ctx.config_mut()))
|
|
||||||
.plugin(tauri_plugin_decorum::init())
|
.plugin(tauri_plugin_decorum::init())
|
||||||
.plugin(tauri_plugin_store::Builder::default().build())
|
.plugin(tauri_plugin_store::Builder::default().build())
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
@@ -377,7 +511,7 @@ fn main() {
|
|||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_upload::init())
|
.plugin(tauri_plugin_upload::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.run(ctx)
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,8 +529,8 @@ fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
|||||||
tauri_plugin_prevent_default::Builder::new().build()
|
tauri_plugin_prevent_default::Builder::new().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_notification(event: &Event, author: Metadata, handle: &tauri::AppHandle) {
|
fn send_event_notification(event: &Event, author: Metadata, handle: &tauri::AppHandle) {
|
||||||
match event.kind() {
|
match event.kind {
|
||||||
Kind::TextNote => {
|
Kind::TextNote => {
|
||||||
if let Err(e) = handle
|
if let Err(e) = handle
|
||||||
.notification()
|
.notification()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"productName": "Lume",
|
"productName": "Lume",
|
||||||
"version": "4.1.0",
|
"version": "4.2.1",
|
||||||
"identifier": "nu.lume.Lume",
|
"identifier": "nu.lume.Lume",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
+11
-11
@@ -1,27 +1,27 @@
|
|||||||
import {
|
import { experimental_createPersister } from "@tanstack/query-persist-client-core";
|
||||||
type PersistedQuery,
|
|
||||||
experimental_createPersister,
|
|
||||||
} from "@tanstack/query-persist-client-core";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||||
import { type } from "@tauri-apps/plugin-os";
|
import { type } from "@tauri-apps/plugin-os";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { newQueryStorage } from "./commons";
|
import { newQueryStorage } from "./commons";
|
||||||
import { routeTree } from "./routes.gen"; // auto generated file
|
|
||||||
import type { LumeEvent } from "./system";
|
import type { LumeEvent } from "./system";
|
||||||
import "./app.css";
|
|
||||||
import { Store } from "@tauri-apps/plugin-store";
|
import "./app.css"; // global styles
|
||||||
|
|
||||||
|
import { createStore } from "@tauri-apps/plugin-store";
|
||||||
|
import { routeTree } from "./routes.gen"; // auto generated file
|
||||||
|
|
||||||
const platform = type();
|
const platform = type();
|
||||||
const tauriStore = new Store(".lume.dat");
|
// @ts-ignore, https://github.com/tauri-apps/plugins-workspace/pull/1860
|
||||||
|
const store = await createStore(".cache", { autoSave: 100 });
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
gcTime: 1000 * 30,
|
gcTime: 1000 * 30,
|
||||||
persister: experimental_createPersister<PersistedQuery>({
|
persister: experimental_createPersister({
|
||||||
storage: newQueryStorage(tauriStore),
|
storage: newQueryStorage(store),
|
||||||
maxAge: 1000 * 60 * 60 * 24, // 24 hours,
|
maxAge: 1000 * 60 * 60 * 12,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+28
-16
@@ -56,7 +56,7 @@ async createAccount(name: string, about: string, picture: string, password: stri
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async importAccount(key: string, password: string | null) : Promise<Result<string, string>> {
|
async importAccount(key: string, password: string) : Promise<Result<string, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("import_account", { key, password }) };
|
return { status: "ok", data: await TAURI_INVOKE("import_account", { key, password }) };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -96,6 +96,9 @@ async resetPassword(key: string, password: string) : Promise<Result<null, string
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async isAccountSync(id: string) : Promise<boolean> {
|
||||||
|
return await TAURI_INVOKE("is_account_sync", { id });
|
||||||
|
},
|
||||||
async login(account: string, password: string) : Promise<Result<string, string>> {
|
async login(account: string, password: string) : Promise<Result<string, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("login", { account, password }) };
|
return { status: "ok", data: await TAURI_INVOKE("login", { account, password }) };
|
||||||
@@ -136,17 +139,9 @@ async setContactList(publicKeys: string[]) : Promise<Result<boolean, string>> {
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async isContactListEmpty() : Promise<Result<boolean, null>> {
|
async checkContact(id: string) : Promise<Result<boolean, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("is_contact_list_empty") };
|
return { status: "ok", data: await TAURI_INVOKE("check_contact", { id }) };
|
||||||
} catch (e) {
|
|
||||||
if(e instanceof Error) throw e;
|
|
||||||
else return { status: "error", error: e as any };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async checkContact(hex: string) : Promise<Result<boolean, string>> {
|
|
||||||
try {
|
|
||||||
return { status: "ok", data: await TAURI_INVOKE("check_contact", { hex }) };
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if(e instanceof Error) throw e;
|
if(e instanceof Error) throw e;
|
||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
@@ -160,6 +155,14 @@ async toggleContact(id: string, alias: string | null) : Promise<Result<string, s
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async getMentionList() : Promise<Result<Mention[], string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_mention_list") };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
async getLumeStore(key: string) : Promise<Result<string, string>> {
|
async getLumeStore(key: string) : Promise<Result<string, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_lume_store", { key }) };
|
return { status: "ok", data: await TAURI_INVOKE("get_lume_store", { key }) };
|
||||||
@@ -256,6 +259,14 @@ async verifyNip05(id: string, nip05: string) : Promise<Result<boolean, string>>
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async isTrustedUser(id: string) : Promise<Result<boolean, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("is_trusted_user", { id }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
async getEventMeta(content: string) : Promise<Result<Meta, null>> {
|
async getEventMeta(content: string) : Promise<Result<Meta, null>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_event_meta", { content }) };
|
return { status: "ok", data: await TAURI_INVOKE("get_event_meta", { content }) };
|
||||||
@@ -304,9 +315,9 @@ async getEventsBy(publicKey: string, limit: number) : Promise<Result<RichEvent[]
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getEventsFromContacts(until: string | null) : Promise<Result<RichEvent[], string>> {
|
async getLocalEvents(until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_events_from_contacts", { until }) };
|
return { status: "ok", data: await TAURI_INVOKE("get_local_events", { until }) };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if(e instanceof Error) throw e;
|
if(e instanceof Error) throw e;
|
||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
@@ -458,12 +469,13 @@ subscription: "subscription"
|
|||||||
/** user-defined types **/
|
/** user-defined types **/
|
||||||
|
|
||||||
export type Column = { label: string; url: string; x: number; y: number; width: number; height: number }
|
export type Column = { label: string; url: string; x: number; y: number; width: number; height: number }
|
||||||
export type Meta = { content: string; images: string[]; videos: string[]; events: string[]; mentions: string[]; hashtags: string[] }
|
export type Mention = { pubkey: string; avatar: string; display_name: string; name: string }
|
||||||
|
export type Meta = { content: string; images: string[]; events: string[]; mentions: string[]; hashtags: string[] }
|
||||||
export type NewSettings = Settings
|
export type NewSettings = Settings
|
||||||
export type Profile = { name: string; display_name: string; about: string | null; picture: string; banner: string | null; nip05: string | null; lud16: string | null; website: string | null }
|
export type Profile = { name: string; display_name: string; about: string | null; picture: string; banner: string | null; nip05: string | null; lud16: string | null; website: string | null }
|
||||||
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
|
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
|
||||||
export type RichEvent = { raw: string; parsed: Meta | null }
|
export type RichEvent = { raw: string; parsed: Meta | null }
|
||||||
export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean; transparent: boolean }
|
export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; trusted_only: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean; transparent: boolean }
|
||||||
export type SubKind = "Subscribe" | "Unsubscribe"
|
export type SubKind = "Subscribe" | "Unsubscribe"
|
||||||
export type Subscription = { label: string; kind: SubKind; event_id: string | null }
|
export type Subscription = { label: string; kind: SubKind; event_id: string | null }
|
||||||
export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean }
|
export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean }
|
||||||
@@ -484,7 +496,7 @@ type __EventObj__<T> = {
|
|||||||
once: (
|
once: (
|
||||||
cb: TAURI_API_EVENT.EventCallback<T>,
|
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||||
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
|
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
|
||||||
emit: T extends null
|
emit: null extends T
|
||||||
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
|
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
|
||||||
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
|
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-59
@@ -15,8 +15,6 @@ import dayjs from "dayjs";
|
|||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import updateLocale from "dayjs/plugin/updateLocale";
|
import updateLocale from "dayjs/plugin/updateLocale";
|
||||||
import { decode } from "light-bolt11-decoder";
|
import { decode } from "light-bolt11-decoder";
|
||||||
import { type BaseEditor, Transforms } from "slate";
|
|
||||||
import { ReactEditor } from "slate-react";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import type { RichEvent, Settings } from "./commands.gen";
|
import type { RichEvent, Settings } from "./commands.gen";
|
||||||
import { LumeEvent } from "./system";
|
import { LumeEvent } from "./system";
|
||||||
@@ -59,51 +57,6 @@ export const isImageUrl = (url: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertImage = (editor: ReactEditor | BaseEditor, url: string) => {
|
|
||||||
const text = { text: "" };
|
|
||||||
const image = [
|
|
||||||
{
|
|
||||||
type: "image",
|
|
||||||
url,
|
|
||||||
children: [text],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const extraText = [
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
children: [text],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// @ts-ignore, idk
|
|
||||||
ReactEditor.focus(editor);
|
|
||||||
Transforms.insertNodes(editor, image);
|
|
||||||
Transforms.insertNodes(editor, extraText);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const insertNostrEvent = (
|
|
||||||
editor: ReactEditor | BaseEditor,
|
|
||||||
eventId: string,
|
|
||||||
) => {
|
|
||||||
const text = { text: "" };
|
|
||||||
const event = [
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
eventId: `nostr:${eventId}`,
|
|
||||||
children: [text],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const extraText = [
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
children: [text],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
Transforms.insertNodes(editor, event);
|
|
||||||
Transforms.insertNodes(editor, extraText);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function formatCreatedAt(time: number, message = false) {
|
export function formatCreatedAt(time: number, message = false) {
|
||||||
let formated: string;
|
let formated: string;
|
||||||
|
|
||||||
@@ -257,18 +210,16 @@ export async function upload(filePath?: string) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const selected =
|
const selected =
|
||||||
filePath ||
|
filePath ??
|
||||||
(
|
(await open({
|
||||||
await open({
|
multiple: false,
|
||||||
multiple: false,
|
filters: [
|
||||||
filters: [
|
{
|
||||||
{
|
name: "Media",
|
||||||
name: "Media",
|
extensions: allowExts,
|
||||||
extensions: allowExts,
|
},
|
||||||
},
|
],
|
||||||
],
|
}));
|
||||||
})
|
|
||||||
).path;
|
|
||||||
|
|
||||||
// User cancelled action
|
// User cancelled action
|
||||||
if (!selected) return null;
|
if (!selected) return null;
|
||||||
@@ -331,6 +282,7 @@ export const appSettings = new Store<Settings>({
|
|||||||
image_resize_service: "https://wsrv.nl",
|
image_resize_service: "https://wsrv.nl",
|
||||||
use_relay_hint: true,
|
use_relay_hint: true,
|
||||||
content_warning: true,
|
content_warning: true,
|
||||||
|
trusted_only: true,
|
||||||
display_avatar: true,
|
display_avatar: true,
|
||||||
display_zap_button: true,
|
display_zap_button: true,
|
||||||
display_repost_button: true,
|
display_repost_button: true,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const Column = memo(function Column({ column }: { column: LumeColumn }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-[440px] shrink-0 p-2">
|
<div className="h-full w-[440px] shrink-0 p-2">
|
||||||
<div className="flex flex-col w-full h-full rounded-xl bg-black/5 dark:bg-white/15">
|
<div className="flex flex-col w-full h-full rounded-xl bg-black/5 dark:bg-white/20">
|
||||||
<Header label={column.label} name={column.name} />
|
<Header label={column.label} name={column.name} />
|
||||||
<div ref={container} className="flex-1 w-full h-full">
|
<div ref={container} className="flex-1 w-full h-full">
|
||||||
{!isCreated ? (
|
{!isCreated ? (
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { SVGProps } from "react";
|
||||||
|
|
||||||
|
export const ReplyIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21.75 12c0-5.156-3.792-8.25-9.75-8.25S2.25 6.844 2.25 12c0 1.337.92 3.605 1.064 3.952l.038.091c.099.27.505 1.71-1.102 3.84 2.167 1.031 4.468-.664 4.468-.664 1.592.84 3.486 1.031 5.282 1.031 5.958 0 9.75-3.094 9.75-8.25z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { SVGProps } from "react";
|
||||||
|
|
||||||
|
export const RepostIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.75 1.5l3.5 3.25L10.75 8m2.5 8l-3.5 3.25 3.5 3.25m-2.5-3.25H14a7.25 7.25 0 004.755-12.723M13.25 4.75H10a7.25 7.25 0 00-4.754 12.724"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { SVGProps } from "react";
|
||||||
|
|
||||||
|
export const ZapIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3.861 11.252l4.6-8a1 1 0 01.867-.502h7.053a1 1 0 01.895 1.447l-1.303 2.606a1 1 0 00.895 1.447h2.467c.891 0 1.337 1.077.707 1.707L9.103 20.897c-.701.7-1.885.063-1.687-.908l1.235-6.039a1 1 0 00-.98-1.2H4.728a1 1 0 01-.867-1.498z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@@ -13,3 +13,8 @@ export * from "./reply";
|
|||||||
// Global components
|
// Global components
|
||||||
export * from "./note";
|
export * from "./note";
|
||||||
export * from "./user";
|
export * from "./user";
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
export * from "./icons/reply";
|
||||||
|
export * from "./icons/repost";
|
||||||
|
export * from "./icons/zap";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LumeWindow } from "@/system";
|
import { LumeWindow } from "@/system";
|
||||||
import { FrameCorners } from "@phosphor-icons/react";
|
import { ListPlus } from "@phosphor-icons/react";
|
||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
import { useNoteContext } from "../provider";
|
import { useNoteContext } from "../provider";
|
||||||
|
|
||||||
@@ -15,12 +15,12 @@ export function NoteOpenThread() {
|
|||||||
onClick={() => LumeWindow.openEvent(event)}
|
onClick={() => LumeWindow.openEvent(event)}
|
||||||
className="group inline-flex h-7 w-14 bg-neutral-100 dark:bg-white/10 rounded-full items-center justify-center text-sm font-medium text-neutral-800 dark:text-neutral-200 hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
|
className="group inline-flex h-7 w-14 bg-neutral-100 dark:bg-white/10 rounded-full items-center justify-center text-sm font-medium text-neutral-800 dark:text-neutral-200 hover:text-blue-500 hover:bg-neutral-200 dark:hover:bg-white/20"
|
||||||
>
|
>
|
||||||
<FrameCorners className="shrink-0 size-4" />
|
<ListPlus className="shrink-0 size-4" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
||||||
Open
|
View thread
|
||||||
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Portal>
|
</Tooltip.Portal>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { cn } from "@/commons";
|
import { cn } from "@/commons";
|
||||||
|
import { ReplyIcon } from "@/components";
|
||||||
import { LumeWindow } from "@/system";
|
import { LumeWindow } from "@/system";
|
||||||
import { ShareFat } from "@phosphor-icons/react";
|
|
||||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
import { useNoteContext } from "../provider";
|
import { useNoteContext } from "../provider";
|
||||||
|
|
||||||
export function NoteReply({ large = false }: { large?: boolean }) {
|
export function NoteReply({
|
||||||
|
label = false,
|
||||||
|
smol = false,
|
||||||
|
}: { label?: boolean; smol?: boolean }) {
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -16,13 +19,13 @@ export function NoteReply({ large = false }: { large?: boolean }) {
|
|||||||
onClick={() => LumeWindow.openEditor(event.id)}
|
onClick={() => LumeWindow.openEditor(event.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||||
large
|
label
|
||||||
? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
|
? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
|
||||||
: "size-7",
|
: "size-7",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ShareFat className="shrink-0 size-4" />
|
<ReplyIcon className={cn("shrink-0", smol ? "size-4" : "size-5")} />
|
||||||
{large ? "Reply" : null}
|
{label ? "Reply" : null}
|
||||||
</button>
|
</button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { appSettings, cn } from "@/commons";
|
import { appSettings, cn } from "@/commons";
|
||||||
import { Spinner } from "@/components";
|
import { RepostIcon, Spinner } from "@/components";
|
||||||
import { LumeWindow } from "@/system";
|
import { LumeWindow } from "@/system";
|
||||||
import { Repeat } from "@phosphor-icons/react";
|
|
||||||
import { useStore } from "@tanstack/react-store";
|
import { useStore } from "@tanstack/react-store";
|
||||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useNoteContext } from "../provider";
|
import { useNoteContext } from "../provider";
|
||||||
|
|
||||||
export function NoteRepost({ large = false }: { large?: boolean }) {
|
export function NoteRepost({
|
||||||
|
label = false,
|
||||||
|
smol = false,
|
||||||
|
}: { label?: boolean; smol?: boolean }) {
|
||||||
const visible = useStore(appSettings, (state) => state.display_repost_button);
|
const visible = useStore(appSettings, (state) => state.display_repost_button);
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
|
|
||||||
@@ -41,11 +43,11 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
|
|||||||
|
|
||||||
const menuItems = await Promise.all([
|
const menuItems = await Promise.all([
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Quote",
|
text: "Repost",
|
||||||
action: async () => repost(),
|
action: async () => repost(),
|
||||||
}),
|
}),
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Repost",
|
text: "Quote",
|
||||||
action: () => LumeWindow.openEditor(null, event.id),
|
action: () => LumeWindow.openEditor(null, event.id),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@@ -65,7 +67,7 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
|
|||||||
onClick={(e) => showContextMenu(e)}
|
onClick={(e) => showContextMenu(e)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||||
large
|
label
|
||||||
? "rounded-full h-7 gap-1.5 w-24 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
|
? "rounded-full h-7 gap-1.5 w-24 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
|
||||||
: "size-7",
|
: "size-7",
|
||||||
)}
|
)}
|
||||||
@@ -73,9 +75,14 @@ export function NoteRepost({ large = false }: { large?: boolean }) {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<Spinner className="size-4" />
|
<Spinner className="size-4" />
|
||||||
) : (
|
) : (
|
||||||
<Repeat className={cn("size-4", isRepost ? "text-blue-500" : "")} />
|
<RepostIcon
|
||||||
|
className={cn(
|
||||||
|
smol ? "size-4" : "size-5",
|
||||||
|
isRepost ? "text-blue-500" : "",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{large ? "Repost" : null}
|
{label ? "Repost" : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { appSettings, cn } from "@/commons";
|
import { appSettings, cn } from "@/commons";
|
||||||
|
import { ZapIcon } from "@/components";
|
||||||
import { LumeWindow } from "@/system";
|
import { LumeWindow } from "@/system";
|
||||||
import { Lightning } from "@phosphor-icons/react";
|
|
||||||
import { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
import { useStore } from "@tanstack/react-store";
|
import { useStore } from "@tanstack/react-store";
|
||||||
import { useNoteContext } from "../provider";
|
import { useNoteContext } from "../provider";
|
||||||
|
|
||||||
export function NoteZap({ large = false }: { large?: boolean }) {
|
export function NoteZap({
|
||||||
|
label = false,
|
||||||
|
smol = false,
|
||||||
|
}: { label?: boolean; smol?: boolean }) {
|
||||||
const search = useSearch({ strict: false });
|
const search = useSearch({ strict: false });
|
||||||
const visible = useStore(appSettings, (state) => state.display_zap_button);
|
const visible = useStore(appSettings, (state) => state.display_zap_button);
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
@@ -18,13 +21,13 @@ export function NoteZap({ large = false }: { large?: boolean }) {
|
|||||||
onClick={() => LumeWindow.openZap(event.id, search.account)}
|
onClick={() => LumeWindow.openZap(event.id, search.account)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
"inline-flex items-center justify-center text-neutral-800 dark:text-neutral-200",
|
||||||
large
|
label
|
||||||
? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
|
? "rounded-full h-7 gap-1.5 w-20 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10"
|
||||||
: "size-7",
|
: "size-7",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Lightning className="size-4" />
|
<ZapIcon className={smol ? "size-4" : "size-5"} />
|
||||||
{large ? "Zap" : null}
|
{label ? "Zap" : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Hashtag } from "./mentions/hashtag";
|
|||||||
import { MentionNote } from "./mentions/note";
|
import { MentionNote } from "./mentions/note";
|
||||||
import { MentionUser } from "./mentions/user";
|
import { MentionUser } from "./mentions/user";
|
||||||
import { Images } from "./preview/images";
|
import { Images } from "./preview/images";
|
||||||
import { Videos } from "./preview/videos";
|
|
||||||
import { useNoteContext } from "./provider";
|
import { useNoteContext } from "./provider";
|
||||||
|
|
||||||
export function NoteContent({
|
export function NoteContent({
|
||||||
@@ -102,14 +101,9 @@ export function NoteContent({
|
|||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
{visible ? (
|
{visible ? (
|
||||||
<>
|
event.meta?.images.length ? (
|
||||||
{event.meta?.images.length ? (
|
<Images urls={event.meta.images} />
|
||||||
<Images urls={event.meta.images} />
|
) : null
|
||||||
) : null}
|
|
||||||
{event.meta?.videos.length ? (
|
|
||||||
<Videos urls={event.meta.videos} />
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Hashtag } from "./mentions/hashtag";
|
|||||||
import { MentionNote } from "./mentions/note";
|
import { MentionNote } from "./mentions/note";
|
||||||
import { MentionUser } from "./mentions/user";
|
import { MentionUser } from "./mentions/user";
|
||||||
import { ImagePreview } from "./preview/image";
|
import { ImagePreview } from "./preview/image";
|
||||||
import { VideoPreview } from "./preview/video";
|
|
||||||
import { useNoteContext } from "./provider";
|
import { useNoteContext } from "./provider";
|
||||||
|
|
||||||
export function NoteContentLarge({
|
export function NoteContentLarge({
|
||||||
@@ -18,7 +17,7 @@ export function NoteContentLarge({
|
|||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
// Get parsed meta
|
// Get parsed meta
|
||||||
const { images, videos, hashtags, events, mentions } = event.meta;
|
const { images, hashtags, events, mentions } = event.meta;
|
||||||
|
|
||||||
// Define rich content
|
// Define rich content
|
||||||
let richContent: ReactNode[] | string = event.content;
|
let richContent: ReactNode[] | string = event.content;
|
||||||
@@ -48,12 +47,6 @@ export function NoteContentLarge({
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const video of videos) {
|
|
||||||
richContent = reactStringReplace(richContent, video, (match, i) => (
|
|
||||||
<VideoPreview key={match + i} url={match} />
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
richContent = reactStringReplace(
|
richContent = reactStringReplace(
|
||||||
richContent,
|
richContent,
|
||||||
/(https?:\/\/\S+)/gi,
|
/(https?:\/\/\S+)/gi,
|
||||||
@@ -75,8 +68,7 @@ export function NoteContentLarge({
|
|||||||
));
|
));
|
||||||
|
|
||||||
return richContent;
|
return richContent;
|
||||||
} catch (e) {
|
} catch {
|
||||||
console.log("[parser]: ", e);
|
|
||||||
return event.content;
|
return event.content;
|
||||||
}
|
}
|
||||||
}, [event.content]);
|
}, [event.content]);
|
||||||
|
|||||||
+172
-21
@@ -1,9 +1,24 @@
|
|||||||
import { cn, replyTime } from "@/commons";
|
import { commands } from "@/commands.gen";
|
||||||
|
import { appSettings, cn, replyTime } from "@/commons";
|
||||||
import { Note } from "@/components/note";
|
import { Note } from "@/components/note";
|
||||||
import type { LumeEvent } from "@/system";
|
import { type LumeEvent, LumeWindow } from "@/system";
|
||||||
import { CaretDown } from "@phosphor-icons/react";
|
import { CaretDown } from "@phosphor-icons/react";
|
||||||
import { Link, useSearch } from "@tanstack/react-router";
|
import { Link, useSearch } from "@tanstack/react-router";
|
||||||
import { memo } from "react";
|
import { useStore } from "@tanstack/react-store";
|
||||||
|
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||||
|
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
import {
|
||||||
|
type ReactNode,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import reactStringReplace from "react-string-replace";
|
||||||
|
import { Hashtag } from "./note/mentions/hashtag";
|
||||||
|
import { MentionUser } from "./note/mentions/user";
|
||||||
import { User } from "./user";
|
import { User } from "./user";
|
||||||
|
|
||||||
export const ReplyNote = memo(function ReplyNote({
|
export const ReplyNote = memo(function ReplyNote({
|
||||||
@@ -13,33 +28,81 @@ export const ReplyNote = memo(function ReplyNote({
|
|||||||
event: LumeEvent;
|
event: LumeEvent;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const trustedOnly = useStore(appSettings, (state) => state.trusted_only);
|
||||||
const search = useSearch({ strict: false });
|
const search = useSearch({ strict: false });
|
||||||
|
|
||||||
|
const [isTrusted, setIsTrusted] = useState<boolean>(null);
|
||||||
|
|
||||||
|
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const menuItems = await Promise.all([
|
||||||
|
MenuItem.new({
|
||||||
|
text: "View Profile",
|
||||||
|
action: () => LumeWindow.openProfile(event.pubkey),
|
||||||
|
}),
|
||||||
|
MenuItem.new({
|
||||||
|
text: "Copy Public Key",
|
||||||
|
action: async () => {
|
||||||
|
const pubkey = await event.pubkeyAsBech32();
|
||||||
|
await writeText(pubkey);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const menu = await Menu.new({
|
||||||
|
items: menuItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
await menu.popup().catch((e) => console.error(e));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function check() {
|
||||||
|
const res = await commands.isTrustedUser(event.pubkey);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
setIsTrusted(res.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trustedOnly) {
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isTrusted !== null && isTrusted === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
<User.Provider pubkey={event.pubkey}>
|
<User.Provider pubkey={event.pubkey}>
|
||||||
<Note.Root className={cn("flex gap-2.5", className)}>
|
<Note.Root className={cn("flex gap-2.5", className)}>
|
||||||
<User.Root className="shrink-0">
|
<User.Root className="shrink-0">
|
||||||
<User.Avatar className="size-8 rounded-full" />
|
<button type="button" onClick={(e) => showContextMenu(e)}>
|
||||||
|
<User.Avatar className="size-8 rounded-full" />
|
||||||
|
</button>
|
||||||
</User.Root>
|
</User.Root>
|
||||||
<div className="flex-1 flex flex-col gap-1">
|
<div className="flex-1 flex flex-col gap-1">
|
||||||
<div>
|
<div>
|
||||||
<User.Name
|
<User.Name
|
||||||
className="shrink-0 inline font-medium text-blue-500"
|
className="mr-2 shrink-0 inline font-medium text-blue-500"
|
||||||
suffix=":"
|
suffix=":"
|
||||||
/>
|
/>
|
||||||
<div className="pl-2 inline select-text text-balance content-break overflow-hidden">
|
<Content
|
||||||
{event.content}
|
text={event.content}
|
||||||
</div>
|
className="inline select-text text-balance content-break overflow-hidden"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-between">
|
<div className="flex-1 flex items-center justify-between">
|
||||||
<span className="text-sm text-neutral-500">
|
<span className="text-sm text-neutral-500">
|
||||||
{replyTime(event.created_at)}
|
{replyTime(event.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center justify-end gap-3">
|
<div className="flex items-center justify-end gap-5">
|
||||||
<Note.Reply />
|
<Note.Reply smol />
|
||||||
<Note.Repost />
|
<Note.Repost smol />
|
||||||
<Note.Zap />
|
<Note.Zap smol />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{event.replies?.length ? (
|
{event.replies?.length ? (
|
||||||
@@ -74,26 +137,53 @@ export const ReplyNote = memo(function ReplyNote({
|
|||||||
function ChildReply({ event }: { event: LumeEvent }) {
|
function ChildReply({ event }: { event: LumeEvent }) {
|
||||||
const search = useSearch({ strict: false });
|
const search = useSearch({ strict: false });
|
||||||
|
|
||||||
|
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const menuItems = await Promise.all([
|
||||||
|
MenuItem.new({
|
||||||
|
text: "View Profile",
|
||||||
|
action: () => LumeWindow.openProfile(event.pubkey),
|
||||||
|
}),
|
||||||
|
MenuItem.new({
|
||||||
|
text: "Copy Public Key",
|
||||||
|
action: async () => {
|
||||||
|
const pubkey = await event.pubkeyAsBech32();
|
||||||
|
await writeText(pubkey);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const menu = await Menu.new({
|
||||||
|
items: menuItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
await menu.popup().catch((e) => console.error(e));
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
<User.Provider pubkey={event.pubkey}>
|
<User.Provider pubkey={event.pubkey}>
|
||||||
<div className="group flex flex-col gap-1">
|
<div className="group flex flex-col gap-1">
|
||||||
<div>
|
<div>
|
||||||
<User.Root className="inline">
|
<User.Root className="inline mr-2">
|
||||||
<User.Name className="font-medium text-blue-500" suffix=":" />
|
<button type="button" onClick={(e) => showContextMenu(e)}>
|
||||||
|
<User.Name className="font-medium text-blue-500" suffix=":" />
|
||||||
|
</button>
|
||||||
</User.Root>
|
</User.Root>
|
||||||
<div className="pl-2 inline select-text text-balance content-break overflow-hidden">
|
<Content
|
||||||
{event.content}
|
text={event.content}
|
||||||
</div>
|
className="inline select-text text-balance content-break overflow-hidden"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-between">
|
<div className="flex-1 flex items-center justify-between">
|
||||||
<span className="text-sm text-neutral-500">
|
<span className="text-sm text-neutral-500">
|
||||||
{replyTime(event.created_at)}
|
{replyTime(event.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<div className="invisible group-hover:visible flex items-center justify-end gap-3">
|
<div className="invisible group-hover:visible flex items-center justify-end gap-5">
|
||||||
<Note.Reply />
|
<Note.Reply smol />
|
||||||
<Note.Repost />
|
<Note.Repost smol />
|
||||||
<Note.Zap />
|
<Note.Zap smol />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{event.replies?.length ? (
|
{event.replies?.length ? (
|
||||||
@@ -124,3 +214,64 @@ function ChildReply({ event }: { event: LumeEvent }) {
|
|||||||
</Note.Provider>
|
</Note.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Content({ text, className }: { text: string; className?: string }) {
|
||||||
|
const content = useMemo(() => {
|
||||||
|
let replacedText: ReactNode[] | string = text.trim();
|
||||||
|
|
||||||
|
const nostr = replacedText
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter((w) => w.startsWith("nostr:"));
|
||||||
|
|
||||||
|
replacedText = reactStringReplace(text, /(https?:\/\/\S+)/g, (match, i) => (
|
||||||
|
<a
|
||||||
|
key={match + i}
|
||||||
|
href={match}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-blue-600 dark:text-blue-400 !underline"
|
||||||
|
>
|
||||||
|
{match}
|
||||||
|
</a>
|
||||||
|
));
|
||||||
|
|
||||||
|
replacedText = reactStringReplace(replacedText, /#(\w+)/g, (match, i) => (
|
||||||
|
<Hashtag key={match + i} tag={match} />
|
||||||
|
));
|
||||||
|
|
||||||
|
for (const word of nostr) {
|
||||||
|
const bech32 = word.replace("nostr:", "");
|
||||||
|
const data = nip19.decode(bech32);
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case "npub":
|
||||||
|
replacedText = reactStringReplace(replacedText, word, (match, i) => (
|
||||||
|
<MentionUser key={match + i} pubkey={data.data} />
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case "nprofile":
|
||||||
|
replacedText = reactStringReplace(replacedText, word, (match, i) => (
|
||||||
|
<MentionUser key={match + i} pubkey={data.data.pubkey} />
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
replacedText = reactStringReplace(replacedText, word, (match, i) => (
|
||||||
|
<a
|
||||||
|
key={match + i}
|
||||||
|
href={`https://njump.me/${bech32}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-blue-600 dark:text-blue-400 !underline"
|
||||||
|
>
|
||||||
|
{match}
|
||||||
|
</a>
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return replacedText;
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
|
return <div className={className}>{content}</div>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const RepostNote = memo(function RepostNote({
|
|||||||
</div>
|
</div>
|
||||||
<Note.Content className="px-3" />
|
<Note.Content className="px-3" />
|
||||||
<div className="flex items-center justify-between px-3 mt-3 h-14">
|
<div className="flex items-center justify-between px-3 mt-3 h-14">
|
||||||
<div className="inline-flex items-center gap-3">
|
<div className="inline-flex items-center gap-6">
|
||||||
<Note.Open />
|
<Note.Open />
|
||||||
<Note.Reply />
|
<Note.Reply />
|
||||||
<Note.Repost />
|
<Note.Repost />
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const TextNote = memo(function TextNote({
|
|||||||
<Note.Menu />
|
<Note.Menu />
|
||||||
</div>
|
</div>
|
||||||
<Note.Content className="px-3" />
|
<Note.Content className="px-3" />
|
||||||
<div className="flex items-center gap-4 px-3 mt-3 h-14">
|
<div className="flex items-center gap-6 px-3 mt-3 h-14">
|
||||||
<Note.Open />
|
<Note.Open />
|
||||||
<Note.Reply />
|
<Note.Reply />
|
||||||
<Note.Repost />
|
<Note.Repost />
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ export function UserAvatar({ className }: { className?: string }) {
|
|||||||
|
|
||||||
const picture = useMemo(() => {
|
const picture = useMemo(() => {
|
||||||
if (service?.length && user.profile?.picture?.length) {
|
if (service?.length && user.profile?.picture?.length) {
|
||||||
const url = `${service}?url=${user.profile?.picture}&w=100&h=100&default=1&n=-1`;
|
if (user.profile?.picture.includes("_next/")) {
|
||||||
return url;
|
return user.profile?.picture;
|
||||||
|
}
|
||||||
|
return `${service}?url=${user.profile?.picture}&w=100&h=100&n=-1&default=${user.profile?.picture}`;
|
||||||
} else {
|
} else {
|
||||||
return user.profile?.picture;
|
return user.profile?.picture;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ export function UserFollowButton({ className }: { className?: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
onClick={() => toggleFollow()}
|
onClick={() => toggleFollow()}
|
||||||
className={cn("w-max", className)}
|
className={cn("w-max gap-1", className)}
|
||||||
>
|
>
|
||||||
{isError ? "Error" : null}
|
{isError ? "Error" : null}
|
||||||
{isPending || isLoading ? <Spinner className="size-4" /> : null}
|
{isPending || isLoading ? <Spinner className="size-3" /> : null}
|
||||||
{isFollow ? "Unfollow" : "Follow"}
|
{isFollow ? "Unfollow" : "Follow"}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
+407
-45
@@ -13,6 +13,7 @@ import { createFileRoute } from '@tanstack/react-router'
|
|||||||
// Import Routes
|
// Import Routes
|
||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
|
import { Route as LoadingImport } from './routes/loading'
|
||||||
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
|
import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays'
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
import { Route as EditorIndexImport } from './routes/editor/index'
|
import { Route as EditorIndexImport } from './routes/editor/index'
|
||||||
@@ -95,6 +96,11 @@ const NewLazyRoute = NewLazyImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
|
const LoadingRoute = LoadingImport.update({
|
||||||
|
path: '/loading',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const BootstrapRelaysRoute = BootstrapRelaysImport.update({
|
const BootstrapRelaysRoute = BootstrapRelaysImport.update({
|
||||||
path: '/bootstrap-relays',
|
path: '/bootstrap-relays',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@@ -338,6 +344,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof BootstrapRelaysImport
|
preLoaderRoute: typeof BootstrapRelaysImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/loading': {
|
||||||
|
id: '/loading'
|
||||||
|
path: '/loading'
|
||||||
|
fullPath: '/loading'
|
||||||
|
preLoaderRoute: typeof LoadingImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/new': {
|
'/new': {
|
||||||
id: '/new'
|
id: '/new'
|
||||||
path: '/new'
|
path: '/new'
|
||||||
@@ -595,51 +608,396 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
// Create and export the route tree
|
// Create and export the route tree
|
||||||
|
|
||||||
export const routeTree = rootRoute.addChildren({
|
interface AccountAppRouteChildren {
|
||||||
IndexRoute,
|
AccountAppHomeRoute: typeof AccountAppHomeRoute
|
||||||
BootstrapRelaysRoute,
|
}
|
||||||
NewLazyRoute,
|
|
||||||
ResetLazyRoute,
|
const AccountAppRouteChildren: AccountAppRouteChildren = {
|
||||||
AccountRoute: AccountRoute.addChildren({
|
AccountAppHomeRoute: AccountAppHomeRoute,
|
||||||
AccountAppRoute: AccountAppRoute.addChildren({ AccountAppHomeRoute }),
|
}
|
||||||
AccountBackupRoute,
|
|
||||||
AccountSettingsLazyRoute: AccountSettingsLazyRoute.addChildren({
|
const AccountAppRouteWithChildren = AccountAppRoute._addFileChildren(
|
||||||
AccountSettingsBitcoinConnectRoute,
|
AccountAppRouteChildren,
|
||||||
AccountSettingsGeneralRoute,
|
)
|
||||||
AccountSettingsProfileRoute,
|
|
||||||
AccountSettingsRelayRoute,
|
interface AccountSettingsLazyRouteChildren {
|
||||||
AccountSettingsWalletRoute,
|
AccountSettingsBitcoinConnectRoute: typeof AccountSettingsBitcoinConnectRoute
|
||||||
}),
|
AccountSettingsGeneralRoute: typeof AccountSettingsGeneralRoute
|
||||||
}),
|
AccountSettingsProfileRoute: typeof AccountSettingsProfileRoute
|
||||||
ColumnsRoute: ColumnsRoute.addChildren({
|
AccountSettingsRelayRoute: typeof AccountSettingsRelayRoute
|
||||||
ColumnsLayoutRoute: ColumnsLayoutRoute.addChildren({
|
AccountSettingsWalletRoute: typeof AccountSettingsWalletRoute
|
||||||
ColumnsLayoutCreateGroupRoute,
|
}
|
||||||
ColumnsLayoutCreateNewsfeedRoute:
|
|
||||||
ColumnsLayoutCreateNewsfeedRoute.addChildren({
|
const AccountSettingsLazyRouteChildren: AccountSettingsLazyRouteChildren = {
|
||||||
ColumnsLayoutCreateNewsfeedF2fRoute,
|
AccountSettingsBitcoinConnectRoute: AccountSettingsBitcoinConnectRoute,
|
||||||
ColumnsLayoutCreateNewsfeedUsersRoute,
|
AccountSettingsGeneralRoute: AccountSettingsGeneralRoute,
|
||||||
}),
|
AccountSettingsProfileRoute: AccountSettingsProfileRoute,
|
||||||
ColumnsLayoutGalleryRoute,
|
AccountSettingsRelayRoute: AccountSettingsRelayRoute,
|
||||||
ColumnsLayoutGlobalRoute,
|
AccountSettingsWalletRoute: AccountSettingsWalletRoute,
|
||||||
ColumnsLayoutGroupRoute,
|
}
|
||||||
ColumnsLayoutStoriesRoute,
|
|
||||||
ColumnsLayoutNewsfeedLazyRoute,
|
const AccountSettingsLazyRouteWithChildren =
|
||||||
ColumnsLayoutNotificationLazyRoute,
|
AccountSettingsLazyRoute._addFileChildren(AccountSettingsLazyRouteChildren)
|
||||||
ColumnsLayoutOnboardingLazyRoute,
|
|
||||||
ColumnsLayoutSearchLazyRoute,
|
interface AccountRouteChildren {
|
||||||
ColumnsLayoutTrendingLazyRoute,
|
AccountAppRoute: typeof AccountAppRouteWithChildren
|
||||||
ColumnsLayoutEventsIdLazyRoute,
|
AccountBackupRoute: typeof AccountBackupRoute
|
||||||
ColumnsLayoutHashtagsContentLazyRoute,
|
AccountSettingsLazyRoute: typeof AccountSettingsLazyRouteWithChildren
|
||||||
ColumnsLayoutRepliesIdLazyRoute,
|
}
|
||||||
ColumnsLayoutUsersIdLazyRoute,
|
|
||||||
}),
|
const AccountRouteChildren: AccountRouteChildren = {
|
||||||
}),
|
AccountAppRoute: AccountAppRouteWithChildren,
|
||||||
ZapIdRoute,
|
AccountBackupRoute: AccountBackupRoute,
|
||||||
AuthConnectLazyRoute,
|
AccountSettingsLazyRoute: AccountSettingsLazyRouteWithChildren,
|
||||||
AuthImportLazyRoute,
|
}
|
||||||
AuthNewLazyRoute,
|
|
||||||
EditorIndexRoute,
|
const AccountRouteWithChildren =
|
||||||
})
|
AccountRoute._addFileChildren(AccountRouteChildren)
|
||||||
|
|
||||||
|
interface ColumnsLayoutCreateNewsfeedRouteChildren {
|
||||||
|
ColumnsLayoutCreateNewsfeedF2fRoute: typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
||||||
|
ColumnsLayoutCreateNewsfeedUsersRoute: typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const ColumnsLayoutCreateNewsfeedRouteChildren: ColumnsLayoutCreateNewsfeedRouteChildren =
|
||||||
|
{
|
||||||
|
ColumnsLayoutCreateNewsfeedF2fRoute: ColumnsLayoutCreateNewsfeedF2fRoute,
|
||||||
|
ColumnsLayoutCreateNewsfeedUsersRoute:
|
||||||
|
ColumnsLayoutCreateNewsfeedUsersRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ColumnsLayoutCreateNewsfeedRouteWithChildren =
|
||||||
|
ColumnsLayoutCreateNewsfeedRoute._addFileChildren(
|
||||||
|
ColumnsLayoutCreateNewsfeedRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface ColumnsLayoutRouteChildren {
|
||||||
|
ColumnsLayoutCreateGroupRoute: typeof ColumnsLayoutCreateGroupRoute
|
||||||
|
ColumnsLayoutCreateNewsfeedRoute: typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||||
|
ColumnsLayoutGalleryRoute: typeof ColumnsLayoutGalleryRoute
|
||||||
|
ColumnsLayoutGlobalRoute: typeof ColumnsLayoutGlobalRoute
|
||||||
|
ColumnsLayoutGroupRoute: typeof ColumnsLayoutGroupRoute
|
||||||
|
ColumnsLayoutStoriesRoute: typeof ColumnsLayoutStoriesRoute
|
||||||
|
ColumnsLayoutNewsfeedLazyRoute: typeof ColumnsLayoutNewsfeedLazyRoute
|
||||||
|
ColumnsLayoutNotificationLazyRoute: typeof ColumnsLayoutNotificationLazyRoute
|
||||||
|
ColumnsLayoutOnboardingLazyRoute: typeof ColumnsLayoutOnboardingLazyRoute
|
||||||
|
ColumnsLayoutSearchLazyRoute: typeof ColumnsLayoutSearchLazyRoute
|
||||||
|
ColumnsLayoutTrendingLazyRoute: typeof ColumnsLayoutTrendingLazyRoute
|
||||||
|
ColumnsLayoutEventsIdLazyRoute: typeof ColumnsLayoutEventsIdLazyRoute
|
||||||
|
ColumnsLayoutHashtagsContentLazyRoute: typeof ColumnsLayoutHashtagsContentLazyRoute
|
||||||
|
ColumnsLayoutRepliesIdLazyRoute: typeof ColumnsLayoutRepliesIdLazyRoute
|
||||||
|
ColumnsLayoutUsersIdLazyRoute: typeof ColumnsLayoutUsersIdLazyRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const ColumnsLayoutRouteChildren: ColumnsLayoutRouteChildren = {
|
||||||
|
ColumnsLayoutCreateGroupRoute: ColumnsLayoutCreateGroupRoute,
|
||||||
|
ColumnsLayoutCreateNewsfeedRoute:
|
||||||
|
ColumnsLayoutCreateNewsfeedRouteWithChildren,
|
||||||
|
ColumnsLayoutGalleryRoute: ColumnsLayoutGalleryRoute,
|
||||||
|
ColumnsLayoutGlobalRoute: ColumnsLayoutGlobalRoute,
|
||||||
|
ColumnsLayoutGroupRoute: ColumnsLayoutGroupRoute,
|
||||||
|
ColumnsLayoutStoriesRoute: ColumnsLayoutStoriesRoute,
|
||||||
|
ColumnsLayoutNewsfeedLazyRoute: ColumnsLayoutNewsfeedLazyRoute,
|
||||||
|
ColumnsLayoutNotificationLazyRoute: ColumnsLayoutNotificationLazyRoute,
|
||||||
|
ColumnsLayoutOnboardingLazyRoute: ColumnsLayoutOnboardingLazyRoute,
|
||||||
|
ColumnsLayoutSearchLazyRoute: ColumnsLayoutSearchLazyRoute,
|
||||||
|
ColumnsLayoutTrendingLazyRoute: ColumnsLayoutTrendingLazyRoute,
|
||||||
|
ColumnsLayoutEventsIdLazyRoute: ColumnsLayoutEventsIdLazyRoute,
|
||||||
|
ColumnsLayoutHashtagsContentLazyRoute: ColumnsLayoutHashtagsContentLazyRoute,
|
||||||
|
ColumnsLayoutRepliesIdLazyRoute: ColumnsLayoutRepliesIdLazyRoute,
|
||||||
|
ColumnsLayoutUsersIdLazyRoute: ColumnsLayoutUsersIdLazyRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ColumnsLayoutRouteWithChildren = ColumnsLayoutRoute._addFileChildren(
|
||||||
|
ColumnsLayoutRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface ColumnsRouteChildren {
|
||||||
|
ColumnsLayoutRoute: typeof ColumnsLayoutRouteWithChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
const ColumnsRouteChildren: ColumnsRouteChildren = {
|
||||||
|
ColumnsLayoutRoute: ColumnsLayoutRouteWithChildren,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ColumnsRouteWithChildren =
|
||||||
|
ColumnsRoute._addFileChildren(ColumnsRouteChildren)
|
||||||
|
|
||||||
|
export interface FileRoutesByFullPath {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||||
|
'/loading': typeof LoadingRoute
|
||||||
|
'/new': typeof NewLazyRoute
|
||||||
|
'/reset': typeof ResetLazyRoute
|
||||||
|
'/$account': typeof AccountSettingsLazyRouteWithChildren
|
||||||
|
'/$account/backup': typeof AccountBackupRoute
|
||||||
|
'/columns': typeof ColumnsLayoutRouteWithChildren
|
||||||
|
'/zap/$id': typeof ZapIdRoute
|
||||||
|
'/auth/connect': typeof AuthConnectLazyRoute
|
||||||
|
'/auth/import': typeof AuthImportLazyRoute
|
||||||
|
'/auth/new': typeof AuthNewLazyRoute
|
||||||
|
'/editor': typeof EditorIndexRoute
|
||||||
|
'/$account/home': typeof AccountAppHomeRoute
|
||||||
|
'/$account/bitcoin-connect': typeof AccountSettingsBitcoinConnectRoute
|
||||||
|
'/$account/general': typeof AccountSettingsGeneralRoute
|
||||||
|
'/$account/profile': typeof AccountSettingsProfileRoute
|
||||||
|
'/$account/relay': typeof AccountSettingsRelayRoute
|
||||||
|
'/$account/wallet': typeof AccountSettingsWalletRoute
|
||||||
|
'/columns/create-group': typeof ColumnsLayoutCreateGroupRoute
|
||||||
|
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||||
|
'/columns/gallery': typeof ColumnsLayoutGalleryRoute
|
||||||
|
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
||||||
|
'/columns/group': typeof ColumnsLayoutGroupRoute
|
||||||
|
'/columns/stories': typeof ColumnsLayoutStoriesRoute
|
||||||
|
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedLazyRoute
|
||||||
|
'/columns/notification': typeof ColumnsLayoutNotificationLazyRoute
|
||||||
|
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||||
|
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
|
||||||
|
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
|
||||||
|
'/columns/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
||||||
|
'/columns/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
||||||
|
'/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
||||||
|
'/columns/hashtags/$content': typeof ColumnsLayoutHashtagsContentLazyRoute
|
||||||
|
'/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
||||||
|
'/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileRoutesByTo {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||||
|
'/loading': typeof LoadingRoute
|
||||||
|
'/new': typeof NewLazyRoute
|
||||||
|
'/reset': typeof ResetLazyRoute
|
||||||
|
'/$account': typeof AccountSettingsLazyRouteWithChildren
|
||||||
|
'/$account/backup': typeof AccountBackupRoute
|
||||||
|
'/columns': typeof ColumnsLayoutRouteWithChildren
|
||||||
|
'/zap/$id': typeof ZapIdRoute
|
||||||
|
'/auth/connect': typeof AuthConnectLazyRoute
|
||||||
|
'/auth/import': typeof AuthImportLazyRoute
|
||||||
|
'/auth/new': typeof AuthNewLazyRoute
|
||||||
|
'/editor': typeof EditorIndexRoute
|
||||||
|
'/$account/home': typeof AccountAppHomeRoute
|
||||||
|
'/$account/bitcoin-connect': typeof AccountSettingsBitcoinConnectRoute
|
||||||
|
'/$account/general': typeof AccountSettingsGeneralRoute
|
||||||
|
'/$account/profile': typeof AccountSettingsProfileRoute
|
||||||
|
'/$account/relay': typeof AccountSettingsRelayRoute
|
||||||
|
'/$account/wallet': typeof AccountSettingsWalletRoute
|
||||||
|
'/columns/create-group': typeof ColumnsLayoutCreateGroupRoute
|
||||||
|
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||||
|
'/columns/gallery': typeof ColumnsLayoutGalleryRoute
|
||||||
|
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
||||||
|
'/columns/group': typeof ColumnsLayoutGroupRoute
|
||||||
|
'/columns/stories': typeof ColumnsLayoutStoriesRoute
|
||||||
|
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedLazyRoute
|
||||||
|
'/columns/notification': typeof ColumnsLayoutNotificationLazyRoute
|
||||||
|
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||||
|
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
|
||||||
|
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
|
||||||
|
'/columns/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
||||||
|
'/columns/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
||||||
|
'/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
||||||
|
'/columns/hashtags/$content': typeof ColumnsLayoutHashtagsContentLazyRoute
|
||||||
|
'/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
||||||
|
'/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileRoutesById {
|
||||||
|
__root__: typeof rootRoute
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||||
|
'/loading': typeof LoadingRoute
|
||||||
|
'/new': typeof NewLazyRoute
|
||||||
|
'/reset': typeof ResetLazyRoute
|
||||||
|
'/$account': typeof AccountRouteWithChildren
|
||||||
|
'/$account/_app': typeof AccountAppRouteWithChildren
|
||||||
|
'/$account/backup': typeof AccountBackupRoute
|
||||||
|
'/columns': typeof ColumnsRouteWithChildren
|
||||||
|
'/columns/_layout': typeof ColumnsLayoutRouteWithChildren
|
||||||
|
'/zap/$id': typeof ZapIdRoute
|
||||||
|
'/$account/_settings': typeof AccountSettingsLazyRouteWithChildren
|
||||||
|
'/auth/connect': typeof AuthConnectLazyRoute
|
||||||
|
'/auth/import': typeof AuthImportLazyRoute
|
||||||
|
'/auth/new': typeof AuthNewLazyRoute
|
||||||
|
'/editor/': typeof EditorIndexRoute
|
||||||
|
'/$account/_app/home': typeof AccountAppHomeRoute
|
||||||
|
'/$account/_settings/bitcoin-connect': typeof AccountSettingsBitcoinConnectRoute
|
||||||
|
'/$account/_settings/general': typeof AccountSettingsGeneralRoute
|
||||||
|
'/$account/_settings/profile': typeof AccountSettingsProfileRoute
|
||||||
|
'/$account/_settings/relay': typeof AccountSettingsRelayRoute
|
||||||
|
'/$account/_settings/wallet': typeof AccountSettingsWalletRoute
|
||||||
|
'/columns/_layout/create-group': typeof ColumnsLayoutCreateGroupRoute
|
||||||
|
'/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||||
|
'/columns/_layout/gallery': typeof ColumnsLayoutGalleryRoute
|
||||||
|
'/columns/_layout/global': typeof ColumnsLayoutGlobalRoute
|
||||||
|
'/columns/_layout/group': typeof ColumnsLayoutGroupRoute
|
||||||
|
'/columns/_layout/stories': typeof ColumnsLayoutStoriesRoute
|
||||||
|
'/columns/_layout/newsfeed': typeof ColumnsLayoutNewsfeedLazyRoute
|
||||||
|
'/columns/_layout/notification': typeof ColumnsLayoutNotificationLazyRoute
|
||||||
|
'/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||||
|
'/columns/_layout/search': typeof ColumnsLayoutSearchLazyRoute
|
||||||
|
'/columns/_layout/trending': typeof ColumnsLayoutTrendingLazyRoute
|
||||||
|
'/columns/_layout/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
||||||
|
'/columns/_layout/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
||||||
|
'/columns/_layout/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
||||||
|
'/columns/_layout/hashtags/$content': typeof ColumnsLayoutHashtagsContentLazyRoute
|
||||||
|
'/columns/_layout/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
||||||
|
'/columns/_layout/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileRouteTypes {
|
||||||
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
fullPaths:
|
||||||
|
| '/'
|
||||||
|
| '/bootstrap-relays'
|
||||||
|
| '/loading'
|
||||||
|
| '/new'
|
||||||
|
| '/reset'
|
||||||
|
| '/$account'
|
||||||
|
| '/$account/backup'
|
||||||
|
| '/columns'
|
||||||
|
| '/zap/$id'
|
||||||
|
| '/auth/connect'
|
||||||
|
| '/auth/import'
|
||||||
|
| '/auth/new'
|
||||||
|
| '/editor'
|
||||||
|
| '/$account/home'
|
||||||
|
| '/$account/bitcoin-connect'
|
||||||
|
| '/$account/general'
|
||||||
|
| '/$account/profile'
|
||||||
|
| '/$account/relay'
|
||||||
|
| '/$account/wallet'
|
||||||
|
| '/columns/create-group'
|
||||||
|
| '/columns/create-newsfeed'
|
||||||
|
| '/columns/gallery'
|
||||||
|
| '/columns/global'
|
||||||
|
| '/columns/group'
|
||||||
|
| '/columns/stories'
|
||||||
|
| '/columns/newsfeed'
|
||||||
|
| '/columns/notification'
|
||||||
|
| '/columns/onboarding'
|
||||||
|
| '/columns/search'
|
||||||
|
| '/columns/trending'
|
||||||
|
| '/columns/create-newsfeed/f2f'
|
||||||
|
| '/columns/create-newsfeed/users'
|
||||||
|
| '/columns/events/$id'
|
||||||
|
| '/columns/hashtags/$content'
|
||||||
|
| '/columns/replies/$id'
|
||||||
|
| '/columns/users/$id'
|
||||||
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
to:
|
||||||
|
| '/'
|
||||||
|
| '/bootstrap-relays'
|
||||||
|
| '/loading'
|
||||||
|
| '/new'
|
||||||
|
| '/reset'
|
||||||
|
| '/$account'
|
||||||
|
| '/$account/backup'
|
||||||
|
| '/columns'
|
||||||
|
| '/zap/$id'
|
||||||
|
| '/auth/connect'
|
||||||
|
| '/auth/import'
|
||||||
|
| '/auth/new'
|
||||||
|
| '/editor'
|
||||||
|
| '/$account/home'
|
||||||
|
| '/$account/bitcoin-connect'
|
||||||
|
| '/$account/general'
|
||||||
|
| '/$account/profile'
|
||||||
|
| '/$account/relay'
|
||||||
|
| '/$account/wallet'
|
||||||
|
| '/columns/create-group'
|
||||||
|
| '/columns/create-newsfeed'
|
||||||
|
| '/columns/gallery'
|
||||||
|
| '/columns/global'
|
||||||
|
| '/columns/group'
|
||||||
|
| '/columns/stories'
|
||||||
|
| '/columns/newsfeed'
|
||||||
|
| '/columns/notification'
|
||||||
|
| '/columns/onboarding'
|
||||||
|
| '/columns/search'
|
||||||
|
| '/columns/trending'
|
||||||
|
| '/columns/create-newsfeed/f2f'
|
||||||
|
| '/columns/create-newsfeed/users'
|
||||||
|
| '/columns/events/$id'
|
||||||
|
| '/columns/hashtags/$content'
|
||||||
|
| '/columns/replies/$id'
|
||||||
|
| '/columns/users/$id'
|
||||||
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/'
|
||||||
|
| '/bootstrap-relays'
|
||||||
|
| '/loading'
|
||||||
|
| '/new'
|
||||||
|
| '/reset'
|
||||||
|
| '/$account'
|
||||||
|
| '/$account/_app'
|
||||||
|
| '/$account/backup'
|
||||||
|
| '/columns'
|
||||||
|
| '/columns/_layout'
|
||||||
|
| '/zap/$id'
|
||||||
|
| '/$account/_settings'
|
||||||
|
| '/auth/connect'
|
||||||
|
| '/auth/import'
|
||||||
|
| '/auth/new'
|
||||||
|
| '/editor/'
|
||||||
|
| '/$account/_app/home'
|
||||||
|
| '/$account/_settings/bitcoin-connect'
|
||||||
|
| '/$account/_settings/general'
|
||||||
|
| '/$account/_settings/profile'
|
||||||
|
| '/$account/_settings/relay'
|
||||||
|
| '/$account/_settings/wallet'
|
||||||
|
| '/columns/_layout/create-group'
|
||||||
|
| '/columns/_layout/create-newsfeed'
|
||||||
|
| '/columns/_layout/gallery'
|
||||||
|
| '/columns/_layout/global'
|
||||||
|
| '/columns/_layout/group'
|
||||||
|
| '/columns/_layout/stories'
|
||||||
|
| '/columns/_layout/newsfeed'
|
||||||
|
| '/columns/_layout/notification'
|
||||||
|
| '/columns/_layout/onboarding'
|
||||||
|
| '/columns/_layout/search'
|
||||||
|
| '/columns/_layout/trending'
|
||||||
|
| '/columns/_layout/create-newsfeed/f2f'
|
||||||
|
| '/columns/_layout/create-newsfeed/users'
|
||||||
|
| '/columns/_layout/events/$id'
|
||||||
|
| '/columns/_layout/hashtags/$content'
|
||||||
|
| '/columns/_layout/replies/$id'
|
||||||
|
| '/columns/_layout/users/$id'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
IndexRoute: typeof IndexRoute
|
||||||
|
BootstrapRelaysRoute: typeof BootstrapRelaysRoute
|
||||||
|
LoadingRoute: typeof LoadingRoute
|
||||||
|
NewLazyRoute: typeof NewLazyRoute
|
||||||
|
ResetLazyRoute: typeof ResetLazyRoute
|
||||||
|
AccountRoute: typeof AccountRouteWithChildren
|
||||||
|
ColumnsRoute: typeof ColumnsRouteWithChildren
|
||||||
|
ZapIdRoute: typeof ZapIdRoute
|
||||||
|
AuthConnectLazyRoute: typeof AuthConnectLazyRoute
|
||||||
|
AuthImportLazyRoute: typeof AuthImportLazyRoute
|
||||||
|
AuthNewLazyRoute: typeof AuthNewLazyRoute
|
||||||
|
EditorIndexRoute: typeof EditorIndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
|
IndexRoute: IndexRoute,
|
||||||
|
BootstrapRelaysRoute: BootstrapRelaysRoute,
|
||||||
|
LoadingRoute: LoadingRoute,
|
||||||
|
NewLazyRoute: NewLazyRoute,
|
||||||
|
ResetLazyRoute: ResetLazyRoute,
|
||||||
|
AccountRoute: AccountRouteWithChildren,
|
||||||
|
ColumnsRoute: ColumnsRouteWithChildren,
|
||||||
|
ZapIdRoute: ZapIdRoute,
|
||||||
|
AuthConnectLazyRoute: AuthConnectLazyRoute,
|
||||||
|
AuthImportLazyRoute: AuthImportLazyRoute,
|
||||||
|
AuthNewLazyRoute: AuthNewLazyRoute,
|
||||||
|
EditorIndexRoute: EditorIndexRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routeTree = rootRoute
|
||||||
|
._addFileChildren(rootRouteChildren)
|
||||||
|
._addFileTypes<FileRouteTypes>()
|
||||||
|
|
||||||
/* prettier-ignore-end */
|
/* prettier-ignore-end */
|
||||||
|
|
||||||
@@ -651,6 +1009,7 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
"/bootstrap-relays",
|
"/bootstrap-relays",
|
||||||
|
"/loading",
|
||||||
"/new",
|
"/new",
|
||||||
"/reset",
|
"/reset",
|
||||||
"/$account",
|
"/$account",
|
||||||
@@ -668,6 +1027,9 @@ export const routeTree = rootRoute.addChildren({
|
|||||||
"/bootstrap-relays": {
|
"/bootstrap-relays": {
|
||||||
"filePath": "bootstrap-relays.tsx"
|
"filePath": "bootstrap-relays.tsx"
|
||||||
},
|
},
|
||||||
|
"/loading": {
|
||||||
|
"filePath": "loading.tsx"
|
||||||
|
},
|
||||||
"/new": {
|
"/new": {
|
||||||
"filePath": "new.lazy.tsx"
|
"filePath": "new.lazy.tsx"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ function Screen() {
|
|||||||
description="Shows a warning for notes that have a content warning."
|
description="Shows a warning for notes that have a content warning."
|
||||||
label="content_warning"
|
label="content_warning"
|
||||||
/>
|
/>
|
||||||
|
<Setting
|
||||||
|
name="Trusted Only"
|
||||||
|
description="Only shows note's replies from your inner circle."
|
||||||
|
label="trusted_only"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -128,11 +133,12 @@ function Screen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky bottom-0 left-0 w-full h-11 flex items-center justify-end px-3 bg-white/20 dark:bg-black-20 backdrop-blur-md border-t border-black/5 dark:border-white/5">
|
<div className="sticky bottom-0 left-0 w-full h-16 flex items-center justify-end px-3">
|
||||||
|
<div className="absolute left-0 bottom-0 w-full h-11 gradient-mask-t-0 bg-neutral-100 dark:bg-neutral-900" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateSettings()}
|
onClick={() => updateSettings()}
|
||||||
className="inline-flex items-center justify-center w-20 rounded-md shadow h-7 bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium"
|
className="relative z-10 inline-flex items-center justify-center w-20 rounded-md shadow h-8 bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium"
|
||||||
>
|
>
|
||||||
{isPending ? <Spinner className="size-4" /> : "Update"}
|
{isPending ? <Spinner className="size-4" /> : "Update"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -68,12 +68,12 @@ function Screen() {
|
|||||||
placeholder="bunker://..."
|
placeholder="bunker://..."
|
||||||
value={uri}
|
value={uri}
|
||||||
onChange={(e) => setUri(e.target.value)}
|
onChange={(e) => setUri(e.target.value)}
|
||||||
className="pl-3 pr-12 rounded-lg w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none"
|
className="pl-3 pr-12 rounded-lg w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => pasteFromClipboard()}
|
onClick={() => pasteFromClipboard()}
|
||||||
className="absolute top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500"
|
className="absolute top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500 dark:text-blue-300"
|
||||||
>
|
>
|
||||||
Paste
|
Paste
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -84,24 +84,26 @@ function Screen() {
|
|||||||
placeholder="nsec or ncryptsec..."
|
placeholder="nsec or ncryptsec..."
|
||||||
value={key}
|
value={key}
|
||||||
onChange={(e) => setKey(e.target.value)}
|
onChange={(e) => setKey(e.target.value)}
|
||||||
className="pl-3 pr-12 rounded-lg w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
|
className="pl-3 pr-12 rounded-lg w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => pasteFromClipboard()}
|
onClick={() => pasteFromClipboard()}
|
||||||
className="absolute uppercase top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500"
|
className="absolute top-1/2 right-2 transform -translate-y-1/2 text-xs font-semibold text-blue-500 dark:text-blue-300"
|
||||||
>
|
>
|
||||||
Paste
|
Paste
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{key.length && !key.startsWith("ncryptsec") ? (
|
{key.length ? (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label
|
<label
|
||||||
htmlFor="password"
|
htmlFor="password"
|
||||||
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||||
>
|
>
|
||||||
Set password to secure your key
|
{!key.startsWith("ncryptsec")
|
||||||
|
? "Set password to secure your key"
|
||||||
|
: "Enter password to decrypt your key"}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
name="password"
|
name="password"
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ function Screen() {
|
|||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="e.g. Alice"
|
placeholder="e.g. Alice"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:ring-0 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-600"
|
className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:ring-0 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -126,7 +126,7 @@ function Screen() {
|
|||||||
onChange={(e) => setAbout(e.target.value)}
|
onChange={(e) => setAbout(e.target.value)}
|
||||||
placeholder="e.g. Artist, anime-lover, and k-pop fan"
|
placeholder="e.g. Artist, anime-lover, and k-pop fan"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
className="px-3 py-1.5 rounded-lg min-h-16 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:ring-0 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-600"
|
className="px-3 py-1.5 rounded-lg min-h-16 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:ring-0 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-px w-full mt-2 bg-neutral-100 dark:bg-neutral-900" />
|
<div className="h-px w-full mt-2 bg-neutral-100 dark:bg-neutral-900" />
|
||||||
@@ -142,7 +142,7 @@ function Screen() {
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:ring-0 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-600"
|
className="px-3 rounded-lg h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:ring-0 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:text-neutral-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Frame>
|
</Frame>
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ function RootEvent() {
|
|||||||
</div>
|
</div>
|
||||||
<Note.ContentLarge className="px-3" />
|
<Note.ContentLarge className="px-3" />
|
||||||
<div className="flex items-center gap-2 px-3 mt-6 h-12 rounded-b-xl bg-neutral-50 dark:bg-white/5">
|
<div className="flex items-center gap-2 px-3 mt-6 h-12 rounded-b-xl bg-neutral-50 dark:bg-white/5">
|
||||||
<Note.Reply large />
|
<Note.Reply label />
|
||||||
<Note.Repost large />
|
<Note.Repost label />
|
||||||
<Note.Zap large />
|
<Note.Zap label />
|
||||||
</div>
|
</div>
|
||||||
</Note.Root>
|
</Note.Root>
|
||||||
</Note.Provider>
|
</Note.Provider>
|
||||||
@@ -182,7 +182,7 @@ function ReplyList() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
events.subscription
|
events.subscription
|
||||||
.emit({ label, kind: "Subscribe", event_id: id, local_only: undefined })
|
.emit({ label, kind: "Subscribe", event_id: id })
|
||||||
.then(() => console.log("Subscribe: ", label));
|
.then(() => console.log("Subscribe: ", label));
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -191,7 +191,6 @@ function ReplyList() {
|
|||||||
label,
|
label,
|
||||||
kind: "Unsubscribe",
|
kind: "Unsubscribe",
|
||||||
event_id: id,
|
event_id: id,
|
||||||
local_only: undefined,
|
|
||||||
})
|
})
|
||||||
.then(() => console.log("Unsubscribe: ", label));
|
.then(() => console.log("Unsubscribe: ", label));
|
||||||
};
|
};
|
||||||
@@ -225,7 +224,7 @@ function ReplyList() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center w-full mb-3 h-12 bg-black/5 dark:bg-white/5 rounded-xl">
|
<div className="flex items-center justify-center w-full mb-3 h-12 bg-black/5 dark:bg-white/5 rounded-xl">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-4" />
|
||||||
<span className="text-sm font-medium">Getting replies...</span>
|
<span className="text-sm font-medium">Getting replies...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function Screen() {
|
|||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||||
const res = await commands.getEventsFromContacts(until);
|
const res = await commands.getLocalEvents(until);
|
||||||
|
|
||||||
if (res.status === "error") {
|
if (res.status === "error") {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
@@ -82,7 +82,7 @@ export function Screen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = listen("newsfeed_synchronized", async () => {
|
const unlisten = listen("synchronized", async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: [label, account] });
|
await queryClient.invalidateQueries({ queryKey: [label, account] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { decodeZapInvoice, formatCreatedAt } from "@/commons";
|
import { decodeZapInvoice, formatCreatedAt } from "@/commons";
|
||||||
import { Note, Spinner, User } from "@/components";
|
import { Note, Spinner, User } from "@/components";
|
||||||
import { LumeEvent, useEvent } from "@/system";
|
import { LumeEvent, LumeWindow, useEvent } from "@/system";
|
||||||
import { Kind, type NostrEvent } from "@/types";
|
import { Kind, type NostrEvent } from "@/types";
|
||||||
import { Info, Repeat } from "@phosphor-icons/react";
|
import { Info, Repeat } from "@phosphor-icons/react";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
@@ -264,40 +264,46 @@ function TextNote({ event }: { event: LumeEvent }) {
|
|||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={event}>
|
<button
|
||||||
<Note.Root className="flex flex-col p-3 mb-3 bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5">
|
type="button"
|
||||||
<User.Provider pubkey={event.pubkey}>
|
onClick={() => LumeWindow.openEvent(event)}
|
||||||
<User.Root className="inline-flex items-center gap-2">
|
className="w-full rounded-xl hover:ring-1 ring-blue-500 mb-3"
|
||||||
<User.Avatar className="rounded-full size-9" />
|
>
|
||||||
<div className="flex flex-col flex-1">
|
<Note.Provider event={event}>
|
||||||
<div className="flex items-baseline justify-between w-full">
|
<Note.Root className="flex flex-col p-3 rounded-xl bg-white dark:bg-black/20 shadow-primary dark:ring-1 dark:ring-white/5">
|
||||||
<User.Name className="text-sm font-semibold leading-tight" />
|
<User.Provider pubkey={event.pubkey}>
|
||||||
<span className="text-sm leading-tight text-black/50 dark:text-white/50">
|
<User.Root className="inline-flex items-center gap-2">
|
||||||
{formatCreatedAt(event.created_at)}
|
<User.Avatar className="rounded-full size-9" />
|
||||||
</span>
|
<div className="flex flex-col flex-1">
|
||||||
</div>
|
<div className="flex items-baseline justify-between w-full">
|
||||||
<div className="inline-flex items-baseline gap-1 text-xs">
|
<User.Name className="text-sm font-semibold leading-tight" />
|
||||||
<span className="leading-tight text-black/50 dark:text-white/50">
|
<span className="text-sm leading-tight text-black/50 dark:text-white/50">
|
||||||
Reply to:
|
{formatCreatedAt(event.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<div className="inline-flex items-baseline gap-1">
|
</div>
|
||||||
{[...new Set(pTags)].map((replyTo) => (
|
<div className="inline-flex items-baseline gap-1 text-xs">
|
||||||
<User.Provider key={replyTo} pubkey={replyTo}>
|
<span className="leading-tight text-black/50 dark:text-white/50">
|
||||||
<User.Root>
|
Reply to:
|
||||||
<User.Name className="font-medium leading-tight" />
|
</span>
|
||||||
</User.Root>
|
<div className="inline-flex items-baseline gap-1">
|
||||||
</User.Provider>
|
{[...new Set(pTags)].map((replyTo) => (
|
||||||
))}
|
<User.Provider key={replyTo} pubkey={replyTo}>
|
||||||
|
<User.Root>
|
||||||
|
<User.Name className="font-medium leading-tight" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</User.Root>
|
||||||
</User.Root>
|
</User.Provider>
|
||||||
</User.Provider>
|
<div className="flex gap-2">
|
||||||
<div className="flex gap-2">
|
<div className="w-9 shrink-0" />
|
||||||
<div className="w-9 shrink-0" />
|
<div className="line-clamp-1 text-start">{event.content}</div>
|
||||||
<div className="line-clamp-1 text-start">{event.content}</div>
|
</div>
|
||||||
</div>
|
</Note.Root>
|
||||||
</Note.Root>
|
</Note.Provider>
|
||||||
</Note.Provider>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const Route = createLazyFileRoute("/columns/_layout/trending")({
|
|||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Screen() {
|
function Screen() {
|
||||||
const { isLoading, isError, data } = useQuery({
|
const { isLoading, isError, data } = useQuery({
|
||||||
queryKey: ["trending-notes"],
|
queryKey: ["trending-notes"],
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
import { insertImage, isImagePath, upload } from "@/commons";
|
import { isImagePath, upload } from "@/commons";
|
||||||
import { Spinner } from "@/components";
|
import { Spinner } from "@/components";
|
||||||
import { Images } from "@phosphor-icons/react";
|
import { Images } from "@phosphor-icons/react";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import { useEffect, useTransition } from "react";
|
import {
|
||||||
import { useSlateStatic } from "slate-react";
|
type Dispatch,
|
||||||
|
type SetStateAction,
|
||||||
|
useEffect,
|
||||||
|
useTransition,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
export function MediaButton() {
|
export function MediaButton({
|
||||||
const editor = useSlateStatic();
|
setText,
|
||||||
|
setAttaches,
|
||||||
|
}: {
|
||||||
|
setText: Dispatch<SetStateAction<string>>;
|
||||||
|
setAttaches: Dispatch<SetStateAction<string[]>>;
|
||||||
|
}) {
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const uploadMedia = () => {
|
const uploadMedia = () => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
const image = await upload();
|
const image = await upload();
|
||||||
return insertImage(editor, image);
|
setText((prev) => `${prev}\n${image}`);
|
||||||
|
setAttaches((prev) => [...prev, image]);
|
||||||
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await message(String(e), { title: "Upload", kind: "error" });
|
await message(String(e), { title: "Upload", kind: "error" });
|
||||||
return;
|
return;
|
||||||
@@ -32,7 +43,8 @@ export function MediaButton() {
|
|||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (isImagePath(item)) {
|
if (isImagePath(item)) {
|
||||||
const image = await upload(item);
|
const image = await upload(item);
|
||||||
insertImage(editor, image);
|
setText((prev) => `${prev}\n${image}`);
|
||||||
|
setAttaches((prev) => [...prev, image]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+283
-309
@@ -1,23 +1,20 @@
|
|||||||
import { cn, insertImage, insertNostrEvent, isImageUrl } from "@/commons";
|
// @ts-nocheck
|
||||||
|
import { type Mention, commands } from "@/commands.gen";
|
||||||
|
import { cn } from "@/commons";
|
||||||
import { Spinner } from "@/components";
|
import { Spinner } from "@/components";
|
||||||
import { Note } from "@/components/note";
|
import { Note } from "@/components/note";
|
||||||
import { MentionNote } from "@/components/note/mentions/note";
|
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import { LumeEvent, useEvent } from "@/system";
|
import { LumeEvent, useEvent } from "@/system";
|
||||||
import { Feather } from "@phosphor-icons/react";
|
import { Feather } from "@phosphor-icons/react";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useRef, useState, useTransition } from "react";
|
||||||
import { type Descendant, Node, Transforms, createEditor } from "slate";
|
import { createPortal } from "react-dom";
|
||||||
import {
|
import {
|
||||||
Editable,
|
RichTextarea,
|
||||||
ReactEditor,
|
type RichTextareaHandle,
|
||||||
Slate,
|
createRegexRenderer,
|
||||||
useFocused,
|
} from "rich-textarea";
|
||||||
useSelected,
|
|
||||||
useSlateStatic,
|
|
||||||
withReact,
|
|
||||||
} from "slate-react";
|
|
||||||
import { MediaButton } from "./-components/media";
|
import { MediaButton } from "./-components/media";
|
||||||
import { PowButton } from "./-components/pow";
|
import { PowButton } from "./-components/pow";
|
||||||
import { WarningButton } from "./-components/warning";
|
import { WarningButton } from "./-components/warning";
|
||||||
@@ -27,11 +24,33 @@ type EditorSearch = {
|
|||||||
quote: string;
|
quote: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EditorElement = {
|
const MENTION_REG = /\B@([\-+\w]*)$/;
|
||||||
type: string;
|
const MAX_LIST_LENGTH = 5;
|
||||||
children: Descendant[];
|
|
||||||
eventId?: string;
|
const renderer = createRegexRenderer([
|
||||||
};
|
[
|
||||||
|
/https?:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+/g,
|
||||||
|
({ children, key, value }) => (
|
||||||
|
<a
|
||||||
|
key={key}
|
||||||
|
href={value}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-blue-500 !underline"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
/(?:^|\W)nostr:(\w+)(?!\w)/g,
|
||||||
|
({ children, key, value }) => (
|
||||||
|
<span key={key} className="text-blue-500">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
export const Route = createFileRoute("/editor/")({
|
export const Route = createFileRoute("/editor/")({
|
||||||
validateSearch: (search: Record<string, string>): EditorSearch => {
|
validateSearch: (search: Record<string, string>): EditorSearch => {
|
||||||
@@ -40,201 +59,295 @@ export const Route = createFileRoute("/editor/")({
|
|||||||
quote: search.quote,
|
quote: search.quote,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeLoad: ({ search }) => {
|
beforeLoad: async ({ search }) => {
|
||||||
let initialValue: EditorElement[];
|
let users: Mention[] = [];
|
||||||
|
let initialValue: string;
|
||||||
|
|
||||||
if (search?.quote?.length) {
|
if (search?.quote?.length) {
|
||||||
const eventId = nip19.noteEncode(search.quote);
|
initialValue = `\nnostr:${nip19.noteEncode(search.quote)}`;
|
||||||
initialValue = [
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
children: [{ text: "" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
eventId: `nostr:${eventId}`,
|
|
||||||
children: [{ text: "" }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
} else {
|
||||||
initialValue = [
|
initialValue = "";
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
children: [{ text: "" }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { initialValue };
|
const res = await commands.getMentionList();
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
users = res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { users, initialValue };
|
||||||
},
|
},
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { reply_to } = Route.useSearch();
|
const { reply_to } = Route.useSearch();
|
||||||
const { initialValue } = Route.useRouteContext();
|
const { users, initialValue } = Route.useRouteContext();
|
||||||
|
|
||||||
const [editorValue, setEditorValue] = useState<EditorElement[]>(null);
|
const [isPending, startTransition] = useTransition();
|
||||||
const [loading, setLoading] = useState(false);
|
const [text, setText] = useState("");
|
||||||
|
const [attaches, setAttaches] = useState<string[]>(null);
|
||||||
const [warning, setWarning] = useState({ enable: false, reason: "" });
|
const [warning, setWarning] = useState({ enable: false, reason: "" });
|
||||||
const [difficulty, setDifficulty] = useState({ enable: false, num: 21 });
|
const [difficulty, setDifficulty] = useState({ enable: false, num: 21 });
|
||||||
const [editor] = useState(() =>
|
const [index, setIndex] = useState<number>(0);
|
||||||
withMentions(withNostrEvent(withImages(withReact(createEditor())))),
|
const [pos, setPos] = useState<{
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
caret: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const ref = useRef<RichTextareaHandle>(null);
|
||||||
|
const targetText = pos ? text.slice(0, pos.caret) : text;
|
||||||
|
const match = pos && targetText.match(MENTION_REG);
|
||||||
|
const name = match?.[1] ?? "";
|
||||||
|
const filtered = useMemo(
|
||||||
|
() =>
|
||||||
|
users
|
||||||
|
.filter((u) => u.name.toLowerCase().startsWith(name.toLowerCase()))
|
||||||
|
.slice(0, MAX_LIST_LENGTH),
|
||||||
|
[name],
|
||||||
);
|
);
|
||||||
|
|
||||||
const reset = () => {
|
const insert = (i: number) => {
|
||||||
// @ts-expect-error, backlog
|
if (!ref.current || !pos) return;
|
||||||
editor.children = [{ type: "paragraph", children: [{ text: "" }] }];
|
|
||||||
setEditorValue([{ type: "paragraph", children: [{ text: "" }] }]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const serialize = (nodes: Descendant[]) => {
|
const selected = filtered[i];
|
||||||
return nodes
|
|
||||||
.map((n) => {
|
|
||||||
// @ts-expect-error, backlog
|
|
||||||
if (n.type === "image") return n.url;
|
|
||||||
// @ts-expect-error, backlog
|
|
||||||
if (n.type === "event") return n.eventId;
|
|
||||||
|
|
||||||
// @ts-expect-error, backlog
|
ref.current.setRangeText(
|
||||||
if (n.children.length) {
|
`nostr:${selected.pubkey} `,
|
||||||
// @ts-expect-error, backlog
|
pos.caret - name.length - 1,
|
||||||
return n.children
|
pos.caret,
|
||||||
.map((n) => {
|
"end",
|
||||||
if (n.type === "mention") return n.npub;
|
);
|
||||||
return Node.string(n).trim();
|
|
||||||
})
|
|
||||||
.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Node.string(n);
|
setPos(null);
|
||||||
})
|
setIndex(0);
|
||||||
.join("\n");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const publish = async () => {
|
const publish = async () => {
|
||||||
try {
|
startTransition(async () => {
|
||||||
// start loading
|
try {
|
||||||
setLoading(true);
|
const content = text.trim();
|
||||||
|
|
||||||
const content = serialize(editor.children);
|
await LumeEvent.publish(
|
||||||
const eventId = await LumeEvent.publish(
|
content,
|
||||||
content,
|
warning.enable && warning.reason.length ? warning.reason : null,
|
||||||
warning.enable && warning.reason.length ? warning.reason : null,
|
difficulty.num,
|
||||||
difficulty.enable && difficulty.num > 0 ? difficulty.num : null,
|
reply_to,
|
||||||
reply_to,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
if (eventId) {
|
setText("");
|
||||||
// stop loading
|
} catch {
|
||||||
setLoading(false);
|
return;
|
||||||
// reset form
|
|
||||||
reset();
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
});
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditorValue(initialValue);
|
if (initialValue?.length) {
|
||||||
|
setText(initialValue);
|
||||||
|
}
|
||||||
}, [initialValue]);
|
}, [initialValue]);
|
||||||
|
|
||||||
if (!editorValue) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full h-full">
|
<div className="flex flex-col w-full h-full">
|
||||||
<Slate editor={editor} initialValue={editorValue}>
|
<div data-tauri-drag-region className="h-11 shrink-0" />
|
||||||
<div data-tauri-drag-region className="h-11 shrink-0" />
|
<div className="flex flex-col flex-1 overflow-y-auto">
|
||||||
<div className="flex flex-col flex-1 overflow-y-auto">
|
{reply_to?.length ? (
|
||||||
{reply_to?.length ? (
|
<div className="flex flex-col gap-2 px-3.5 pb-3 border-b border-black/5 dark:border-white/5">
|
||||||
<div className="flex flex-col gap-2 px-3.5 pb-3 border-b border-black/5 dark:border-white/5">
|
<span className="text-sm font-semibold">Reply to:</span>
|
||||||
<span className="text-sm font-semibold">Reply to:</span>
|
<EmbedNote id={reply_to} />
|
||||||
<ChildNote id={reply_to} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="px-4 py-4 overflow-y-auto">
|
|
||||||
<Editable
|
|
||||||
key={JSON.stringify(editorValue)}
|
|
||||||
autoFocus={true}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect="none"
|
|
||||||
spellCheck={false}
|
|
||||||
renderElement={(props) => <Element {...props} />}
|
|
||||||
placeholder={
|
|
||||||
reply_to ? "Type your reply..." : "What're you up to?"
|
|
||||||
}
|
|
||||||
className="focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{warning.enable ? (
|
|
||||||
<div className="flex items-center w-full px-4 border-t h-11 shrink-0 border-black/5 dark:border-white/5">
|
|
||||||
<span className="text-sm shrink-0 text-black/50 dark:text-white/50">
|
|
||||||
Reason:
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="NSFW..."
|
|
||||||
value={warning.reason}
|
|
||||||
onChange={(e) =>
|
|
||||||
setWarning((prev) => ({ ...prev, reason: e.target.value }))
|
|
||||||
}
|
|
||||||
className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{difficulty.enable ? (
|
<div className="p-4 overflow-y-auto h-full">
|
||||||
<div className="flex items-center w-full px-4 border-t h-11 shrink-0 border-black/5 dark:border-white/5">
|
<RichTextarea
|
||||||
<span className="text-sm shrink-0 text-black/50 dark:text-white/50">
|
ref={ref}
|
||||||
Difficulty:
|
value={text}
|
||||||
</span>
|
placeholder={reply_to ? "Type your reply..." : "What're you up to?"}
|
||||||
<input
|
style={{ width: "100%", height: "100%" }}
|
||||||
type="text"
|
className="text-[15px] leading-normal resize-none border-none focus:outline-none focus:ring-0 placeholder:text-neutral-500 placeholder:pt-[1.5px] placeholder:pl-2"
|
||||||
inputMode="numeric"
|
onChange={(e) => setText(e.target.value)}
|
||||||
pattern="[0-9]"
|
onKeyDown={(e) => {
|
||||||
onKeyDown={(event) => {
|
if (!pos || !filtered.length) return;
|
||||||
if (!/[0-9]/.test(event.key)) {
|
switch (e.code) {
|
||||||
event.preventDefault();
|
case "ArrowUp": {
|
||||||
|
e.preventDefault();
|
||||||
|
const nextIndex =
|
||||||
|
index <= 0 ? filtered.length - 1 : index - 1;
|
||||||
|
setIndex(nextIndex);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}}
|
case "ArrowDown": {
|
||||||
placeholder="21"
|
e.preventDefault();
|
||||||
defaultValue={difficulty.num}
|
const prevIndex =
|
||||||
onChange={(e) =>
|
index >= filtered.length - 1 ? 0 : index + 1;
|
||||||
setWarning((prev) => ({ ...prev, num: Number(e.target.value) }))
|
setIndex(prevIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Enter":
|
||||||
|
e.preventDefault();
|
||||||
|
insert(index);
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
e.preventDefault();
|
||||||
|
setPos(null);
|
||||||
|
setIndex(0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50"
|
}}
|
||||||
/>
|
onSelectionChange={(r) => {
|
||||||
</div>
|
if (
|
||||||
) : null}
|
r.focused &&
|
||||||
<div
|
MENTION_REG.test(text.slice(0, r.selectionStart))
|
||||||
data-tauri-drag-region
|
) {
|
||||||
className="flex items-center w-full h-16 gap-4 px-4 border-t divide-x divide-black/5 dark:divide-white/5 shrink-0 border-black/5 dark:border-white/5"
|
setPos({
|
||||||
>
|
top: r.top + r.height,
|
||||||
<button
|
left: r.left,
|
||||||
type="button"
|
caret: r.selectionStart,
|
||||||
onClick={() => publish()}
|
});
|
||||||
className="inline-flex items-center justify-center h-8 gap-1 px-2.5 text-sm font-medium rounded-lg bg-black/10 w-max hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
setIndex(0);
|
||||||
|
} else {
|
||||||
|
setPos(null);
|
||||||
|
setIndex(0);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{renderer}
|
||||||
<Spinner className="size-4" />
|
</RichTextarea>
|
||||||
) : (
|
{pos
|
||||||
<Feather className="size-4" weight="fill" />
|
? createPortal(
|
||||||
)}
|
<Menu
|
||||||
Publish
|
top={pos.top}
|
||||||
</button>
|
left={pos.left}
|
||||||
<div className="inline-flex items-center flex-1 gap-2 pl-4">
|
users={filtered}
|
||||||
<MediaButton />
|
index={index}
|
||||||
<WarningButton setWarning={setWarning} />
|
insert={insert}
|
||||||
<PowButton setDifficulty={setDifficulty} />
|
/>,
|
||||||
</div>
|
document.body,
|
||||||
|
)
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
</Slate>
|
</div>
|
||||||
|
{warning.enable ? (
|
||||||
|
<div className="flex items-center w-full px-4 border-t h-11 shrink-0 border-black/5 dark:border-white/5">
|
||||||
|
<span className="text-sm shrink-0 text-black/50 dark:text-white/50">
|
||||||
|
Reason:
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="NSFW..."
|
||||||
|
value={warning.reason}
|
||||||
|
onChange={(e) =>
|
||||||
|
setWarning((prev) => ({ ...prev, reason: e.target.value }))
|
||||||
|
}
|
||||||
|
className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{difficulty.enable ? (
|
||||||
|
<div className="flex items-center w-full px-4 border-t h-11 shrink-0 border-black/5 dark:border-white/5">
|
||||||
|
<span className="text-sm shrink-0 text-black/50 dark:text-white/50">
|
||||||
|
Difficulty:
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="[0-9]"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (!/[0-9]/.test(event.key)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="21"
|
||||||
|
defaultValue={difficulty.num}
|
||||||
|
onChange={(e) =>
|
||||||
|
setWarning((prev) => ({ ...prev, num: Number(e.target.value) }))
|
||||||
|
}
|
||||||
|
className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="flex items-center w-full h-16 gap-4 px-4 border-t divide-x divide-black/5 dark:divide-white/5 shrink-0 border-black/5 dark:border-white/5"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => publish()}
|
||||||
|
className="inline-flex items-center justify-center h-8 gap-1 px-2.5 text-sm font-medium rounded-lg bg-black/10 w-max hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
) : (
|
||||||
|
<Feather className="size-4" weight="fill" />
|
||||||
|
)}
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
|
<div className="inline-flex items-center flex-1 gap-2 pl-4">
|
||||||
|
<MediaButton setText={setText} setAttaches={setAttaches} />
|
||||||
|
<WarningButton setWarning={setWarning} />
|
||||||
|
<PowButton setDifficulty={setDifficulty} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChildNote({ id }: { id: string }) {
|
function Menu({
|
||||||
|
users,
|
||||||
|
index,
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
insert,
|
||||||
|
}: {
|
||||||
|
users: Mention[];
|
||||||
|
index: number;
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
insert: (index: number) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
top: top,
|
||||||
|
left: left,
|
||||||
|
}}
|
||||||
|
className="fixed w-[200px] text-sm bg-white dark:bg-black shadow-lg shadow-neutral-500/20 dark:shadow-none dark:ring-1 dark:ring-neutral-700 rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
{users.map((u, i) => (
|
||||||
|
<div
|
||||||
|
key={u.pubkey}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-1.5 p-2",
|
||||||
|
index === i ? "bg-neutral-100 dark:bg-neutral-900" : null,
|
||||||
|
)}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
insert(i);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="size-7 shrink-0">
|
||||||
|
{u.avatar?.length ? (
|
||||||
|
<img
|
||||||
|
src={u.avatar}
|
||||||
|
className="size-7 rounded-full outline outline-1 -outline-offset-1 outline-black/15"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="size-7 rounded-full bg-blue-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{u.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmbedNote({ id }: { id: string }) {
|
||||||
const { isLoading, isError, data } = useEvent(id);
|
const { isLoading, isError, data } = useEvent(id);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -258,142 +371,3 @@ function ChildNote({ id }: { id: string }) {
|
|||||||
</Note.Provider>
|
</Note.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const withNostrEvent = (editor: ReactEditor) => {
|
|
||||||
const { insertData, isVoid } = editor;
|
|
||||||
|
|
||||||
editor.isVoid = (element) => {
|
|
||||||
// @ts-expect-error, wtf
|
|
||||||
return element.type === "event" ? true : isVoid(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.insertData = (data) => {
|
|
||||||
const text = data.getData("text/plain");
|
|
||||||
|
|
||||||
if (text.startsWith("nevent") || text.startsWith("note")) {
|
|
||||||
insertNostrEvent(editor, text);
|
|
||||||
} else {
|
|
||||||
insertData(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return editor;
|
|
||||||
};
|
|
||||||
|
|
||||||
const withMentions = (editor: ReactEditor) => {
|
|
||||||
const { isInline, isVoid, markableVoid } = editor;
|
|
||||||
|
|
||||||
editor.isInline = (element) => {
|
|
||||||
// @ts-expect-error, wtf
|
|
||||||
return element.type === "mention" ? true : isInline(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.isVoid = (element) => {
|
|
||||||
// @ts-expect-error, wtf
|
|
||||||
return element.type === "mention" ? true : isVoid(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.markableVoid = (element) => {
|
|
||||||
// @ts-expect-error, wtf
|
|
||||||
return element.type === "mention" || markableVoid(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
return editor;
|
|
||||||
};
|
|
||||||
|
|
||||||
const withImages = (editor: ReactEditor) => {
|
|
||||||
const { insertData, isVoid } = editor;
|
|
||||||
|
|
||||||
editor.isVoid = (element) => {
|
|
||||||
// @ts-expect-error, wtf
|
|
||||||
return element.type === "image" ? true : isVoid(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.insertData = (data) => {
|
|
||||||
const text = data.getData("text/plain");
|
|
||||||
|
|
||||||
if (isImageUrl(text)) {
|
|
||||||
insertImage(editor, text);
|
|
||||||
} else {
|
|
||||||
insertData(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return editor;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Image = ({ attributes, element, children }) => {
|
|
||||||
const editor = useSlateStatic();
|
|
||||||
const selected = useSelected();
|
|
||||||
const focused = useFocused();
|
|
||||||
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...attributes}>
|
|
||||||
{children}
|
|
||||||
<img
|
|
||||||
src={element.url}
|
|
||||||
alt={element.url}
|
|
||||||
className={cn(
|
|
||||||
"my-2 h-auto w-1/2 rounded-lg object-cover ring-2 outline outline-1 -outline-offset-1 outline-black/15",
|
|
||||||
selected && focused ? "ring-blue-500" : "ring-transparent",
|
|
||||||
)}
|
|
||||||
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
|
||||||
onKeyDown={() => Transforms.removeNodes(editor, { at: path })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Mention = ({ attributes, element }) => {
|
|
||||||
const editor = useSlateStatic();
|
|
||||||
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
{...attributes}
|
|
||||||
type="button"
|
|
||||||
contentEditable={false}
|
|
||||||
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
|
||||||
className="inline-block text-blue-500 align-baseline hover:text-blue-600"
|
|
||||||
>{`@${element.name}`}</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Event = ({ attributes, element, children }) => {
|
|
||||||
const editor = useSlateStatic();
|
|
||||||
const path = ReactEditor.findPath(editor as ReactEditor, element);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...attributes}>
|
|
||||||
{children}
|
|
||||||
<div
|
|
||||||
contentEditable={false}
|
|
||||||
className="relative my-2 user-select-none"
|
|
||||||
onClick={() => Transforms.removeNodes(editor, { at: path })}
|
|
||||||
onKeyDown={() => Transforms.removeNodes(editor, { at: path })}
|
|
||||||
>
|
|
||||||
<MentionNote eventId={element.eventId} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Element = (props) => {
|
|
||||||
const { attributes, children, element } = props;
|
|
||||||
|
|
||||||
switch (element.type) {
|
|
||||||
case "image":
|
|
||||||
return <Image {...props} />;
|
|
||||||
case "mention":
|
|
||||||
return <Mention {...props} />;
|
|
||||||
case "event":
|
|
||||||
return <Event {...props} />;
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<p {...attributes} className="text-[15px]">
|
|
||||||
{children}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
+52
-38
@@ -2,7 +2,7 @@ import { commands } from "@/commands.gen";
|
|||||||
import { appSettings, displayNpub } from "@/commons";
|
import { appSettings, displayNpub } from "@/commons";
|
||||||
import { Frame, Spinner, User } from "@/components";
|
import { Frame, Spinner, User } from "@/components";
|
||||||
import { ArrowRight, DotsThree, GearSix, Plus } from "@phosphor-icons/react";
|
import { ArrowRight, DotsThree, GearSix, Plus } from "@phosphor-icons/react";
|
||||||
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
import {
|
import {
|
||||||
@@ -37,6 +37,32 @@ function Screen() {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const showContextMenu = useCallback(
|
||||||
|
async (e: React.MouseEvent, account: string) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const menuItems = await Promise.all([
|
||||||
|
MenuItem.new({
|
||||||
|
text: "Reset password",
|
||||||
|
enabled: !account.includes("_nostrconnect"),
|
||||||
|
// @ts-ignore, this is tanstack router bug
|
||||||
|
action: () => navigate({ to: "/reset", search: { account } }),
|
||||||
|
}),
|
||||||
|
MenuItem.new({
|
||||||
|
text: "Delete account",
|
||||||
|
action: async () => await deleteAccount(account),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const menu = await Menu.new({
|
||||||
|
items: menuItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
await menu.popup().catch((e) => console.error(e));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const deleteAccount = async (account: string) => {
|
const deleteAccount = async (account: string) => {
|
||||||
const res = await commands.deleteAccount(account);
|
const res = await commands.deleteAccount(account);
|
||||||
|
|
||||||
@@ -64,11 +90,23 @@ function Screen() {
|
|||||||
appSettings.setState(() => settings.data);
|
appSettings.setState(() => settings.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate({
|
const status = await commands.isAccountSync(res.data);
|
||||||
to: "/$account/home",
|
|
||||||
params: { account: res.data },
|
if (status) {
|
||||||
replace: true,
|
navigate({
|
||||||
});
|
to: "/$account/home",
|
||||||
|
// @ts-ignore, this is tanstack router bug
|
||||||
|
params: { account: res.data },
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
navigate({
|
||||||
|
to: "/loading",
|
||||||
|
// @ts-ignore, this is tanstack router bug
|
||||||
|
search: { account: res.data },
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await message(res.error, { title: "Login", kind: "error" });
|
await message(res.error, { title: "Login", kind: "error" });
|
||||||
return;
|
return;
|
||||||
@@ -76,31 +114,6 @@ function Screen() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const showContextMenu = useCallback(
|
|
||||||
async (e: React.MouseEvent, account: string) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const menuItems = await Promise.all([
|
|
||||||
MenuItem.new({
|
|
||||||
text: "Reset password",
|
|
||||||
enabled: !account.includes("_nostrconnect"),
|
|
||||||
action: () => navigate({ to: "/reset", search: { account } }),
|
|
||||||
}),
|
|
||||||
MenuItem.new({
|
|
||||||
text: "Delete account",
|
|
||||||
action: async () => await deleteAccount(account),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const menu = await Menu.new({
|
|
||||||
items: menuItems,
|
|
||||||
});
|
|
||||||
|
|
||||||
await menu.popup().catch((e) => console.error(e));
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoLogin) {
|
if (autoLogin) {
|
||||||
loginWith();
|
loginWith();
|
||||||
@@ -147,8 +160,9 @@ function Screen() {
|
|||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") loginWith();
|
if (e.key === "Enter") loginWith();
|
||||||
}}
|
}}
|
||||||
|
disabled={isPending}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
className="px-3 rounded-full w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400 dark:placeholder:text-neutral-600"
|
className="px-3 rounded-full w-full h-10 bg-transparent border border-neutral-200 dark:border-neutral-500 focus:border-blue-500 focus:outline-none placeholder:text-neutral-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -193,8 +207,8 @@ function Screen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Link
|
<a
|
||||||
to="/new"
|
href="/new"
|
||||||
className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5"
|
className="flex items-center justify-between hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2.5 p-3">
|
<div className="flex items-center gap-2.5 p-3">
|
||||||
@@ -205,17 +219,17 @@ function Screen() {
|
|||||||
New account
|
New account
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</a>
|
||||||
</Frame>
|
</Frame>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-2 right-2">
|
<div className="absolute bottom-2 right-2">
|
||||||
<Link
|
<a
|
||||||
to="/bootstrap-relays"
|
href="/bootstrap-relays"
|
||||||
className="h-8 w-max text-xs px-3 inline-flex items-center justify-center gap-1.5 bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 rounded-full"
|
className="h-8 w-max text-xs px-3 inline-flex items-center justify-center gap-1.5 bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 rounded-full"
|
||||||
>
|
>
|
||||||
<GearSix className="size-4" />
|
<GearSix className="size-4" />
|
||||||
Manage Relays
|
Manage Relays
|
||||||
</Link>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export const Route = createFileRoute("/")({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { accounts };
|
return {
|
||||||
|
accounts: accounts.filter((account) => !account.endsWith("Lume")),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Frame, Spinner } from "@/components";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
type RouteSearch = {
|
||||||
|
account: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/loading")({
|
||||||
|
validateSearch: (search: Record<string, string>): RouteSearch => {
|
||||||
|
return {
|
||||||
|
account: search.account,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const navigate = Route.useNavigate();
|
||||||
|
const search = Route.useSearch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisten = listen("synchronized", () => {
|
||||||
|
navigate({
|
||||||
|
to: "/$account/home",
|
||||||
|
// @ts-ignore, this is tanstack router bug
|
||||||
|
params: { account: search.account },
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((f) => f());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="size-full flex items-center justify-center">
|
||||||
|
<Frame
|
||||||
|
className="p-6 h-36 flex flex-col gap-2 items-center justify-center text-center rounded-xl overflow-hidden"
|
||||||
|
shadow
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
<p className="text-sm text-neutral-600 dark:text-neutral-40">
|
||||||
|
Fetching necessary data for the first time login...
|
||||||
|
</p>
|
||||||
|
</Frame>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+62
-27
@@ -1,6 +1,4 @@
|
|||||||
import { GoBack } from "@/components";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { ArrowLeft } from "@phosphor-icons/react";
|
|
||||||
import { Link, createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/new")({
|
export const Route = createLazyFileRoute("/new")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
@@ -10,42 +8,79 @@ function Screen() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="relative size-full flex items-center justify-center"
|
className="bg-white/50 dark:bg-black/50 relative size-full flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<div className="w-[320px] flex flex-col gap-8">
|
<div className="w-[350px] flex flex-col gap-8">
|
||||||
<div className="flex flex-col gap-1 text-center">
|
<div className="flex flex-col gap-1 text-center">
|
||||||
<h1 className="leading-tight text-xl font-semibold">
|
<h1 className="leading-tight text-lg font-semibold">
|
||||||
Welcome to Nostr.
|
How would you like to use Lume?
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Link
|
<a
|
||||||
to="/auth/new"
|
href="/auth/connect"
|
||||||
className="w-full h-10 bg-blue-500 font-medium hover:bg-blue-600 text-white rounded-lg inline-flex items-center justify-center shadow"
|
className="w-full p-4 rounded-xl hover:shadow-lg hover:ring-0 hover:bg-white dark:hover:bg-neutral-900 ring-1 ring-black/5 dark:ring-white/5"
|
||||||
>
|
>
|
||||||
Create a new identity
|
<h3 className="mb-1.5 font-medium">Continue with Nostr Connect</h3>
|
||||||
</Link>
|
<div className="text-sm">
|
||||||
<div className="w-full h-px bg-black/5 dark:bg-white/5" />
|
<p className="text-neutral-500 dark:text-neutral-600">
|
||||||
|
Your account will be handled by a remote signer. Lume will not
|
||||||
|
store your account keys.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/auth/import"
|
||||||
|
className="w-full p-4 rounded-xl hover:shadow-lg hover:ring-0 hover:bg-white dark:hover:bg-neutral-900 ring-1 ring-black/5 dark:ring-white/5"
|
||||||
|
>
|
||||||
|
<h3 className="mb-1.5 font-medium">Continue with Secret Key</h3>
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="text-neutral-500 dark:text-neutral-600">
|
||||||
|
Lume will store your keys in secure storage. You can provide a
|
||||||
|
password to add extra security.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex-1 h-px bg-black/5 dark:bg-white/5" />
|
||||||
|
<div className="shrink-0 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
Do you not have a Nostr account yet?
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 h-px bg-black/5 dark:bg-white/5" />
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Link
|
<a
|
||||||
to="/auth/connect"
|
href="https://nsec.app"
|
||||||
className="w-full h-10 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-100 dark:text-black rounded-lg inline-flex items-center justify-center"
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-sm bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 rounded-lg flex items-center gap-1.5 h-9 px-1"
|
||||||
>
|
>
|
||||||
Login with Nostr Connect
|
<div className="size-7 rounded-md bg-black inline-flex items-center justify-center">
|
||||||
</Link>
|
<img src="/nsec_app.svg" alt="nsec.app" className="size-5" />
|
||||||
<Link
|
</div>
|
||||||
to="/auth/import"
|
Create one with nsec.app
|
||||||
className="w-full h-10 bg-white hover:bg-neutral-100 dark:hover:bg-neutral-100 dark:text-black rounded-lg inline-flex items-center justify-center"
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://nosta.me"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-sm bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 rounded-lg flex items-center gap-1.5 h-9 px-1"
|
||||||
>
|
>
|
||||||
Login with Private Key
|
<div className="size-7 rounded-md bg-black overflow-hidden">
|
||||||
</Link>
|
<img
|
||||||
|
src="/nosta.jpg"
|
||||||
|
alt="nosta"
|
||||||
|
className="size-7 object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
Create one with nosta.me
|
||||||
|
</a>
|
||||||
|
<p className="text-xs text-neutral-400 dark:text-neutral-600">
|
||||||
|
Or you can create account from other Nostr clients.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GoBack className="fixed top-11 left-2 flex items-center gap-1.5 text-sm font-medium">
|
|
||||||
<ArrowLeft className="size-5" />
|
|
||||||
Back
|
|
||||||
</GoBack>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ export function useEvent(id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(relayHint);
|
|
||||||
|
|
||||||
// Build query
|
// Build query
|
||||||
if (relayHint?.length) {
|
if (relayHint?.length) {
|
||||||
query = await commands.getEventFrom(normalizeId, relayHint);
|
query = await commands.getEventFrom(normalizeId, relayHint);
|
||||||
@@ -56,7 +54,7 @@ export function useEvent(id: string) {
|
|||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
staleTime: Number.POSITIVE_INFINITY,
|
staleTime: Number.POSITIVE_INFINITY,
|
||||||
retry: false,
|
retry: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { isLoading, isError, error, data };
|
return { isLoading, isError, error, data };
|
||||||
|
|||||||
@@ -17,18 +17,15 @@ export function useProfile(pubkey: string, embed?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let normalizeId = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
let normalizeId = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||||
let relayHint: string;
|
|
||||||
|
|
||||||
if (normalizeId.startsWith("nprofile")) {
|
if (normalizeId.startsWith("nprofile")) {
|
||||||
const decoded = nip19.decode(normalizeId);
|
const decoded = nip19.decode(normalizeId);
|
||||||
|
|
||||||
if (decoded.type === "nprofile") {
|
if (decoded.type === "nprofile") {
|
||||||
relayHint = decoded.data.relays[0];
|
|
||||||
normalizeId = decoded.data.pubkey;
|
normalizeId = decoded.data.pubkey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(relayHint);
|
|
||||||
const query = await commands.getProfile(normalizeId);
|
const query = await commands.getProfile(normalizeId);
|
||||||
|
|
||||||
if (query.status === "ok") {
|
if (query.status === "ok") {
|
||||||
@@ -41,7 +38,7 @@ export function useProfile(pubkey: string, embed?: string) {
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
staleTime: Number.POSITIVE_INFINITY,
|
staleTime: Number.POSITIVE_INFINITY,
|
||||||
retry: false,
|
retry: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { isLoading, isError, profile };
|
return { isLoading, isError, profile };
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const LumeWindow = {
|
|||||||
eTags.find((el) => el[3] === "reply")?.[1] ?? eTags[1]?.[1];
|
eTags.find((el) => el[3] === "reply")?.[1] ?? eTags[1]?.[1];
|
||||||
|
|
||||||
const url = `/columns/events/${root ?? reply ?? event.id}`;
|
const url = `/columns/events/${root ?? reply ?? event.id}`;
|
||||||
const label = `event-${root ?? reply ?? event.id}`;
|
const label = `event-${root?.substring(0, 6) ?? reply?.substring(0, 6) ?? event.id.substring(0, 6)}`;
|
||||||
|
|
||||||
LumeWindow.openColumn({ label, url, name: "Thread" });
|
LumeWindow.openColumn({ label, url, name: "Thread" });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export enum Kind {
|
|||||||
export interface Meta {
|
export interface Meta {
|
||||||
content: string;
|
content: string;
|
||||||
images: string[];
|
images: string[];
|
||||||
videos: string[];
|
|
||||||
events: string[];
|
events: string[];
|
||||||
mentions: string[];
|
mentions: string[];
|
||||||
hashtags: string[];
|
hashtags: string[];
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
outDir: "./dist",
|
outDir: "./dist",
|
||||||
|
target: "esnext",
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user