Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
790fee6c05 | ||
|
|
9f5b14956a | ||
|
|
e04497d841 | ||
|
|
106c627ec4 | ||
|
|
c40762cc04 | ||
|
|
d2b5ae0507 | ||
|
|
8c6aea8050 | ||
|
|
090a815f99 | ||
|
|
d841163ba7 | ||
|
|
8398ae80d3 | ||
|
|
fe60f75e96 | ||
|
|
e098743d43 |
@@ -1,63 +1,30 @@
|
|||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Lume is a Nostr client for desktop include Linux, Windows and macOS. It is free and open source, you can look at source code on Github. Lume is actively improving the app and adding new features, you can expect new update every month.
|
Lume is a Nostr client for macOS and Windows 11. It is free and open source, you can look at source code on Github. Lume is actively improving the app and adding new features, you can expect new update every month.
|
||||||
|
|
||||||
## Usage
|
## Installation and Usage
|
||||||
|
|
||||||
Download Lume v4 for your platform here: [https://github.com/lumehq/lume/releases](https://github.com/lumehq/lume/releases)
|
- *Microsoft Windows*: See the releases area for a file named something like Lume_VERSION_x64-setup.exe or Lume_VERSION_x64_en-US.msi
|
||||||
|
|
||||||
Supported platform: macOS. Windows and Linux are coming soon.
|
- *macOS*: See the releases area for a file named something like Lume_VERSION_PLATFORM.dmg
|
||||||
|
|
||||||
Windows and Linux are availabel on v3 and below.
|
Lume only supported macOS and Windows 11. Linux user can consider using [Gossip client](https://github.com/mikedilger/gossip)
|
||||||
|
|
||||||
## Prerequisites
|
## Screenshots
|
||||||
|
|
||||||
- Node.js >= 18: https://nodejs.org/en
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
- Rust: https://rustup.rs/
|
## Building from Source
|
||||||
|
|
||||||
- PNPM: https://pnpm.io
|
See [Developing](docs/DEVELOPING.md)
|
||||||
|
|
||||||
- Tauri v2: https://beta.tauri.app/guides/prerequisites/
|
|
||||||
|
|
||||||
## Develop
|
|
||||||
|
|
||||||
Clone project
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/lumehq/lume.git && cd lume
|
|
||||||
```
|
|
||||||
|
|
||||||
Install packages
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Run dev build
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm tauri dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Generate production build
|
|
||||||
|
|
||||||
```
|
|
||||||
pnpm tauri build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Nix
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
1. [Install Nix](https://zero-to-flakes.com/install)
|
|
||||||
1. [Setup `direnv`](https://zero-to-flakes.com/direnv)
|
|
||||||
|
|
||||||
`cd` into the root folder of the project to enter `nix develop` shell. Run `direnv allow` (only once). Then run `pnpm` or `bun` (experimental) commands as described above.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (C) 2023-2024 Ren Amamiya & other Lume contributors (see AUTHORS.md)
|
Copyright (C) 2023-2024 Ren Amamiya & other Lume contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Developing
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js >= 20: https://nodejs.org/
|
||||||
|
|
||||||
|
- Rust: https://rustup.rs/
|
||||||
|
|
||||||
|
- PNPM: https://pnpm.io/
|
||||||
|
|
||||||
|
- Tauri: https://tauri.app/guides/prerequisites/
|
||||||
|
|
||||||
|
## Build from source
|
||||||
|
|
||||||
|
Clone project
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/lumehq/lume.git && cd lume
|
||||||
|
```
|
||||||
|
|
||||||
|
Install required dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Run dev
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Build
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm tauri build
|
||||||
|
```
|
||||||
+36
-37
@@ -12,37 +12,36 @@
|
|||||||
"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.56.2",
|
"@tanstack/query-persist-client-core": "^5.59.0",
|
||||||
"@tanstack/react-query": "^5.56.2",
|
"@tanstack/react-query": "^5.59.0",
|
||||||
"@tanstack/react-router": "^1.58.3",
|
"@tanstack/react-router": "^1.63.5",
|
||||||
"@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.4",
|
"@tauri-apps/api": "^2.0.2",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.1",
|
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": "2.0.0-rc.1",
|
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||||
"@tauri-apps/plugin-fs": "2.0.0-rc.2",
|
"@tauri-apps/plugin-fs": "^2.0.0",
|
||||||
"@tauri-apps/plugin-http": "2.0.0-rc.2",
|
"@tauri-apps/plugin-http": "^2.0.0",
|
||||||
"@tauri-apps/plugin-os": "2.0.0-rc.1",
|
"@tauri-apps/plugin-os": "^2.0.0",
|
||||||
"@tauri-apps/plugin-process": "2.0.0-rc.1",
|
"@tauri-apps/plugin-process": "^2.0.0",
|
||||||
"@tauri-apps/plugin-shell": "2.0.0-rc.1",
|
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||||
"@tauri-apps/plugin-store": "2.0.0-rc.1",
|
"@tauri-apps/plugin-store": "^2.0.0",
|
||||||
"@tauri-apps/plugin-updater": "2.0.0-rc.1",
|
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||||
"@tauri-apps/plugin-upload": "2.0.0-rc.1",
|
"@tauri-apps/plugin-upload": "^2.0.0",
|
||||||
"@tauri-apps/plugin-window-state": "2.0.0-rc.1",
|
"@tauri-apps/plugin-window-state": "^2.0.0",
|
||||||
"bitcoin-units": "^1.0.0",
|
"bitcoin-units": "^1.0.0",
|
||||||
"boring-avatars": "^1.11.2",
|
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"i18next": "^23.15.1",
|
"i18next": "^23.15.2",
|
||||||
"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",
|
||||||
@@ -54,30 +53,30 @@
|
|||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
"rich-textarea": "^0.26.3",
|
"rich-textarea": "^0.26.3",
|
||||||
"use-debounce": "^10.0.3",
|
"use-debounce": "^10.0.3",
|
||||||
"virtua": "^0.33.7"
|
"virtua": "^0.34.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.2",
|
"@biomejs/biome": "^1.9.3",
|
||||||
"@evilmartians/harmony": "^1.2.0",
|
"@evilmartians/harmony": "^1.2.0",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tanstack/router-devtools": "^1.58.3",
|
"@tanstack/router-devtools": "^1.63.5",
|
||||||
"@tanstack/router-plugin": "^1.58.4",
|
"@tanstack/router-plugin": "^1.63.5",
|
||||||
"@tauri-apps/cli": "2.0.0-rc.8",
|
"@tauri-apps/cli": "^2.0.2",
|
||||||
"@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.47",
|
"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.3",
|
||||||
"tailwind-scrollbar": "^3.1.0",
|
"tailwind-scrollbar": "^3.1.0",
|
||||||
"tailwindcss": "^3.4.12",
|
"tailwindcss": "^3.4.13",
|
||||||
"tailwindcss-content-visibility": "^0.2.0",
|
"tailwindcss-content-visibility": "^1.0.0",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.8",
|
||||||
"vite-tsconfig-paths": "^5.0.1"
|
"vite-tsconfig-paths": "^5.0.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
Generated
+614
-600
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 |
Binary file not shown.
|
Before Width: | Height: | Size: 171 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 201 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 170 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
Generated
+570
-192
File diff suppressed because it is too large
Load Diff
+15
-15
@@ -8,29 +8,29 @@ 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]
|
||||||
tauri = { version = "2.0.0-rc", features = [
|
tauri = { version = "2.0.0", 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-decorum = { git = "https://github.com/clearlysid/tauri-plugin-decorum.git" }
|
tauri-plugin-decorum = { git = "https://github.com/clearlysid/tauri-plugin-decorum.git" }
|
||||||
tauri-plugin-prevent-default = "0.4"
|
tauri-plugin-prevent-default = "0.6"
|
||||||
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-sdk = { git = "https://github.com/rust-nostr/nostr", features = ["lmdb"] }
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
"zap-*",
|
"zap-*",
|
||||||
"event-*",
|
"event-*",
|
||||||
"user-*",
|
"user-*",
|
||||||
"editor-*"
|
"editor-*",
|
||||||
|
"popup-*"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:path:default",
|
"core:path:default",
|
||||||
@@ -60,9 +61,7 @@
|
|||||||
"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 @@
|
|||||||
{"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: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-*","popup-*"],"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"]}}
|
||||||
@@ -1821,72 +1821,122 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"allow": {
|
"allow": {
|
||||||
"items": {
|
"items": {
|
||||||
"title": "Entry",
|
"title": "ShellScopeEntry",
|
||||||
"description": "A command allowed to be executed by the webview API.",
|
"description": "Shell scope entry.",
|
||||||
"type": "object",
|
"anyOf": [
|
||||||
"required": [
|
{
|
||||||
"args",
|
"type": "object",
|
||||||
"cmd",
|
"required": [
|
||||||
"name",
|
"cmd",
|
||||||
"sidecar"
|
"name"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"args": {
|
"args": {
|
||||||
"description": "The allowed arguments for the command execution.",
|
"description": "The allowed arguments for the command execution.",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/ShellAllowedArgs"
|
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cmd": {
|
||||||
|
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"cmd": {
|
{
|
||||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
"type": "object",
|
||||||
"type": "string"
|
"required": [
|
||||||
},
|
"name",
|
||||||
"name": {
|
"sidecar"
|
||||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
],
|
||||||
"type": "string"
|
"properties": {
|
||||||
},
|
"args": {
|
||||||
"sidecar": {
|
"description": "The allowed arguments for the command execution.",
|
||||||
"description": "If this command is a sidecar command.",
|
"allOf": [
|
||||||
"type": "boolean"
|
{
|
||||||
|
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sidecar": {
|
||||||
|
"description": "If this command is a sidecar command.",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deny": {
|
"deny": {
|
||||||
"items": {
|
"items": {
|
||||||
"title": "Entry",
|
"title": "ShellScopeEntry",
|
||||||
"description": "A command allowed to be executed by the webview API.",
|
"description": "Shell scope entry.",
|
||||||
"type": "object",
|
"anyOf": [
|
||||||
"required": [
|
{
|
||||||
"args",
|
"type": "object",
|
||||||
"cmd",
|
"required": [
|
||||||
"name",
|
"cmd",
|
||||||
"sidecar"
|
"name"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"args": {
|
"args": {
|
||||||
"description": "The allowed arguments for the command execution.",
|
"description": "The allowed arguments for the command execution.",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/ShellAllowedArgs"
|
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cmd": {
|
||||||
|
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"cmd": {
|
{
|
||||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
"type": "object",
|
||||||
"type": "string"
|
"required": [
|
||||||
},
|
"name",
|
||||||
"name": {
|
"sidecar"
|
||||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
],
|
||||||
"type": "string"
|
"properties": {
|
||||||
},
|
"args": {
|
||||||
"sidecar": {
|
"description": "The allowed arguments for the command execution.",
|
||||||
"description": "If this command is a sidecar command.",
|
"allOf": [
|
||||||
"type": "boolean"
|
{
|
||||||
|
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sidecar": {
|
||||||
|
"description": "If this command is a sidecar command.",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5394,6 +5444,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "store:allow-clear"
|
"const": "store:allow-clear"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the create_store command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "store:allow-create-store"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the delete command without any pre-configured scope.",
|
"description": "Enables the delete command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -5454,6 +5509,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "store:deny-clear"
|
"const": "store:deny-clear"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the create_store command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "store:deny-create-store"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the delete command without any pre-configured scope.",
|
"description": "Denies the delete command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -5710,7 +5770,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ShellAllowedArg": {
|
"ShellScopeEntryAllowedArg": {
|
||||||
"description": "A command argument allowed to be executed by the webview API.",
|
"description": "A command argument allowed to be executed by the webview API.",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -5738,18 +5798,18 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ShellAllowedArgs": {
|
"ShellScopeEntryAllowedArgs": {
|
||||||
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
|
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
|
"description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.",
|
"description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/ShellAllowedArg"
|
"$ref": "#/definitions/ShellScopeEntryAllowedArg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1821,72 +1821,122 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"allow": {
|
"allow": {
|
||||||
"items": {
|
"items": {
|
||||||
"title": "Entry",
|
"title": "ShellScopeEntry",
|
||||||
"description": "A command allowed to be executed by the webview API.",
|
"description": "Shell scope entry.",
|
||||||
"type": "object",
|
"anyOf": [
|
||||||
"required": [
|
{
|
||||||
"args",
|
"type": "object",
|
||||||
"cmd",
|
"required": [
|
||||||
"name",
|
"cmd",
|
||||||
"sidecar"
|
"name"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"args": {
|
"args": {
|
||||||
"description": "The allowed arguments for the command execution.",
|
"description": "The allowed arguments for the command execution.",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/ShellAllowedArgs"
|
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cmd": {
|
||||||
|
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"cmd": {
|
{
|
||||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
"type": "object",
|
||||||
"type": "string"
|
"required": [
|
||||||
},
|
"name",
|
||||||
"name": {
|
"sidecar"
|
||||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
],
|
||||||
"type": "string"
|
"properties": {
|
||||||
},
|
"args": {
|
||||||
"sidecar": {
|
"description": "The allowed arguments for the command execution.",
|
||||||
"description": "If this command is a sidecar command.",
|
"allOf": [
|
||||||
"type": "boolean"
|
{
|
||||||
|
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sidecar": {
|
||||||
|
"description": "If this command is a sidecar command.",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deny": {
|
"deny": {
|
||||||
"items": {
|
"items": {
|
||||||
"title": "Entry",
|
"title": "ShellScopeEntry",
|
||||||
"description": "A command allowed to be executed by the webview API.",
|
"description": "Shell scope entry.",
|
||||||
"type": "object",
|
"anyOf": [
|
||||||
"required": [
|
{
|
||||||
"args",
|
"type": "object",
|
||||||
"cmd",
|
"required": [
|
||||||
"name",
|
"cmd",
|
||||||
"sidecar"
|
"name"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"args": {
|
"args": {
|
||||||
"description": "The allowed arguments for the command execution.",
|
"description": "The allowed arguments for the command execution.",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/ShellAllowedArgs"
|
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cmd": {
|
||||||
|
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"cmd": {
|
{
|
||||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
"type": "object",
|
||||||
"type": "string"
|
"required": [
|
||||||
},
|
"name",
|
||||||
"name": {
|
"sidecar"
|
||||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
],
|
||||||
"type": "string"
|
"properties": {
|
||||||
},
|
"args": {
|
||||||
"sidecar": {
|
"description": "The allowed arguments for the command execution.",
|
||||||
"description": "If this command is a sidecar command.",
|
"allOf": [
|
||||||
"type": "boolean"
|
{
|
||||||
|
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sidecar": {
|
||||||
|
"description": "If this command is a sidecar command.",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5394,6 +5444,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "store:allow-clear"
|
"const": "store:allow-clear"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the create_store command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "store:allow-create-store"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the delete command without any pre-configured scope.",
|
"description": "Enables the delete command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -5454,6 +5509,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "store:deny-clear"
|
"const": "store:deny-clear"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the create_store command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "store:deny-create-store"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the delete command without any pre-configured scope.",
|
"description": "Denies the delete command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -5710,7 +5770,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ShellAllowedArg": {
|
"ShellScopeEntryAllowedArg": {
|
||||||
"description": "A command argument allowed to be executed by the webview API.",
|
"description": "A command argument allowed to be executed by the webview API.",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -5738,18 +5798,18 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ShellAllowedArgs": {
|
"ShellScopeEntryAllowedArgs": {
|
||||||
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
|
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
|
"description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.",
|
"description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/ShellAllowedArg"
|
"$ref": "#/definitions/ShellScopeEntryAllowedArg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,18 +11,18 @@
|
|||||||
{
|
{
|
||||||
"default": true,
|
"default": true,
|
||||||
"official": true,
|
"official": true,
|
||||||
"label": "columns_gallery",
|
"label": "Launchpad",
|
||||||
"name": "Columns Gallery",
|
"name": "Launchpad",
|
||||||
"description": "Expand your experiences.",
|
"description": "Expand your experiences.",
|
||||||
"url": "/columns/gallery",
|
"url": "/columns/launchpad",
|
||||||
"picture": ""
|
"picture": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": false,
|
"default": false,
|
||||||
"official": true,
|
"official": true,
|
||||||
"label": "local_feeds",
|
"label": "newsfeed",
|
||||||
"name": "Local Feeds",
|
"name": "Newsfeed",
|
||||||
"description": "All notes from your follows.",
|
"description": "All notes from you're following.",
|
||||||
"url": "/columns/newsfeed",
|
"url": "/columns/newsfeed",
|
||||||
"picture": ""
|
"picture": ""
|
||||||
},
|
},
|
||||||
@@ -58,19 +58,10 @@
|
|||||||
"official": true,
|
"official": true,
|
||||||
"label": "global_feeds",
|
"label": "global_feeds",
|
||||||
"name": "Global Feeds",
|
"name": "Global Feeds",
|
||||||
"description": "Discover all global notes.",
|
"description": "All global notes from all connected relays.",
|
||||||
"url": "/columns/global",
|
"url": "/columns/global",
|
||||||
"picture": ""
|
"picture": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": false,
|
|
||||||
"official": true,
|
|
||||||
"label": "group_feeds",
|
|
||||||
"name": "Group",
|
|
||||||
"description": "Custom feeds for group of people.",
|
|
||||||
"url": "/columns/group",
|
|
||||||
"picture": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": false,
|
"default": false,
|
||||||
"official": true,
|
"official": true,
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
wss://relay.damus.io,
|
wss://relay.damus.io,
|
||||||
wss://relay.nostr.net,
|
wss://relay.nostr.net,
|
||||||
|
wss://nostr.fmt.wiz.biz,
|
||||||
|
wss://offchain.pub,
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ use std::{
|
|||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tauri::{Emitter, Manager, State};
|
use tauri::{Emitter, Manager, State};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{get_user_settings, init_nip65},
|
common::{get_latest_event, init_nip65},
|
||||||
Nostr, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT, NOTIFICATION_SUB_ID,
|
Nostr, NOTIFICATION_SUB_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||||
@@ -220,6 +221,17 @@ pub fn is_account_sync(id: String, handle: tauri::AppHandle) -> bool {
|
|||||||
fs::metadata(config_dir.join(id)).is_ok()
|
fs::metadata(config_dir.join(id)).is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn create_sync_file(id: String, handle: tauri::AppHandle) -> bool {
|
||||||
|
let config_dir = handle
|
||||||
|
.path()
|
||||||
|
.app_config_dir()
|
||||||
|
.expect("Error: app config directory not found.");
|
||||||
|
|
||||||
|
File::create(config_dir.join(id)).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
@@ -271,137 +283,155 @@ pub async fn login(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect to user's relay (NIP-65)
|
// NIP-65: Connect to user's relay list
|
||||||
init_nip65(client, &public_key).await;
|
init_nip65(client, &public_key).await;
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
// NIP-03: Get user's contact list
|
||||||
let config_dir = handle
|
let contact_list = {
|
||||||
.path()
|
if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await {
|
||||||
.app_config_dir()
|
state.contact_list.lock().await.clone_from(&contacts);
|
||||||
.expect("Error: app config directory not found.");
|
contacts
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let public_key_clone = public_key.clone();
|
||||||
|
|
||||||
|
// Run seperate thread for sync
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
let state = handle.state::<Nostr>();
|
let state = handle.state::<Nostr>();
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
let author = PublicKey::from_str(&public_key).unwrap();
|
||||||
|
|
||||||
let signer = client.signer().await.unwrap();
|
// Subscribe for new notification
|
||||||
let public_key = signer.public_key().await.unwrap();
|
if let Ok(e) = client
|
||||||
|
|
||||||
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
|
||||||
let notification = Filter::new().pubkey(public_key).kinds(vec![
|
|
||||||
Kind::TextNote,
|
|
||||||
Kind::Repost,
|
|
||||||
Kind::Reaction,
|
|
||||||
Kind::ZapReceipt,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Sync notification with negentropy
|
|
||||||
let _ = client
|
|
||||||
.reconcile(
|
|
||||||
notification.clone().limit(NOTIFICATION_NEG_LIMIT),
|
|
||||||
NegentropyOptions::default(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Subscribing for new notification...
|
|
||||||
if let Err(e) = client
|
|
||||||
.subscribe_with_id(
|
.subscribe_with_id(
|
||||||
notification_id,
|
SubscriptionId::new(NOTIFICATION_SUB_ID),
|
||||||
vec![notification.since(Timestamp::now())],
|
vec![Filter::new().pubkey(author).since(Timestamp::now())],
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
println!("Error: {}", e)
|
println!("Subscribed: {}", e.success.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's settings
|
// Get events from contact list
|
||||||
if let Ok(settings) = get_user_settings(client).await {
|
|
||||||
state.settings.lock().await.clone_from(&settings);
|
|
||||||
};
|
|
||||||
|
|
||||||
let contact_list = client
|
|
||||||
.get_contact_list(Some(Duration::from_secs(5)))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
state.contact_list.lock().await.clone_from(&contact_list);
|
|
||||||
|
|
||||||
// Get user's contact list
|
|
||||||
if !contact_list.is_empty() {
|
if !contact_list.is_empty() {
|
||||||
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
|
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
|
||||||
|
|
||||||
let metadata = Filter::new()
|
// Syncing all metadata events from contact list
|
||||||
.authors(authors.clone())
|
|
||||||
.kind(Kind::Metadata)
|
|
||||||
.limit(authors.len());
|
|
||||||
|
|
||||||
if let Ok(report) = client
|
if let Ok(report) = client
|
||||||
.reconcile(metadata, NegentropyOptions::default())
|
.reconcile(
|
||||||
|
Filter::new()
|
||||||
|
.authors(authors.clone())
|
||||||
|
.kinds(vec![Kind::Metadata, Kind::ContactList, Kind::EventDeletion])
|
||||||
|
.limit(authors.len() * 20),
|
||||||
|
NegentropyOptions::default(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
println!("received [metadata]: {}", report.received.len())
|
println!("Received: {}", report.received.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
let newsfeed = Filter::new()
|
// Syncing all events from contact list
|
||||||
.authors(authors.clone())
|
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
|
||||||
.limit(NEWSFEED_NEG_LIMIT);
|
|
||||||
|
|
||||||
if client
|
|
||||||
.reconcile(newsfeed, NegentropyOptions::default())
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
// Save state
|
|
||||||
let _ = File::create(config_dir.join(public_key.to_bech32().unwrap()));
|
|
||||||
// Update frontend
|
|
||||||
handle.emit("synchronized", ()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let contacts = Filter::new()
|
|
||||||
.authors(authors.clone())
|
|
||||||
.kind(Kind::ContactList)
|
|
||||||
.limit(authors.len() * 1000);
|
|
||||||
|
|
||||||
if let Ok(report) = client
|
if let Ok(report) = client
|
||||||
.reconcile(contacts, NegentropyOptions::default())
|
.reconcile(
|
||||||
|
Filter::new()
|
||||||
|
.authors(authors.clone())
|
||||||
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
|
.limit(authors.len() * 50),
|
||||||
|
NegentropyOptions::default(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
println!("received [contact list]: {}", report.received.len())
|
println!("Received: {}", report.received.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create the trusted public key list from contact list
|
||||||
|
// TODO: create a cached file
|
||||||
|
let mut trusted_list: HashSet<PublicKey> = HashSet::new();
|
||||||
|
|
||||||
for author in authors.into_iter() {
|
for author in authors.into_iter() {
|
||||||
|
trusted_list.insert(author);
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.author(author)
|
.author(author)
|
||||||
.kind(Kind::ContactList)
|
.kind(Kind::ContactList)
|
||||||
.limit(1);
|
.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 Ok(events) = client.database().query(vec![filter]).await {
|
||||||
if let Some(event) = events.into_iter().next() {
|
if let Some(event) = get_latest_event(&events) {
|
||||||
for tag in event.tags.into_iter() {
|
for tag in event.tags.iter() {
|
||||||
if let Some(TagStandard::PublicKey {
|
if let Some(TagStandard::PublicKey {
|
||||||
public_key,
|
public_key,
|
||||||
uppercase: false,
|
uppercase: false,
|
||||||
..
|
..
|
||||||
}) = tag.to_standardized()
|
}) = tag.to_owned().to_standardized()
|
||||||
{
|
{
|
||||||
list.push(public_key)
|
trusted_list.insert(public_key);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if !list.is_empty() {
|
|
||||||
circles.insert(author, list);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
handle.emit("synchronized", ()).unwrap();
|
// Update app's state
|
||||||
};
|
state.trusted_list.lock().await.clone_from(&trusted_list);
|
||||||
|
|
||||||
|
// Syncing all user's events
|
||||||
|
if let Ok(report) = client
|
||||||
|
.reconcile(Filter::new().author(author), NegentropyOptions::default())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
println!("Received: {}", report.received.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Syncing all tagged events for current user
|
||||||
|
if let Ok(report) = client
|
||||||
|
.reconcile(
|
||||||
|
Filter::new().pubkey(author).kinds(vec![
|
||||||
|
Kind::TextNote,
|
||||||
|
Kind::Repost,
|
||||||
|
Kind::Reaction,
|
||||||
|
Kind::ZapReceipt,
|
||||||
|
]),
|
||||||
|
NegentropyOptions::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
println!("Received: {}", report.received.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Syncing all events for trusted list
|
||||||
|
let trusted: Vec<PublicKey> = trusted_list.into_iter().collect();
|
||||||
|
if let Ok(report) = client
|
||||||
|
.reconcile(
|
||||||
|
Filter::new()
|
||||||
|
.authors(trusted)
|
||||||
|
.kinds(vec![
|
||||||
|
Kind::Metadata,
|
||||||
|
Kind::TextNote,
|
||||||
|
Kind::Repost,
|
||||||
|
Kind::EventDeletion,
|
||||||
|
])
|
||||||
|
.limit(30000),
|
||||||
|
NegentropyOptions::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
println!("Received: {}", report.received.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a little longer
|
||||||
|
// TODO: remove?
|
||||||
|
sleep(Duration::from_secs(5)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle
|
||||||
|
.emit("neg_synchronized", ())
|
||||||
|
.expect("Something wrong!");
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(public_key)
|
Ok(public_key_clone)
|
||||||
}
|
}
|
||||||
|
|||||||
+140
-230
@@ -5,7 +5,7 @@ use specta::Type;
|
|||||||
use std::{str::FromStr, time::Duration};
|
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_tags, get_latest_event, parse_event, process_event, Meta};
|
||||||
use crate::{Nostr, DEFAULT_DIFFICULTY, FETCH_LIMIT};
|
use crate::{Nostr, DEFAULT_DIFFICULTY, FETCH_LIMIT};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Type)]
|
#[derive(Debug, Clone, Serialize, Type)]
|
||||||
@@ -30,7 +30,7 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent,
|
|||||||
|
|
||||||
match client.database().query(vec![filter.clone()]).await {
|
match client.database().query(vec![filter.clone()]).await {
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
if let Some(event) = events.first() {
|
if let Some(event) = get_latest_event(&events) {
|
||||||
let raw = event.as_json();
|
let raw = event.as_json();
|
||||||
let parsed = if event.kind == Kind::TextNote {
|
let parsed = if event.kind == Kind::TextNote {
|
||||||
Some(parse_event(&event.content).await)
|
Some(parse_event(&event.content).await)
|
||||||
@@ -40,26 +40,25 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent,
|
|||||||
|
|
||||||
Ok(RichEvent { raw, parsed })
|
Ok(RichEvent { raw, parsed })
|
||||||
} else {
|
} else {
|
||||||
|
println!("Not found, getting event from relays...");
|
||||||
match client
|
match client
|
||||||
.get_events_of(
|
.stream_events_of(vec![filter], Some(Duration::from_secs(10)))
|
||||||
vec![filter],
|
|
||||||
EventSource::relays(Some(Duration::from_secs(5))),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(events) => {
|
Ok(mut rx) => {
|
||||||
if let Some(event) = events.first() {
|
let mut raw: String = String::new();
|
||||||
let raw = event.as_json();
|
let mut parsed: Option<Meta> = None;
|
||||||
let parsed = if event.kind == Kind::TextNote {
|
|
||||||
|
while let Some(event) = rx.next().await {
|
||||||
|
raw = event.as_json();
|
||||||
|
parsed = if event.kind == Kind::TextNote {
|
||||||
Some(parse_event(&event.content).await)
|
Some(parse_event(&event.content).await)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(RichEvent { raw, parsed })
|
|
||||||
} else {
|
|
||||||
Err("Cannot found this event with current relay list".into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(RichEvent { raw, parsed })
|
||||||
}
|
}
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
}
|
}
|
||||||
@@ -198,7 +197,7 @@ pub async fn subscribe_to(id: String, state: State<'_, Nostr>) -> Result<(), Str
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_events_by(
|
pub async fn get_all_events_by_author(
|
||||||
public_key: String,
|
public_key: String,
|
||||||
limit: i32,
|
limit: i32,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
@@ -215,183 +214,62 @@ pub async fn get_events_by(
|
|||||||
.get_events_of(vec![filter], EventSource::Database)
|
.get_events_of(vec![filter], EventSource::Database)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(events) => {
|
Ok(events) => Ok(process_event(client, events).await),
|
||||||
let fils = filter_converstation(events);
|
|
||||||
let futures = fils.iter().map(|ev| async move {
|
|
||||||
let raw = ev.as_json();
|
|
||||||
let parsed = if ev.kind == Kind::TextNote {
|
|
||||||
Some(parse_event(&ev.content).await)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
RichEvent { raw, parsed }
|
|
||||||
});
|
|
||||||
let rich_events = join_all(futures).await;
|
|
||||||
|
|
||||||
Ok(rich_events)
|
|
||||||
}
|
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_local_events(
|
pub async fn get_all_events_by_authors(
|
||||||
until: Option<&str>,
|
public_keys: Vec<String>,
|
||||||
|
until: Option<String>,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
) -> Result<Vec<RichEvent>, String> {
|
) -> Result<Vec<RichEvent>, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
|
||||||
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())?,
|
||||||
|
None => Timestamp::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let authors: Vec<PublicKey> = public_keys
|
||||||
|
.iter()
|
||||||
|
.map(|pk| PublicKey::from_str(pk).map_err(|err| err.to_string()))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
|
.limit(FETCH_LIMIT)
|
||||||
|
.until(as_of)
|
||||||
|
.authors(authors);
|
||||||
|
|
||||||
|
match client
|
||||||
|
.get_events_of(vec![filter], EventSource::Database)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(events) => Ok(process_event(client, events).await),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_all_events_by_hashtags(
|
||||||
|
hashtags: Vec<String>,
|
||||||
|
until: Option<String>,
|
||||||
|
state: State<'_, Nostr>,
|
||||||
|
) -> Result<Vec<RichEvent>, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
|
let as_of = match until {
|
||||||
|
Some(until) => Timestamp::from_str(&until).map_err(|err| err.to_string())?,
|
||||||
None => Timestamp::now(),
|
None => Timestamp::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
match client.database().query(vec![filter]).await {
|
|
||||||
Ok(events) => {
|
|
||||||
let fils = filter_converstation(events);
|
|
||||||
let futures = fils.iter().map(|ev| async move {
|
|
||||||
let raw = ev.as_json();
|
|
||||||
let parsed = if ev.kind == Kind::TextNote {
|
|
||||||
Some(parse_event(&ev.content).await)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
RichEvent { raw, parsed }
|
|
||||||
});
|
|
||||||
let rich_events = join_all(futures).await;
|
|
||||||
|
|
||||||
Ok(rich_events)
|
|
||||||
}
|
|
||||||
Err(err) => Err(err.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub async fn get_group_events(
|
|
||||||
public_keys: Vec<&str>,
|
|
||||||
until: Option<&str>,
|
|
||||||
state: State<'_, Nostr>,
|
|
||||||
) -> Result<Vec<RichEvent>, String> {
|
|
||||||
let client = &state.client;
|
|
||||||
|
|
||||||
let as_of = match until {
|
|
||||||
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
|
||||||
None => Timestamp::now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let authors: Vec<PublicKey> = public_keys
|
|
||||||
.iter()
|
|
||||||
.map(|p| {
|
|
||||||
if p.starts_with("npub1") {
|
|
||||||
PublicKey::from_bech32(p).map_err(|err| err.to_string())
|
|
||||||
} else {
|
|
||||||
PublicKey::from_hex(p).map_err(|err| err.to_string())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
|
||||||
.limit(20)
|
|
||||||
.until(as_of)
|
|
||||||
.authors(authors);
|
|
||||||
|
|
||||||
match client
|
|
||||||
.get_events_of(
|
|
||||||
vec![filter],
|
|
||||||
EventSource::both(Some(Duration::from_secs(5))),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(events) => {
|
|
||||||
let fils = filter_converstation(events);
|
|
||||||
let futures = fils.iter().map(|ev| async move {
|
|
||||||
let raw = ev.as_json();
|
|
||||||
let parsed = if ev.kind == Kind::TextNote {
|
|
||||||
Some(parse_event(&ev.content).await)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
RichEvent { raw, parsed }
|
|
||||||
});
|
|
||||||
|
|
||||||
let rich_events = join_all(futures).await;
|
|
||||||
|
|
||||||
Ok(rich_events)
|
|
||||||
}
|
|
||||||
Err(err) => Err(err.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub async fn get_global_events(
|
|
||||||
until: Option<&str>,
|
|
||||||
state: State<'_, Nostr>,
|
|
||||||
) -> Result<Vec<RichEvent>, String> {
|
|
||||||
let client = &state.client;
|
|
||||||
let as_of = match until {
|
|
||||||
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
|
||||||
None => Timestamp::now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
|
||||||
.limit(20)
|
|
||||||
.until(as_of);
|
|
||||||
|
|
||||||
match client
|
|
||||||
.get_events_of(
|
|
||||||
vec![filter],
|
|
||||||
EventSource::both(Some(Duration::from_secs(5))),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(events) => {
|
|
||||||
let fils = filter_converstation(events);
|
|
||||||
let futures = fils.iter().map(|ev| async move {
|
|
||||||
let raw = ev.as_json();
|
|
||||||
let parsed = if ev.kind == Kind::TextNote {
|
|
||||||
Some(parse_event(&ev.content).await)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
RichEvent { raw, parsed }
|
|
||||||
});
|
|
||||||
let rich_events = join_all(futures).await;
|
|
||||||
|
|
||||||
Ok(rich_events)
|
|
||||||
}
|
|
||||||
Err(err) => Err(err.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub async fn get_hashtag_events(
|
|
||||||
hashtags: Vec<&str>,
|
|
||||||
until: Option<&str>,
|
|
||||||
state: State<'_, Nostr>,
|
|
||||||
) -> Result<Vec<RichEvent>, String> {
|
|
||||||
let client = &state.client;
|
|
||||||
let as_of = match until {
|
|
||||||
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
|
||||||
None => Timestamp::now(),
|
|
||||||
};
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
|
||||||
.limit(20)
|
|
||||||
.until(as_of)
|
.until(as_of)
|
||||||
.hashtags(hashtags);
|
.hashtags(hashtags);
|
||||||
|
|
||||||
@@ -402,22 +280,61 @@ pub async fn get_hashtag_events(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(events) => {
|
Ok(events) => Ok(process_event(client, events).await),
|
||||||
let fils = filter_converstation(events);
|
Err(err) => Err(err.to_string()),
|
||||||
let futures = fils.iter().map(|ev| async move {
|
}
|
||||||
let raw = ev.as_json();
|
}
|
||||||
let parsed = if ev.kind == Kind::TextNote {
|
|
||||||
Some(parse_event(&ev.content).await)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
RichEvent { raw, parsed }
|
#[tauri::command]
|
||||||
});
|
#[specta::specta]
|
||||||
let rich_events = join_all(futures).await;
|
pub async fn get_local_events(
|
||||||
|
until: Option<String>,
|
||||||
|
state: State<'_, Nostr>,
|
||||||
|
) -> Result<Vec<RichEvent>, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
Ok(rich_events)
|
let as_of = match until {
|
||||||
}
|
Some(until) => Timestamp::from_str(&until).map_err(|err| err.to_string())?,
|
||||||
|
None => Timestamp::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
|
.limit(FETCH_LIMIT)
|
||||||
|
.until(as_of);
|
||||||
|
|
||||||
|
match client.database().query(vec![filter]).await {
|
||||||
|
Ok(events) => Ok(process_event(client, events).await),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_global_events(
|
||||||
|
until: Option<String>,
|
||||||
|
state: State<'_, Nostr>,
|
||||||
|
) -> Result<Vec<RichEvent>, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
|
let as_of = match until {
|
||||||
|
Some(until) => Timestamp::from_str(&until).map_err(|err| err.to_string())?,
|
||||||
|
None => Timestamp::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
|
.limit(FETCH_LIMIT)
|
||||||
|
.until(as_of);
|
||||||
|
|
||||||
|
match client
|
||||||
|
.get_events_of(
|
||||||
|
vec![filter],
|
||||||
|
EventSource::both(Some(Duration::from_secs(5))),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(events) => Ok(process_event(client, events).await),
|
||||||
Err(err) => Err(err.to_string()),
|
Err(err) => Err(err.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -433,7 +350,7 @@ pub async fn publish(
|
|||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
|
||||||
// Create tags from content
|
// Create tags from content
|
||||||
let mut tags = create_event_tags(&content);
|
let mut tags = create_tags(&content);
|
||||||
|
|
||||||
// Add client tag
|
// Add client tag
|
||||||
// TODO: allow user config this setting
|
// TODO: allow user config this setting
|
||||||
@@ -470,10 +387,8 @@ pub async fn reply(
|
|||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let database = client.database();
|
let database = client.database();
|
||||||
|
|
||||||
// Create tags from content
|
|
||||||
let mut tags = create_event_tags(&content);
|
|
||||||
|
|
||||||
let reply_id = EventId::parse(&to).map_err(|err| err.to_string())?;
|
let reply_id = EventId::parse(&to).map_err(|err| err.to_string())?;
|
||||||
|
let mut tags = create_tags(&content);
|
||||||
|
|
||||||
match database.query(vec![Filter::new().id(reply_id)]).await {
|
match database.query(vec![Filter::new().id(reply_id)]).await {
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
@@ -539,7 +454,7 @@ pub async fn reply(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
pub async fn repost(raw: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let event = Event::from_json(raw).map_err(|err| err.to_string())?;
|
let event = Event::from_json(raw).map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
@@ -592,7 +507,7 @@ pub async fn event_to_bech32(id: String, state: State<'_, Nostr>) -> Result<Stri
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
pub async fn user_to_bech32(user: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let public_key = PublicKey::parse(user).map_err(|err| err.to_string())?;
|
let public_key = PublicKey::parse(user).map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
@@ -632,48 +547,43 @@ pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result<Strin
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn search(
|
pub async fn search(query: String, state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
|
||||||
query: String,
|
|
||||||
until: Option<String>,
|
|
||||||
state: State<'_, Nostr>,
|
|
||||||
) -> Result<Vec<RichEvent>, String> {
|
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
let filter = Filter::new().search(query);
|
||||||
|
|
||||||
let timestamp = match until {
|
match client.database().query(vec![filter]).await {
|
||||||
Some(str) => Timestamp::from_str(&str).map_err(|err| err.to_string())?,
|
Ok(events) => Ok(process_event(client, events).await),
|
||||||
None => Timestamp::now(),
|
Err(e) => Err(e.to_string()),
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
let signer = client.signer().await.map_err(|err| err.to_string())?;
|
||||||
|
let public_key = signer.public_key().await.map_err(|err| err.to_string())?;
|
||||||
|
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kinds(vec![Kind::TextNote, Kind::Metadata])
|
.author(public_key)
|
||||||
.search(query)
|
.event(event_id)
|
||||||
.until(timestamp)
|
.kind(Kind::EventDeletion);
|
||||||
.limit(FETCH_LIMIT);
|
|
||||||
|
|
||||||
match client
|
match client.database().query(vec![filter]).await {
|
||||||
.get_events_of(
|
Ok(events) => Ok(!events.is_empty()),
|
||||||
vec![filter],
|
Err(e) => Err(e.to_string()),
|
||||||
EventSource::both(Some(Duration::from_secs(5))),
|
}
|
||||||
)
|
}
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(events) => {
|
|
||||||
let fils = filter_converstation(events);
|
|
||||||
let futures = fils.iter().map(|ev| async move {
|
|
||||||
let raw = ev.as_json();
|
|
||||||
let parsed = if ev.kind == Kind::TextNote {
|
|
||||||
Some(parse_event(&ev.content).await)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
RichEvent { raw, parsed }
|
#[tauri::command]
|
||||||
});
|
#[specta::specta]
|
||||||
|
pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> {
|
||||||
|
let client = &state.client;
|
||||||
|
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
|
||||||
|
let builder = EventBuilder::delete(vec![event_id]);
|
||||||
|
|
||||||
let rich_events = join_all(futures).await;
|
match client.send_event_builder(builder).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
Ok(rich_events)
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ use nostr_sdk::prelude::*;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
use std::{str::FromStr, time::Duration};
|
use std::{str::FromStr, time::Duration};
|
||||||
use tauri::State;
|
use tauri::{Emitter, Manager, State};
|
||||||
use tauri_specta::Event;
|
use tauri_specta::Event;
|
||||||
|
|
||||||
use crate::{common::get_latest_event, NewSettings, Nostr, Settings};
|
use crate::{
|
||||||
|
common::{get_latest_event, process_event},
|
||||||
|
NewSettings, Nostr, RichEvent, Settings,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||||
pub struct Profile {
|
pub struct Profile {
|
||||||
@@ -48,30 +51,30 @@ pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<
|
|||||||
|
|
||||||
match client.database().query(vec![filter.clone()]).await {
|
match client.database().query(vec![filter.clone()]).await {
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
if let Some(event) = events.first() {
|
if let Some(event) = get_latest_event(&events) {
|
||||||
if let Ok(metadata) = Metadata::from_json(&event.content) {
|
if let Ok(metadata) = Metadata::from_json(&event.content) {
|
||||||
Ok(metadata.as_json())
|
Ok(metadata.as_json())
|
||||||
} else {
|
} else {
|
||||||
Err("Parse metadata failed".into())
|
Err("Parse metadata failed".into())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
println!("Not found, getting event from relays...");
|
||||||
match client
|
match client
|
||||||
.get_events_of(
|
.stream_events_of(vec![filter], Some(Duration::from_secs(10)))
|
||||||
vec![filter],
|
|
||||||
EventSource::relays(Some(Duration::from_secs(5))),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(events) => {
|
Ok(mut rx) => {
|
||||||
if let Some(event) = events.first() {
|
let mut metadata: String = Metadata::new().as_json();
|
||||||
if let Ok(metadata) = Metadata::from_json(&event.content) {
|
|
||||||
Ok(metadata.as_json())
|
while let Some(event) = rx.next().await {
|
||||||
} else {
|
println!("Event: {}", event.as_json());
|
||||||
Err("Parse metadata failed".into())
|
if let Ok(m) = Metadata::from_json(&event.content) {
|
||||||
|
metadata = m.as_json();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Ok(Metadata::new().as_json())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
}
|
}
|
||||||
@@ -105,18 +108,13 @@ pub async fn set_contact_list(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
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 contact_list = state.contact_list.lock().await.clone();
|
||||||
|
let vec: Vec<String> = contact_list
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| f.public_key.to_hex())
|
||||||
|
.collect();
|
||||||
|
|
||||||
match client.get_contact_list(Some(Duration::from_secs(5))).await {
|
Ok(vec)
|
||||||
Ok(contact_list) => {
|
|
||||||
let list = contact_list
|
|
||||||
.into_iter()
|
|
||||||
.map(|f| f.public_key.to_hex())
|
|
||||||
.collect();
|
|
||||||
Ok(list)
|
|
||||||
}
|
|
||||||
Err(err) => Err(err.to_string()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -200,6 +198,170 @@ pub async fn toggle_contact(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn set_group(
|
||||||
|
title: String,
|
||||||
|
description: Option<String>,
|
||||||
|
image: Option<String>,
|
||||||
|
users: Vec<String>,
|
||||||
|
state: State<'_, Nostr>,
|
||||||
|
handle: tauri::AppHandle,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
let public_keys: Vec<PublicKey> = users
|
||||||
|
.iter()
|
||||||
|
.map(|u| PublicKey::from_str(u).unwrap())
|
||||||
|
.collect();
|
||||||
|
let label = title.to_lowercase().replace(" ", "-");
|
||||||
|
let mut tags: Vec<Tag> = vec![Tag::title(title)];
|
||||||
|
|
||||||
|
if let Some(desc) = description {
|
||||||
|
tags.push(Tag::description(desc))
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(img) = image {
|
||||||
|
let url = UncheckedUrl::new(img);
|
||||||
|
tags.push(Tag::image(url, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
let builder = EventBuilder::follow_set(label, public_keys.clone()).add_tags(tags);
|
||||||
|
|
||||||
|
match client.send_event_builder(builder).await {
|
||||||
|
Ok(report) => {
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let state = handle.state::<Nostr>();
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
|
.authors(public_keys)
|
||||||
|
.limit(500);
|
||||||
|
|
||||||
|
if let Ok(report) = client.reconcile(filter, NegentropyOptions::default()).await {
|
||||||
|
println!("Received: {}", report.received.len());
|
||||||
|
handle.emit("synchronized", ()).unwrap();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(report.id().to_hex())
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_group(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
let event_id = EventId::from_str(&id).map_err(|e| e.to_string())?;
|
||||||
|
let filter = Filter::new().kind(Kind::FollowSet).id(event_id);
|
||||||
|
|
||||||
|
match client.database().query(vec![filter]).await {
|
||||||
|
Ok(events) => match get_latest_event(&events) {
|
||||||
|
Some(ev) => Ok(ev.as_json()),
|
||||||
|
None => Err("Not found.".to_string()),
|
||||||
|
},
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_all_groups(state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
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 filter = Filter::new().kind(Kind::FollowSet).author(public_key);
|
||||||
|
|
||||||
|
match client.database().query(vec![filter]).await {
|
||||||
|
Ok(events) => Ok(process_event(client, events).await),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn set_interest(
|
||||||
|
title: String,
|
||||||
|
description: Option<String>,
|
||||||
|
image: Option<String>,
|
||||||
|
hashtags: Vec<String>,
|
||||||
|
state: State<'_, Nostr>,
|
||||||
|
handle: tauri::AppHandle,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
let label = title.to_lowercase().replace(" ", "-");
|
||||||
|
let mut tags: Vec<Tag> = vec![Tag::title(title)];
|
||||||
|
|
||||||
|
if let Some(desc) = description {
|
||||||
|
tags.push(Tag::description(desc))
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(img) = image {
|
||||||
|
let url = UncheckedUrl::new(img);
|
||||||
|
tags.push(Tag::image(url, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
let builder = EventBuilder::interest_set(label, hashtags.clone()).add_tags(tags);
|
||||||
|
|
||||||
|
match client.send_event_builder(builder).await {
|
||||||
|
Ok(report) => {
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let state = handle.state::<Nostr>();
|
||||||
|
let client = &state.client;
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||||
|
.hashtags(hashtags)
|
||||||
|
.limit(500);
|
||||||
|
|
||||||
|
if let Ok(report) = client.reconcile(filter, NegentropyOptions::default()).await {
|
||||||
|
println!("Received: {}", report.received.len());
|
||||||
|
handle.emit("synchronized", ()).unwrap();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(report.id().to_hex())
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_interest(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
let event_id = EventId::from_str(&id).map_err(|e| e.to_string())?;
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kinds(vec![Kind::Interests, Kind::InterestSet])
|
||||||
|
.id(event_id);
|
||||||
|
|
||||||
|
match client.database().query(vec![filter]).await {
|
||||||
|
Ok(events) => match get_latest_event(&events) {
|
||||||
|
Some(ev) => Ok(ev.as_json()),
|
||||||
|
None => Err("Not found.".to_string()),
|
||||||
|
},
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_all_interests(state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
|
||||||
|
let client = &state.client;
|
||||||
|
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 filter = Filter::new()
|
||||||
|
.kinds(vec![Kind::InterestSet, Kind::Interests])
|
||||||
|
.author(public_key);
|
||||||
|
|
||||||
|
match client.database().query(vec![filter]).await {
|
||||||
|
Ok(events) => Ok(process_event(client, events).await),
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_mention_list(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {
|
pub async fn get_mention_list(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {
|
||||||
@@ -230,61 +392,6 @@ pub async fn get_mention_list(state: State<'_, Nostr>) -> Result<Vec<Mention>, S
|
|||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub async fn set_lume_store(
|
|
||||||
key: String,
|
|
||||||
content: String,
|
|
||||||
state: State<'_, Nostr>,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let client = &state.client;
|
|
||||||
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 encrypted = signer
|
|
||||||
.nip44_encrypt(&public_key, content)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
let tag = Tag::identifier(key);
|
|
||||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]);
|
|
||||||
|
|
||||||
match client.send_event_builder(builder).await {
|
|
||||||
Ok(event_id) => Ok(event_id.to_string()),
|
|
||||||
Err(err) => Err(err.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
#[specta::specta]
|
|
||||||
pub async fn get_lume_store(key: String, state: State<'_, Nostr>) -> Result<String, String> {
|
|
||||||
let client = &state.client;
|
|
||||||
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 filter = Filter::new()
|
|
||||||
.author(public_key)
|
|
||||||
.kind(Kind::ApplicationSpecificData)
|
|
||||||
.identifier(key)
|
|
||||||
.limit(10);
|
|
||||||
|
|
||||||
match client
|
|
||||||
.get_events_of(vec![filter], EventSource::Database)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(events) => {
|
|
||||||
if let Some(event) = get_latest_event(&events) {
|
|
||||||
match signer.nip44_decrypt(&public_key, &event.content).await {
|
|
||||||
Ok(decrypted) => Ok(decrypted),
|
|
||||||
Err(_) => Err(event.content.to_string()),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err("Not found".into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => Err(err.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||||
@@ -470,31 +577,24 @@ pub async fn get_notifications(state: State<'_, Nostr>) -> Result<Vec<String>, S
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_settings(state: State<'_, Nostr>) -> Result<Settings, ()> {
|
pub async fn get_user_settings(state: State<'_, Nostr>) -> Result<Settings, ()> {
|
||||||
Ok(state.settings.lock().await.clone())
|
Ok(state.settings.lock().await.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn set_settings(
|
pub async fn set_user_settings(
|
||||||
settings: &str,
|
settings: String,
|
||||||
state: State<'_, Nostr>,
|
state: State<'_, Nostr>,
|
||||||
handle: tauri::AppHandle,
|
handle: tauri::AppHandle,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let ident = "lume_v4:settings";
|
let tags = vec![Tag::identifier("lume_user_setting")];
|
||||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
let builder = EventBuilder::new(Kind::ApplicationSpecificData, &settings, tags);
|
||||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
|
||||||
let encrypted = signer
|
|
||||||
.nip44_encrypt(&public_key, settings)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
let tag = Tag::identifier(ident);
|
|
||||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, encrypted, vec![tag]);
|
|
||||||
|
|
||||||
match client.send_event_builder(builder).await {
|
match client.send_event_builder(builder).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let parsed: Settings = serde_json::from_str(settings).map_err(|e| e.to_string())?;
|
let parsed: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
state.settings.lock().await.clone_from(&parsed);
|
state.settings.lock().await.clone_from(&parsed);
|
||||||
@@ -523,9 +623,8 @@ pub async fn verify_nip05(id: String, nip05: &str) -> Result<bool, String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn is_trusted_user(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
pub async fn is_trusted_user(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||||
let circles = &state.circles.lock().await;
|
let trusted_list = &state.trusted_list.lock().await;
|
||||||
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
|
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)
|
Ok(trusted_list.contains(&public_key))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,15 +113,15 @@ pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<bool
|
|||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||||
if let Some(window) = app_handle.get_window(&window.label) {
|
if let Some(current_window) = app_handle.get_window(&window.label) {
|
||||||
if window.is_visible().unwrap_or_default() {
|
if current_window.is_visible().unwrap_or_default() {
|
||||||
let _ = window.set_focus();
|
let _ = current_window.set_focus();
|
||||||
} else {
|
} else {
|
||||||
let _ = window.show();
|
let _ = current_window.show();
|
||||||
let _ = window.set_focus();
|
let _ = current_window.set_focus();
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let window = WebviewWindowBuilder::new(
|
let new_window = WebviewWindowBuilder::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
&window.label,
|
&window.label,
|
||||||
WebviewUrl::App(PathBuf::from(window.url)),
|
WebviewUrl::App(PathBuf::from(window.url)),
|
||||||
@@ -129,7 +129,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
|||||||
.title(&window.title)
|
.title(&window.title)
|
||||||
.min_inner_size(window.width, window.height)
|
.min_inner_size(window.width, window.height)
|
||||||
.inner_size(window.width, window.height)
|
.inner_size(window.width, window.height)
|
||||||
.hidden_title(true)
|
.hidden_title(window.hidden_title)
|
||||||
.title_bar_style(TitleBarStyle::Overlay)
|
.title_bar_style(TitleBarStyle::Overlay)
|
||||||
.minimizable(window.minimizable)
|
.minimizable(window.minimizable)
|
||||||
.maximizable(window.maximizable)
|
.maximizable(window.maximizable)
|
||||||
@@ -144,7 +144,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Restore native border
|
// Restore native border
|
||||||
window.add_border(None);
|
new_window.add_border(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -217,7 +217,7 @@ pub fn reopen_lume(app: tauri::AppHandle) {
|
|||||||
|
|
||||||
// Set a custom inset to the traffic lights
|
// Set a custom inset to the traffic lights
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
window.set_traffic_lights_inset(7.0, 13.0).unwrap();
|
window.set_traffic_lights_inset(7.0, 10.0).unwrap();
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let win = window.clone();
|
let win = window.clone();
|
||||||
@@ -225,7 +225,7 @@ pub fn reopen_lume(app: tauri::AppHandle) {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
window.on_window_event(move |event| {
|
window.on_window_event(move |event| {
|
||||||
if let tauri::WindowEvent::ThemeChanged(_) = event {
|
if let tauri::WindowEvent::ThemeChanged(_) = event {
|
||||||
win.set_traffic_lights_inset(7.0, 13.0).unwrap();
|
win.set_traffic_lights_inset(7.0, 10.0).unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+135
-176
@@ -1,3 +1,4 @@
|
|||||||
|
use futures::future::join_all;
|
||||||
use linkify::LinkFinder;
|
use linkify::LinkFinder;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use reqwest::Client as ReqClient;
|
use reqwest::Client as ReqClient;
|
||||||
@@ -7,7 +8,7 @@ use std::collections::HashSet;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::Settings;
|
use crate::RichEvent;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Type)]
|
#[derive(Debug, Clone, Serialize, Type)]
|
||||||
pub struct Meta {
|
pub struct Meta {
|
||||||
@@ -18,6 +19,7 @@ pub struct Meta {
|
|||||||
pub hashtags: Vec<String>,
|
pub hashtags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
||||||
const NOSTR_EVENTS: [&str; 10] = [
|
const NOSTR_EVENTS: [&str; 10] = [
|
||||||
"@nevent1",
|
"@nevent1",
|
||||||
"@note1",
|
"@note1",
|
||||||
@@ -42,126 +44,12 @@ const NOSTR_MENTIONS: [&str; 10] = [
|
|||||||
"Nostr:nprofile1",
|
"Nostr:nprofile1",
|
||||||
"Nostr:naddr1",
|
"Nostr:naddr1",
|
||||||
];
|
];
|
||||||
const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
|
||||||
|
|
||||||
pub fn get_latest_event(events: &[Event]) -> Option<&Event> {
|
pub fn get_latest_event(events: &[Event]) -> Option<&Event> {
|
||||||
events.iter().next()
|
events.iter().next()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn filter_converstation(events: Vec<Event>) -> Vec<Event> {
|
pub fn create_tags(content: &str) -> Vec<Tag> {
|
||||||
events
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|ev| {
|
|
||||||
let tags = ev.get_tags_content(TagKind::SingleLetter(SingleLetterTag::lowercase(
|
|
||||||
Alphabet::E,
|
|
||||||
)));
|
|
||||||
|
|
||||||
if tags.is_empty() {
|
|
||||||
Some(ev)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<Event>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dedup_event(events: &[Event]) -> Vec<Event> {
|
|
||||||
let mut seen_ids = HashSet::new();
|
|
||||||
events
|
|
||||||
.iter()
|
|
||||||
.filter(|&event| {
|
|
||||||
let e = TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E));
|
|
||||||
let e_tags: Vec<&Tag> = event.tags.iter().filter(|el| el.kind() == e).collect();
|
|
||||||
let ids: Vec<&str> = e_tags.iter().filter_map(|tag| tag.content()).collect();
|
|
||||||
let is_dup = ids.iter().any(|id| seen_ids.contains(*id));
|
|
||||||
|
|
||||||
for id in &ids {
|
|
||||||
seen_ids.insert(*id);
|
|
||||||
}
|
|
||||||
|
|
||||||
!is_dup
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn parse_event(content: &str) -> Meta {
|
|
||||||
let mut finder = LinkFinder::new();
|
|
||||||
finder.url_must_have_scheme(false);
|
|
||||||
|
|
||||||
// Get urls
|
|
||||||
let urls: Vec<_> = finder.links(content).collect();
|
|
||||||
// Get words
|
|
||||||
let words: Vec<_> = content.split_whitespace().collect();
|
|
||||||
|
|
||||||
let hashtags = words
|
|
||||||
.iter()
|
|
||||||
.filter(|&&word| word.starts_with('#'))
|
|
||||||
.map(|&s| s.to_string())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let events = words
|
|
||||||
.iter()
|
|
||||||
.filter(|&&word| NOSTR_EVENTS.iter().any(|&el| word.starts_with(el)))
|
|
||||||
.map(|&s| s.to_string())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mentions = words
|
|
||||||
.iter()
|
|
||||||
.filter(|&&word| NOSTR_MENTIONS.iter().any(|&el| word.starts_with(el)))
|
|
||||||
.map(|&s| s.to_string())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut images = Vec::new();
|
|
||||||
let mut text = content.to_string();
|
|
||||||
|
|
||||||
if !urls.is_empty() {
|
|
||||||
let client = ReqClient::new();
|
|
||||||
|
|
||||||
for url in urls {
|
|
||||||
let url_str = url.as_str();
|
|
||||||
|
|
||||||
if let Ok(parsed_url) = Url::from_str(url_str) {
|
|
||||||
if let Some(ext) = parsed_url
|
|
||||||
.path_segments()
|
|
||||||
.and_then(|segments| segments.last().and_then(|s| s.split('.').last()))
|
|
||||||
{
|
|
||||||
if IMAGES.contains(&ext) {
|
|
||||||
text = text.replace(url_str, "");
|
|
||||||
images.push(url_str.to_string());
|
|
||||||
// Process the next item.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the content type of URL via HEAD request
|
|
||||||
if let Ok(res) = client.head(url_str).send().await {
|
|
||||||
if let Some(content_type) = res.headers().get("Content-Type") {
|
|
||||||
if content_type.to_str().unwrap_or("").starts_with("image") {
|
|
||||||
text = text.replace(url_str, "");
|
|
||||||
images.push(url_str.to_string());
|
|
||||||
// Process the next item.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up the resulting content string to remove extra spaces
|
|
||||||
let cleaned_text = text.trim().to_string();
|
|
||||||
|
|
||||||
Meta {
|
|
||||||
content: cleaned_text,
|
|
||||||
events,
|
|
||||||
mentions,
|
|
||||||
hashtags,
|
|
||||||
images,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_event_tags(content: &str) -> Vec<Tag> {
|
|
||||||
let mut tags: Vec<Tag> = vec![];
|
let mut tags: Vec<Tag> = vec![];
|
||||||
let mut tag_set: HashSet<String> = HashSet::new();
|
let mut tag_set: HashSet<String> = HashSet::new();
|
||||||
|
|
||||||
@@ -242,6 +130,72 @@ pub fn create_event_tags(content: &str) -> Vec<Tag> {
|
|||||||
tags
|
tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn process_event(client: &Client, events: Vec<Event>) -> Vec<RichEvent> {
|
||||||
|
// Remove event thread if event is TextNote
|
||||||
|
let events: Vec<Event> = events
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|ev| {
|
||||||
|
if ev.kind == Kind::TextNote {
|
||||||
|
let tags = ev
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.filter(|t| t.is_reply() || t.is_root())
|
||||||
|
.filter_map(|t| t.content())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if tags.is_empty() {
|
||||||
|
Some(ev)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Get deletion request by event's authors
|
||||||
|
let ids: Vec<EventId> = events.iter().map(|ev| ev.id).collect();
|
||||||
|
let filter = Filter::new().events(ids).kind(Kind::EventDeletion);
|
||||||
|
|
||||||
|
let mut final_events: Vec<Event> = events.clone();
|
||||||
|
|
||||||
|
if let Ok(requests) = client.database().query(vec![filter]).await {
|
||||||
|
if !requests.is_empty() {
|
||||||
|
let ids: Vec<&str> = requests
|
||||||
|
.iter()
|
||||||
|
.flat_map(|ev| ev.get_tags_content(TagKind::e()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Remove event if event is deleted by author
|
||||||
|
final_events = events
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|ev| {
|
||||||
|
if ids.iter().any(|&i| i == ev.id.to_hex()) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert raw event to rich event
|
||||||
|
let futures = final_events.iter().map(|ev| async move {
|
||||||
|
let raw = ev.as_json();
|
||||||
|
let parsed = if ev.kind == Kind::TextNote {
|
||||||
|
Some(parse_event(&ev.content).await)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
RichEvent { raw, parsed }
|
||||||
|
});
|
||||||
|
|
||||||
|
join_all(futures).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn init_nip65(client: &Client, public_key: &str) {
|
pub async fn init_nip65(client: &Client, public_key: &str) {
|
||||||
let author = PublicKey::from_str(public_key).unwrap();
|
let author = PublicKey::from_str(public_key).unwrap();
|
||||||
let filter = Filter::new().author(author).kind(Kind::RelayList).limit(1);
|
let filter = Filter::new().author(author).kind(Kind::RelayList).limit(1);
|
||||||
@@ -277,73 +231,78 @@ pub async fn init_nip65(client: &Client, public_key: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_settings(client: &Client) -> Result<Settings, String> {
|
pub async fn parse_event(content: &str) -> Meta {
|
||||||
let ident = "lume_v4:settings";
|
let mut finder = LinkFinder::new();
|
||||||
let signer = client
|
finder.url_must_have_scheme(false);
|
||||||
.signer()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to get signer: {:?}", e))?;
|
|
||||||
let public_key = signer
|
|
||||||
.public_key()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to get public key: {:?}", e))?;
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
// Get urls
|
||||||
.author(public_key)
|
let urls: Vec<_> = finder.links(content).collect();
|
||||||
.kind(Kind::ApplicationSpecificData)
|
// Get words
|
||||||
.identifier(ident)
|
let words: Vec<_> = content.split_whitespace().collect();
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
match client
|
let hashtags = words
|
||||||
.get_events_of(
|
.iter()
|
||||||
vec![filter],
|
.filter(|&&word| word.starts_with('#'))
|
||||||
EventSource::both(Some(Duration::from_secs(5))),
|
.map(|&s| s.to_string())
|
||||||
)
|
.collect::<Vec<_>>();
|
||||||
.await
|
|
||||||
{
|
let events = words
|
||||||
Ok(events) => {
|
.iter()
|
||||||
if let Some(event) = events.first() {
|
.filter(|&&word| NOSTR_EVENTS.iter().any(|&el| word.starts_with(el)))
|
||||||
match signer.nip44_decrypt(&public_key, &event.content).await {
|
.map(|&s| s.to_string())
|
||||||
Ok(decrypted) => match serde_json::from_str(&decrypted) {
|
.collect::<Vec<_>>();
|
||||||
Ok(parsed) => Ok(parsed),
|
|
||||||
Err(_) => Err("Could not parse settings payload".into()),
|
let mentions = words
|
||||||
},
|
.iter()
|
||||||
Err(e) => Err(format!("Failed to decrypt settings content: {:?}", e)),
|
.filter(|&&word| NOSTR_MENTIONS.iter().any(|&el| word.starts_with(el)))
|
||||||
|
.map(|&s| s.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut images = Vec::new();
|
||||||
|
let mut text = content.to_string();
|
||||||
|
|
||||||
|
if !urls.is_empty() {
|
||||||
|
let client = ReqClient::new();
|
||||||
|
|
||||||
|
for url in urls {
|
||||||
|
let url_str = url.as_str();
|
||||||
|
|
||||||
|
if let Ok(parsed_url) = Url::from_str(url_str) {
|
||||||
|
if let Some(ext) = parsed_url
|
||||||
|
.path_segments()
|
||||||
|
.and_then(|segments| segments.last().and_then(|s| s.split('.').last()))
|
||||||
|
{
|
||||||
|
if IMAGES.contains(&ext) {
|
||||||
|
text = text.replace(url_str, "");
|
||||||
|
images.push(url_str.to_string());
|
||||||
|
// Process the next item.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the content type of URL via HEAD request
|
||||||
|
if let Ok(res) = client.head(url_str).send().await {
|
||||||
|
if let Some(content_type) = res.headers().get("Content-Type") {
|
||||||
|
if content_type.to_str().unwrap_or("").starts_with("image") {
|
||||||
|
text = text.replace(url_str, "");
|
||||||
|
images.push(url_str.to_string());
|
||||||
|
// Process the next item.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Err("Settings not found.".into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => Err(format!(
|
|
||||||
"Failed to get events for ApplicationSpecificData: {:?}",
|
|
||||||
e
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
// Clean up the resulting content string to remove extra spaces
|
||||||
mod tests {
|
let cleaned_text = text.trim().to_string();
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
Meta {
|
||||||
async fn test_parse_event() {
|
content: cleaned_text,
|
||||||
let content = "Check this image: https://example.com/image.jpg #cool @npub1";
|
events,
|
||||||
let meta = parse_event(content).await;
|
mentions,
|
||||||
|
hashtags,
|
||||||
assert_eq!(meta.content, "Check this image: #cool @npub1");
|
images,
|
||||||
assert_eq!(meta.images, vec!["https://example.com/image.jpg"]);
|
|
||||||
assert_eq!(meta.hashtags, vec!["#cool"]);
|
|
||||||
assert_eq!(meta.mentions, vec!["@npub1"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_parse_video() {
|
|
||||||
let content = "Check this video: https://example.com/video.mp4 #cool @npub1";
|
|
||||||
let meta = parse_event(content).await;
|
|
||||||
|
|
||||||
assert_eq!(meta.content, "Check this video: #cool @npub1");
|
|
||||||
assert_eq!(meta.images, Vec::<String>::new());
|
|
||||||
assert_eq!(meta.hashtags, vec!["#cool"]);
|
|
||||||
assert_eq!(meta.mentions, vec!["@npub1"]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-35
@@ -7,13 +7,12 @@
|
|||||||
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::{Profile as DatabaseProfile, *};
|
||||||
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,
|
collections::HashSet,
|
||||||
fs,
|
fs,
|
||||||
io::{self, BufRead},
|
io::{self, BufRead},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
@@ -32,7 +31,7 @@ pub struct Nostr {
|
|||||||
client: Client,
|
client: Client,
|
||||||
settings: Mutex<Settings>,
|
settings: Mutex<Settings>,
|
||||||
contact_list: Mutex<Vec<Contact>>,
|
contact_list: Mutex<Vec<Contact>>,
|
||||||
circles: Mutex<HashMap<PublicKey, Vec<PublicKey>>>,
|
trusted_list: Mutex<HashSet<PublicKey>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||||
@@ -84,7 +83,6 @@ struct NewSettings(Settings);
|
|||||||
|
|
||||||
pub const DEFAULT_DIFFICULTY: u8 = 21;
|
pub const DEFAULT_DIFFICULTY: u8 = 21;
|
||||||
pub const FETCH_LIMIT: usize = 100;
|
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";
|
||||||
|
|
||||||
@@ -108,6 +106,7 @@ fn main() {
|
|||||||
delete_account,
|
delete_account,
|
||||||
reset_password,
|
reset_password,
|
||||||
is_account_sync,
|
is_account_sync,
|
||||||
|
create_sync_file,
|
||||||
login,
|
login,
|
||||||
get_profile,
|
get_profile,
|
||||||
set_profile,
|
set_profile,
|
||||||
@@ -116,8 +115,12 @@ fn main() {
|
|||||||
check_contact,
|
check_contact,
|
||||||
toggle_contact,
|
toggle_contact,
|
||||||
get_mention_list,
|
get_mention_list,
|
||||||
get_lume_store,
|
set_group,
|
||||||
set_lume_store,
|
get_group,
|
||||||
|
get_all_groups,
|
||||||
|
set_interest,
|
||||||
|
get_interest,
|
||||||
|
get_all_interests,
|
||||||
set_wallet,
|
set_wallet,
|
||||||
load_wallet,
|
load_wallet,
|
||||||
remove_wallet,
|
remove_wallet,
|
||||||
@@ -125,8 +128,8 @@ fn main() {
|
|||||||
zap_event,
|
zap_event,
|
||||||
copy_friend,
|
copy_friend,
|
||||||
get_notifications,
|
get_notifications,
|
||||||
get_settings,
|
get_user_settings,
|
||||||
set_settings,
|
set_user_settings,
|
||||||
verify_nip05,
|
verify_nip05,
|
||||||
is_trusted_user,
|
is_trusted_user,
|
||||||
get_event_meta,
|
get_event_meta,
|
||||||
@@ -134,11 +137,13 @@ fn main() {
|
|||||||
get_event_from,
|
get_event_from,
|
||||||
get_replies,
|
get_replies,
|
||||||
subscribe_to,
|
subscribe_to,
|
||||||
get_events_by,
|
get_all_events_by_author,
|
||||||
|
get_all_events_by_authors,
|
||||||
|
get_all_events_by_hashtags,
|
||||||
get_local_events,
|
get_local_events,
|
||||||
get_group_events,
|
|
||||||
get_global_events,
|
get_global_events,
|
||||||
get_hashtag_events,
|
is_deleted_event,
|
||||||
|
request_delete,
|
||||||
search,
|
search,
|
||||||
publish,
|
publish,
|
||||||
reply,
|
reply,
|
||||||
@@ -269,7 +274,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect
|
// Connect
|
||||||
client.connect().await;
|
client.connect_with_timeout(Duration::from_secs(20)).await;
|
||||||
|
|
||||||
client
|
client
|
||||||
});
|
});
|
||||||
@@ -279,7 +284,7 @@ fn main() {
|
|||||||
client,
|
client,
|
||||||
settings: Mutex::new(Settings::default()),
|
settings: Mutex::new(Settings::default()),
|
||||||
contact_list: Mutex::new(Vec::new()),
|
contact_list: Mutex::new(Vec::new()),
|
||||||
circles: Mutex::new(HashMap::new()),
|
trusted_list: Mutex::new(HashSet::new()),
|
||||||
});
|
});
|
||||||
|
|
||||||
Subscription::listen_any(app, move |event| {
|
Subscription::listen_any(app, move |event| {
|
||||||
@@ -427,14 +432,15 @@ fn main() {
|
|||||||
// Send native notification
|
// Send native notification
|
||||||
if allow_notification {
|
if allow_notification {
|
||||||
let author = client
|
let author = client
|
||||||
.fetch_metadata(
|
.database()
|
||||||
event.pubkey,
|
.profile(event.pubkey)
|
||||||
Some(Duration::from_secs(3)),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|_| Metadata::new());
|
.unwrap_or_else(|_| {
|
||||||
|
DatabaseProfile::new(event.pubkey, Metadata::new())
|
||||||
|
});
|
||||||
|
let metadata = author.metadata();
|
||||||
|
|
||||||
send_event_notification(&event, author, &handle_clone);
|
send_event_notification(&event, metadata, &handle_clone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,28 +473,36 @@ fn main() {
|
|||||||
if let tauri::WindowEvent::Focused(focused) = event {
|
if let tauri::WindowEvent::Focused(focused) = event {
|
||||||
if !focused {
|
if !focused {
|
||||||
let handle = window.app_handle().to_owned();
|
let handle = window.app_handle().to_owned();
|
||||||
|
let config_dir = handle.path().app_config_dir().unwrap();
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let state = handle.state::<Nostr>();
|
let state = handle.state::<Nostr>();
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
|
||||||
if client.signer().await.is_ok() {
|
if let Ok(signer) = client.signer().await {
|
||||||
if let Ok(contact_list) =
|
let public_key = signer.public_key().await.unwrap();
|
||||||
client.get_contact_list(Some(Duration::from_secs(5))).await
|
let bech32 = public_key.to_bech32().unwrap();
|
||||||
{
|
|
||||||
let authors: Vec<PublicKey> =
|
|
||||||
contact_list.iter().map(|f| f.public_key).collect();
|
|
||||||
let newsfeed = Filter::new()
|
|
||||||
.authors(authors)
|
|
||||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
|
||||||
.limit(NEWSFEED_NEG_LIMIT);
|
|
||||||
|
|
||||||
if client
|
if fs::metadata(config_dir.join(bech32)).is_ok() {
|
||||||
.reconcile(newsfeed, NegentropyOptions::default())
|
if let Ok(contact_list) =
|
||||||
.await
|
client.get_contact_list(Some(Duration::from_secs(5))).await
|
||||||
.is_ok()
|
|
||||||
{
|
{
|
||||||
handle.emit("synchronized", ()).unwrap();
|
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(1000),
|
||||||
|
NegentropyOptions::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
handle.emit("synchronized", ()).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -509,6 +523,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())
|
||||||
|
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.2.0",
|
"version": "4.3.0",
|
||||||
"identifier": "nu.lume.Lume",
|
"identifier": "nu.lume.Lume",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
+10
-7
@@ -1,24 +1,27 @@
|
|||||||
|
import { 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 { routeTree } from "./routes.gen"; // auto generated file
|
|
||||||
import type { LumeEvent } from "./system";
|
|
||||||
import "./app.css";
|
|
||||||
import { experimental_createPersister } from "@tanstack/query-persist-client-core";
|
|
||||||
import { Store } from "@tauri-apps/plugin-store";
|
|
||||||
import { newQueryStorage } from "./commons";
|
import { newQueryStorage } from "./commons";
|
||||||
|
import type { LumeEvent } from "./system";
|
||||||
|
|
||||||
|
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 store = new Store(".cache");
|
// @ts-expect-error, required: https://github.com/tauri-apps/plugins-workspace/pull/1860
|
||||||
|
const store = await createStore(".cache", { autoSave: 250 });
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
gcTime: 1000 * 30,
|
gcTime: 1000 * 30,
|
||||||
persister: experimental_createPersister({
|
persister: experimental_createPersister({
|
||||||
storage: newQueryStorage(store),
|
storage: newQueryStorage(store),
|
||||||
maxAge: 1000 * 60 * 60 * 12, // 12 hours
|
maxAge: 1000 * 60 * 60 * 12,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+73
-22
@@ -99,6 +99,9 @@ async resetPassword(key: string, password: string) : Promise<Result<null, string
|
|||||||
async isAccountSync(id: string) : Promise<boolean> {
|
async isAccountSync(id: string) : Promise<boolean> {
|
||||||
return await TAURI_INVOKE("is_account_sync", { id });
|
return await TAURI_INVOKE("is_account_sync", { id });
|
||||||
},
|
},
|
||||||
|
async createSyncFile(id: string) : Promise<boolean> {
|
||||||
|
return await TAURI_INVOKE("create_sync_file", { 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 }) };
|
||||||
@@ -163,17 +166,49 @@ async getMentionList() : Promise<Result<Mention[], string>> {
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getLumeStore(key: string) : Promise<Result<string, string>> {
|
async setGroup(title: string, description: string | null, image: string | null, users: 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("set_group", { title, description, image, users }) };
|
||||||
} 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 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setLumeStore(key: string, content: string) : Promise<Result<string, string>> {
|
async getGroup(id: string) : Promise<Result<string, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("set_lume_store", { key, content }) };
|
return { status: "ok", data: await TAURI_INVOKE("get_group", { id }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getAllGroups() : Promise<Result<RichEvent[], string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_all_groups") };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setInterest(title: string, description: string | null, image: string | null, hashtags: string[]) : Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("set_interest", { title, description, image, hashtags }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getInterest(id: string) : Promise<Result<string, string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_interest", { id }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getAllInterests() : Promise<Result<RichEvent[], string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_all_interests") };
|
||||||
} 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 };
|
||||||
@@ -235,17 +270,17 @@ async getNotifications() : Promise<Result<string[], string>> {
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getSettings() : Promise<Result<Settings, null>> {
|
async getUserSettings() : Promise<Result<Settings, null>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_settings") };
|
return { status: "ok", data: await TAURI_INVOKE("get_user_settings") };
|
||||||
} 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 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setSettings(settings: string) : Promise<Result<null, string>> {
|
async setUserSettings(settings: string) : Promise<Result<null, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("set_settings", { settings }) };
|
return { status: "ok", data: await TAURI_INVOKE("set_user_settings", { settings }) };
|
||||||
} 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 };
|
||||||
@@ -307,9 +342,25 @@ async subscribeTo(id: string) : Promise<Result<null, string>> {
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getEventsBy(publicKey: string, limit: number) : Promise<Result<RichEvent[], string>> {
|
async getAllEventsByAuthor(publicKey: string, limit: number) : Promise<Result<RichEvent[], string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_events_by", { publicKey, limit }) };
|
return { status: "ok", data: await TAURI_INVOKE("get_all_events_by_author", { publicKey, limit }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getAllEventsByAuthors(publicKeys: string[], until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_all_events_by_authors", { publicKeys, until }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getAllEventsByHashtags(hashtags: string[], until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_all_events_by_hashtags", { hashtags, 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 };
|
||||||
@@ -323,14 +374,6 @@ async getLocalEvents(until: string | null) : Promise<Result<RichEvent[], string>
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getGroupEvents(publicKeys: string[], until: string | null) : Promise<Result<RichEvent[], string>> {
|
|
||||||
try {
|
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_group_events", { publicKeys, until }) };
|
|
||||||
} catch (e) {
|
|
||||||
if(e instanceof Error) throw e;
|
|
||||||
else return { status: "error", error: e as any };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async getGlobalEvents(until: string | null) : Promise<Result<RichEvent[], string>> {
|
async getGlobalEvents(until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_global_events", { until }) };
|
return { status: "ok", data: await TAURI_INVOKE("get_global_events", { until }) };
|
||||||
@@ -339,17 +382,25 @@ async getGlobalEvents(until: string | null) : Promise<Result<RichEvent[], string
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getHashtagEvents(hashtags: string[], until: string | null) : Promise<Result<RichEvent[], string>> {
|
async isDeletedEvent(id: string) : Promise<Result<boolean, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_hashtag_events", { hashtags, until }) };
|
return { status: "ok", data: await TAURI_INVOKE("is_deleted_event", { id }) };
|
||||||
} 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 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async search(query: string, until: string | null) : Promise<Result<RichEvent[], string>> {
|
async requestDelete(id: string) : Promise<Result<null, string>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("search", { query, until }) };
|
return { status: "ok", data: await TAURI_INVOKE("request_delete", { id }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async search(query: string) : Promise<Result<RichEvent[], string>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("search", { query }) };
|
||||||
} 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 };
|
||||||
|
|||||||
+4
-1
@@ -18,7 +18,7 @@ import { decode } from "light-bolt11-decoder";
|
|||||||
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";
|
||||||
import type { NostrEvent } from "./types";
|
import type { LumeColumn, NostrEvent } from "./types";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
dayjs.extend(updateLocale);
|
dayjs.extend(updateLocale);
|
||||||
@@ -154,6 +154,7 @@ export function decodeZapInvoice(tags?: string[][]) {
|
|||||||
(s: { name: string }) => s.name === "amount",
|
(s: { name: string }) => s.name === "amount",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// @ts-ignore, its fine.
|
||||||
const amount = Number.parseInt(amountSection.value);
|
const amount = Number.parseInt(amountSection.value);
|
||||||
const displayValue = getBitcoinDisplayValues(amount);
|
const displayValue = getBitcoinDisplayValues(amount);
|
||||||
|
|
||||||
@@ -289,3 +290,5 @@ export const appSettings = new Store<Settings>({
|
|||||||
display_media: true,
|
display_media: true,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const appColumns = new Store<LumeColumn[]>([]);
|
||||||
|
|||||||
+22
-17
@@ -1,7 +1,9 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
|
import { appColumns } from "@/commons";
|
||||||
import type { LumeColumn } from "@/types";
|
import type { LumeColumn } from "@/types";
|
||||||
import { Check, DotsThree } from "@phosphor-icons/react";
|
import { CaretDown, Check } from "@phosphor-icons/react";
|
||||||
import { useParams } from "@tanstack/react-router";
|
import { useParams } from "@tanstack/react-router";
|
||||||
|
import { useStore } from "@tanstack/react-store";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||||
@@ -86,10 +88,10 @@ export const Column = memo(function Column({ column }: { column: LumeColumn }) {
|
|||||||
}, [params.account]);
|
}, [params.account]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-[440px] shrink-0 p-2">
|
<div className="h-full w-[440px] shrink-0 border-r border-black/5 dark:border-white/5">
|
||||||
<div className="flex flex-col w-full h-full rounded-xl bg-black/5 dark:bg-white/20">
|
<div className="flex flex-col gap-px size-full">
|
||||||
<Header label={column.label} name={column.name} />
|
<Header label={column.label} />
|
||||||
<div ref={container} className="flex-1 w-full h-full">
|
<div ref={container} className="flex-1 size-full">
|
||||||
{!isCreated ? (
|
{!isCreated ? (
|
||||||
<div className="size-full flex items-center justify-center">
|
<div className="size-full flex items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -101,10 +103,14 @@ export const Column = memo(function Column({ column }: { column: LumeColumn }) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function Header({ label, name }: { label: string; name: string }) {
|
function Header({ label }: { label: string }) {
|
||||||
const [title, setTitle] = useState(name);
|
const [title, setTitle] = useState("");
|
||||||
const [isChanged, setIsChanged] = useState(false);
|
const [isChanged, setIsChanged] = useState(false);
|
||||||
|
|
||||||
|
const column = useStore(appColumns, (state) =>
|
||||||
|
state.find((col) => col.label === label),
|
||||||
|
);
|
||||||
|
|
||||||
const saveNewTitle = async () => {
|
const saveNewTitle = async () => {
|
||||||
const mainWindow = getCurrentWindow();
|
const mainWindow = getCurrentWindow();
|
||||||
await mainWindow.emit("columns", { type: "set_title", label, title });
|
await mainWindow.emit("columns", { type: "set_title", label, title });
|
||||||
@@ -173,29 +179,28 @@ function Header({ label, name }: { label: string; name: string }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (title.length !== name.length) setIsChanged(true);
|
if (title.length > 0) setIsChanged(true);
|
||||||
}, [title]);
|
}, [title.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between w-full px-1 h-9 shrink-0">
|
<div className="group flex items-center justify-center gap-2 w-full h-9 shrink-0">
|
||||||
<div className="size-7" />
|
|
||||||
<div className="flex items-center justify-center shrink-0 h-7">
|
<div className="flex items-center justify-center shrink-0 h-7">
|
||||||
<div className="relative flex items-center gap-2">
|
<div className="relative flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
contentEditable
|
contentEditable
|
||||||
suppressContentEditableWarning={true}
|
suppressContentEditableWarning={true}
|
||||||
onBlur={(e) => setTitle(e.currentTarget.textContent)}
|
onBlur={(e) => setTitle(e.currentTarget.textContent)}
|
||||||
className="text-sm font-medium focus:outline-none"
|
className="text-[12px] font-semibold focus:outline-none"
|
||||||
>
|
>
|
||||||
{name}
|
{column.name}
|
||||||
</div>
|
</div>
|
||||||
{isChanged ? (
|
{isChanged ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => saveNewTitle()}
|
onClick={() => saveNewTitle()}
|
||||||
className="text-teal-500 hover:text-teal-600"
|
className="text-teal-500 hover:text-teal-600 inline-flex items-center justify-center size-6 border-[.5px] border-neutral-200 dark:border-neutral-800 shadow shadow-neutral-200/50 dark:shadow-none rounded-full bg-white dark:bg-black"
|
||||||
>
|
>
|
||||||
<Check className="size-4" />
|
<Check className="size-3" weight="bold" />
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -203,9 +208,9 @@ function Header({ label, name }: { label: string; name: string }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => showContextMenu(e)}
|
onClick={(e) => showContextMenu(e)}
|
||||||
className="inline-flex items-center justify-center rounded-lg size-7 hover:bg-black/10 dark:hover:bg-white/10"
|
className="hidden shrink-0 group-hover:inline-flex items-center justify-center size-6 border-[.5px] border-neutral-200 dark:border-neutral-800 shadow shadow-neutral-200/50 dark:shadow-none rounded-full bg-white dark:bg-black"
|
||||||
>
|
>
|
||||||
<DotsThree className="size-5" />
|
<CaretDown className="size-3" weight="bold" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function NoteOpenThread() {
|
|||||||
</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();
|
||||||
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ export function NoteContent({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"select-text text-pretty content-break overflow-hidden",
|
"select-text text-pretty content-break overflow-hidden",
|
||||||
event.content.length > 500 ? "max-h-[250px] gradient-mask-b-0" : "",
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { LumeWindow } from "@/system";
|
|
||||||
|
|
||||||
export function Hashtag({ tag }: { tag: string }) {
|
export function Hashtag({ tag }: { tag: string }) {
|
||||||
return (
|
return (
|
||||||
<button
|
<span className="leading-normal cursor-default text-blue-500 hover:text-blue-600 font-normal">
|
||||||
type="button"
|
|
||||||
onClick={() => LumeWindow.openHashtag(tag)}
|
|
||||||
className="leading-normal cursor-default text-blue-500 hover:text-blue-600 font-normal"
|
|
||||||
>
|
|
||||||
{tag}
|
{tag}
|
||||||
</button>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { replyTime } from "@/commons";
|
|||||||
import { Note, Spinner } from "@/components";
|
import { Note, Spinner } from "@/components";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import { LumeWindow, useEvent } from "@/system";
|
import { LumeWindow, useEvent } from "@/system";
|
||||||
import { memo } from "react";
|
import { nip19 } from "nostr-tools";
|
||||||
|
import { type ReactNode, memo, useMemo } from "react";
|
||||||
|
import reactStringReplace from "react-string-replace";
|
||||||
|
import { Hashtag } from "./hashtag";
|
||||||
|
import { MentionUser } from "./user";
|
||||||
|
|
||||||
export const MentionNote = memo(function MentionNote({
|
export const MentionNote = memo(function MentionNote({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -13,14 +17,17 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative my-2">
|
<div className="relative my-2">
|
||||||
<div className="pl-3 before:content-[''] before:absolute before:top-1.5 before:bottom-1.5 before:left-0 before:border-l-[2px] before:border-black/10 dark:before:border-white/10">
|
<div className="min-h-[64px] pl-3 before:content-[''] before:absolute before:top-1.5 before:bottom-1.5 before:left-0 before:border-l-[2px] before:border-black/10 dark:before:border-white/10">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Spinner />
|
<div className="h-[64px] flex items-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
) : isError || !event ? (
|
) : isError || !event ? (
|
||||||
<p className="text-sm font-medium text-red-500">
|
<div className="h-[64px] flex items-center">
|
||||||
{error.message ||
|
<p className="text-sm font-medium text-red-500">
|
||||||
"Quoted note is not found with your current relay set"}
|
{error.message || "Note can be found with your current relay set"}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
<User.Provider pubkey={event.pubkey}>
|
<User.Provider pubkey={event.pubkey}>
|
||||||
@@ -33,9 +40,11 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
/>
|
/>
|
||||||
</User.Root>
|
</User.Root>
|
||||||
<div className="pl-2 inline select-text text-balance content-break overflow-hidden">
|
<div className="pl-2 inline select-text text-balance content-break overflow-hidden">
|
||||||
{event.content.length > 120
|
{event.content.length > 300 ? (
|
||||||
? `${event.content.substring(0, 120)}...`
|
`${event.content.substring(0, 300)}...`
|
||||||
: event.content}
|
) : (
|
||||||
|
<Content text={event.content} className="inline" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-between">
|
<div className="flex-1 flex items-center justify-between">
|
||||||
@@ -63,3 +72,64 @@ export const MentionNote = memo(function MentionNote({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,51 +1,68 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
import { DotsThree } from "@phosphor-icons/react";
|
import { DotsThree } from "@phosphor-icons/react";
|
||||||
|
import { useSearch } from "@tanstack/react-router";
|
||||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useNoteContext } from "./provider";
|
import { useNoteContext } from "./provider";
|
||||||
|
|
||||||
export function NoteMenu() {
|
export function NoteMenu() {
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
|
const { account }: { account: string } = useSearch({ strict: false });
|
||||||
|
|
||||||
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
|
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const menuItems = await Promise.all([
|
const list = [
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Copy Sharable Link",
|
text: "Copy ID",
|
||||||
action: async () => {
|
|
||||||
const eventId = await event.idAsBech32();
|
|
||||||
await writeText(`https://njump.me/${eventId}`);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
MenuItem.new({
|
|
||||||
text: "Copy Event ID",
|
|
||||||
action: async () => {
|
action: async () => {
|
||||||
const eventId = await event.idAsBech32();
|
const eventId = await event.idAsBech32();
|
||||||
await writeText(eventId);
|
await writeText(eventId);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Copy Public Key",
|
text: "Copy author",
|
||||||
action: async () => {
|
action: async () => {
|
||||||
const pubkey = await event.pubkeyAsBech32();
|
const pubkey = await event.pubkeyAsBech32();
|
||||||
await writeText(pubkey);
|
await writeText(pubkey);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
PredefinedMenuItem.new({ item: "Separator" }),
|
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Copy Raw Event",
|
text: "Copy sharable link",
|
||||||
action: async () => {
|
action: async () => {
|
||||||
event.meta = undefined;
|
const eventId = await event.idAsBech32();
|
||||||
const raw = JSON.stringify(event);
|
await writeText(`https://njump.me/${eventId}`);
|
||||||
await writeText(raw);
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
PredefinedMenuItem.new({ item: "Separator" }),
|
||||||
|
MenuItem.new({
|
||||||
|
text: "Copy raw event",
|
||||||
|
action: async () => {
|
||||||
|
event.meta = undefined;
|
||||||
|
await writeText(JSON.stringify(event));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
const menu = await Menu.new({
|
if (account?.length) {
|
||||||
items: menuItems,
|
const pubkey = nip19.decode(account).data;
|
||||||
});
|
|
||||||
|
if (event.pubkey === pubkey) {
|
||||||
|
list.push(
|
||||||
|
MenuItem.new({
|
||||||
|
text: "Request delete",
|
||||||
|
action: async () => {
|
||||||
|
await commands.requestDelete(event.id);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = await Promise.all(list);
|
||||||
|
const menu = await Menu.new({ items });
|
||||||
|
|
||||||
await menu.popup().catch((e) => console.error(e));
|
await menu.popup().catch((e) => console.error(e));
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ export const Quote = memo(function Quote({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
<Note.Root
|
<Note.Root className={cn("", className)}>
|
||||||
className={cn(
|
|
||||||
"bg-white dark:bg-black/20 rounded-xl flex flex-col gap-3 shadow-primary dark:ring-1 ring-neutral-800/50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Note.Child event={event.quote} isRoot />
|
<Note.Child event={event.quote} isRoot />
|
||||||
<div className="flex items-center gap-2 px-3">
|
<div className="flex items-center gap-2 px-3">
|
||||||
|
|||||||
@@ -99,10 +99,10 @@ export const ReplyNote = memo(function ReplyNote({
|
|||||||
<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 ? (
|
||||||
@@ -180,10 +180,10 @@ function ChildReply({ event }: { event: LumeEvent }) {
|
|||||||
<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 ? (
|
||||||
|
|||||||
@@ -12,15 +12,10 @@ export const RepostNote = memo(function RepostNote({
|
|||||||
event: LumeEvent;
|
event: LumeEvent;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { isLoading, isError, data } = useEvent(event.repostId);
|
const { isLoading, isError, data } = useEvent(event.repostId, event.content);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Note.Root
|
<Note.Root className={cn("", className)}>
|
||||||
className={cn(
|
|
||||||
"bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center h-20 gap-2">
|
<div className="flex items-center justify-center h-20 gap-2">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -41,7 +36,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 />
|
||||||
|
|||||||
@@ -12,18 +12,13 @@ export const TextNote = memo(function TextNote({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
<Note.Root
|
<Note.Root className={cn("", className)}>
|
||||||
className={cn(
|
|
||||||
"bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between px-3 h-14">
|
<div className="flex items-center justify-between px-3 h-14">
|
||||||
<Note.User />
|
<Note.User />
|
||||||
<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,6 +15,9 @@ 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) {
|
||||||
|
if (user.profile?.picture.includes("_next/")) {
|
||||||
|
return user.profile?.picture;
|
||||||
|
}
|
||||||
return `${service}?url=${user.profile?.picture}&w=100&h=100&n=-1&default=${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;
|
||||||
|
|||||||
+151
-132
@@ -13,6 +13,8 @@ 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 SetInterestImport } from './routes/set-interest'
|
||||||
|
import { Route as SetGroupImport } from './routes/set-group'
|
||||||
import { Route as LoadingImport } from './routes/loading'
|
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'
|
||||||
@@ -22,17 +24,17 @@ import { Route as ColumnsLayoutImport } from './routes/columns/_layout'
|
|||||||
import { Route as AccountBackupImport } from './routes/$account/backup'
|
import { Route as AccountBackupImport } from './routes/$account/backup'
|
||||||
import { Route as AccountAppImport } from './routes/$account/_app'
|
import { Route as AccountAppImport } from './routes/$account/_app'
|
||||||
import { Route as ColumnsLayoutStoriesImport } from './routes/columns/_layout/stories'
|
import { Route as ColumnsLayoutStoriesImport } from './routes/columns/_layout/stories'
|
||||||
import { Route as ColumnsLayoutGroupImport } from './routes/columns/_layout/group'
|
import { Route as ColumnsLayoutNewsfeedImport } from './routes/columns/_layout/newsfeed'
|
||||||
import { Route as ColumnsLayoutGlobalImport } from './routes/columns/_layout/global'
|
import { Route as ColumnsLayoutGlobalImport } from './routes/columns/_layout/global'
|
||||||
import { Route as ColumnsLayoutGalleryImport } from './routes/columns/_layout/gallery'
|
|
||||||
import { Route as ColumnsLayoutCreateNewsfeedImport } from './routes/columns/_layout/create-newsfeed'
|
import { Route as ColumnsLayoutCreateNewsfeedImport } from './routes/columns/_layout/create-newsfeed'
|
||||||
import { Route as ColumnsLayoutCreateGroupImport } from './routes/columns/_layout/create-group'
|
|
||||||
import { Route as AccountSettingsWalletImport } from './routes/$account/_settings/wallet'
|
import { Route as AccountSettingsWalletImport } from './routes/$account/_settings/wallet'
|
||||||
import { Route as AccountSettingsRelayImport } from './routes/$account/_settings/relay'
|
import { Route as AccountSettingsRelayImport } from './routes/$account/_settings/relay'
|
||||||
import { Route as AccountSettingsProfileImport } from './routes/$account/_settings/profile'
|
import { Route as AccountSettingsProfileImport } from './routes/$account/_settings/profile'
|
||||||
import { Route as AccountSettingsGeneralImport } from './routes/$account/_settings/general'
|
import { Route as AccountSettingsGeneralImport } from './routes/$account/_settings/general'
|
||||||
import { Route as AccountSettingsBitcoinConnectImport } from './routes/$account/_settings/bitcoin-connect'
|
import { Route as AccountSettingsBitcoinConnectImport } from './routes/$account/_settings/bitcoin-connect'
|
||||||
import { Route as AccountAppHomeImport } from './routes/$account/_app/home'
|
import { Route as AccountAppHomeImport } from './routes/$account/_app/home'
|
||||||
|
import { Route as ColumnsLayoutInterestsIdImport } from './routes/columns/_layout/interests.$id'
|
||||||
|
import { Route as ColumnsLayoutGroupsIdImport } from './routes/columns/_layout/groups.$id'
|
||||||
import { Route as ColumnsLayoutCreateNewsfeedUsersImport } from './routes/columns/_layout/create-newsfeed.users'
|
import { Route as ColumnsLayoutCreateNewsfeedUsersImport } from './routes/columns/_layout/create-newsfeed.users'
|
||||||
import { Route as ColumnsLayoutCreateNewsfeedF2fImport } from './routes/columns/_layout/create-newsfeed.f2f'
|
import { Route as ColumnsLayoutCreateNewsfeedF2fImport } from './routes/columns/_layout/create-newsfeed.f2f'
|
||||||
|
|
||||||
@@ -58,8 +60,8 @@ const ColumnsLayoutOnboardingLazyImport = createFileRoute(
|
|||||||
const ColumnsLayoutNotificationLazyImport = createFileRoute(
|
const ColumnsLayoutNotificationLazyImport = createFileRoute(
|
||||||
'/columns/_layout/notification',
|
'/columns/_layout/notification',
|
||||||
)()
|
)()
|
||||||
const ColumnsLayoutNewsfeedLazyImport = createFileRoute(
|
const ColumnsLayoutLaunchpadLazyImport = createFileRoute(
|
||||||
'/columns/_layout/newsfeed',
|
'/columns/_layout/launchpad',
|
||||||
)()
|
)()
|
||||||
const ColumnsLayoutUsersIdLazyImport = createFileRoute(
|
const ColumnsLayoutUsersIdLazyImport = createFileRoute(
|
||||||
'/columns/_layout/users/$id',
|
'/columns/_layout/users/$id',
|
||||||
@@ -67,9 +69,6 @@ const ColumnsLayoutUsersIdLazyImport = createFileRoute(
|
|||||||
const ColumnsLayoutRepliesIdLazyImport = createFileRoute(
|
const ColumnsLayoutRepliesIdLazyImport = createFileRoute(
|
||||||
'/columns/_layout/replies/$id',
|
'/columns/_layout/replies/$id',
|
||||||
)()
|
)()
|
||||||
const ColumnsLayoutHashtagsContentLazyImport = createFileRoute(
|
|
||||||
'/columns/_layout/hashtags/$content',
|
|
||||||
)()
|
|
||||||
const ColumnsLayoutEventsIdLazyImport = createFileRoute(
|
const ColumnsLayoutEventsIdLazyImport = createFileRoute(
|
||||||
'/columns/_layout/events/$id',
|
'/columns/_layout/events/$id',
|
||||||
)()
|
)()
|
||||||
@@ -96,6 +95,16 @@ 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 SetInterestRoute = SetInterestImport.update({
|
||||||
|
path: '/set-interest',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() => import('./routes/set-interest.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
|
const SetGroupRoute = SetGroupImport.update({
|
||||||
|
path: '/set-group',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any).lazy(() => import('./routes/set-group.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const LoadingRoute = LoadingImport.update({
|
const LoadingRoute = LoadingImport.update({
|
||||||
path: '/loading',
|
path: '/loading',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@@ -190,11 +199,13 @@ const ColumnsLayoutNotificationLazyRoute =
|
|||||||
import('./routes/columns/_layout/notification.lazy').then((d) => d.Route),
|
import('./routes/columns/_layout/notification.lazy').then((d) => d.Route),
|
||||||
)
|
)
|
||||||
|
|
||||||
const ColumnsLayoutNewsfeedLazyRoute = ColumnsLayoutNewsfeedLazyImport.update({
|
const ColumnsLayoutLaunchpadLazyRoute = ColumnsLayoutLaunchpadLazyImport.update(
|
||||||
path: '/newsfeed',
|
{
|
||||||
getParentRoute: () => ColumnsLayoutRoute,
|
path: '/launchpad',
|
||||||
} as any).lazy(() =>
|
getParentRoute: () => ColumnsLayoutRoute,
|
||||||
import('./routes/columns/_layout/newsfeed.lazy').then((d) => d.Route),
|
} as any,
|
||||||
|
).lazy(() =>
|
||||||
|
import('./routes/columns/_layout/launchpad.lazy').then((d) => d.Route),
|
||||||
)
|
)
|
||||||
|
|
||||||
const ColumnsLayoutStoriesRoute = ColumnsLayoutStoriesImport.update({
|
const ColumnsLayoutStoriesRoute = ColumnsLayoutStoriesImport.update({
|
||||||
@@ -204,11 +215,11 @@ const ColumnsLayoutStoriesRoute = ColumnsLayoutStoriesImport.update({
|
|||||||
import('./routes/columns/_layout/stories.lazy').then((d) => d.Route),
|
import('./routes/columns/_layout/stories.lazy').then((d) => d.Route),
|
||||||
)
|
)
|
||||||
|
|
||||||
const ColumnsLayoutGroupRoute = ColumnsLayoutGroupImport.update({
|
const ColumnsLayoutNewsfeedRoute = ColumnsLayoutNewsfeedImport.update({
|
||||||
path: '/group',
|
path: '/newsfeed',
|
||||||
getParentRoute: () => ColumnsLayoutRoute,
|
getParentRoute: () => ColumnsLayoutRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
import('./routes/columns/_layout/group.lazy').then((d) => d.Route),
|
import('./routes/columns/_layout/newsfeed.lazy').then((d) => d.Route),
|
||||||
)
|
)
|
||||||
|
|
||||||
const ColumnsLayoutGlobalRoute = ColumnsLayoutGlobalImport.update({
|
const ColumnsLayoutGlobalRoute = ColumnsLayoutGlobalImport.update({
|
||||||
@@ -216,26 +227,12 @@ const ColumnsLayoutGlobalRoute = ColumnsLayoutGlobalImport.update({
|
|||||||
getParentRoute: () => ColumnsLayoutRoute,
|
getParentRoute: () => ColumnsLayoutRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const ColumnsLayoutGalleryRoute = ColumnsLayoutGalleryImport.update({
|
|
||||||
path: '/gallery',
|
|
||||||
getParentRoute: () => ColumnsLayoutRoute,
|
|
||||||
} as any).lazy(() =>
|
|
||||||
import('./routes/columns/_layout/gallery.lazy').then((d) => d.Route),
|
|
||||||
)
|
|
||||||
|
|
||||||
const ColumnsLayoutCreateNewsfeedRoute =
|
const ColumnsLayoutCreateNewsfeedRoute =
|
||||||
ColumnsLayoutCreateNewsfeedImport.update({
|
ColumnsLayoutCreateNewsfeedImport.update({
|
||||||
path: '/create-newsfeed',
|
path: '/create-newsfeed',
|
||||||
getParentRoute: () => ColumnsLayoutRoute,
|
getParentRoute: () => ColumnsLayoutRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const ColumnsLayoutCreateGroupRoute = ColumnsLayoutCreateGroupImport.update({
|
|
||||||
path: '/create-group',
|
|
||||||
getParentRoute: () => ColumnsLayoutRoute,
|
|
||||||
} as any).lazy(() =>
|
|
||||||
import('./routes/columns/_layout/create-group.lazy').then((d) => d.Route),
|
|
||||||
)
|
|
||||||
|
|
||||||
const AccountSettingsWalletRoute = AccountSettingsWalletImport.update({
|
const AccountSettingsWalletRoute = AccountSettingsWalletImport.update({
|
||||||
path: '/wallet',
|
path: '/wallet',
|
||||||
getParentRoute: () => AccountSettingsLazyRoute,
|
getParentRoute: () => AccountSettingsLazyRoute,
|
||||||
@@ -297,16 +294,6 @@ const ColumnsLayoutRepliesIdLazyRoute = ColumnsLayoutRepliesIdLazyImport.update(
|
|||||||
import('./routes/columns/_layout/replies.$id.lazy').then((d) => d.Route),
|
import('./routes/columns/_layout/replies.$id.lazy').then((d) => d.Route),
|
||||||
)
|
)
|
||||||
|
|
||||||
const ColumnsLayoutHashtagsContentLazyRoute =
|
|
||||||
ColumnsLayoutHashtagsContentLazyImport.update({
|
|
||||||
path: '/hashtags/$content',
|
|
||||||
getParentRoute: () => ColumnsLayoutRoute,
|
|
||||||
} as any).lazy(() =>
|
|
||||||
import('./routes/columns/_layout/hashtags.$content.lazy').then(
|
|
||||||
(d) => d.Route,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const ColumnsLayoutEventsIdLazyRoute = ColumnsLayoutEventsIdLazyImport.update({
|
const ColumnsLayoutEventsIdLazyRoute = ColumnsLayoutEventsIdLazyImport.update({
|
||||||
path: '/events/$id',
|
path: '/events/$id',
|
||||||
getParentRoute: () => ColumnsLayoutRoute,
|
getParentRoute: () => ColumnsLayoutRoute,
|
||||||
@@ -314,6 +301,20 @@ const ColumnsLayoutEventsIdLazyRoute = ColumnsLayoutEventsIdLazyImport.update({
|
|||||||
import('./routes/columns/_layout/events.$id.lazy').then((d) => d.Route),
|
import('./routes/columns/_layout/events.$id.lazy').then((d) => d.Route),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ColumnsLayoutInterestsIdRoute = ColumnsLayoutInterestsIdImport.update({
|
||||||
|
path: '/interests/$id',
|
||||||
|
getParentRoute: () => ColumnsLayoutRoute,
|
||||||
|
} as any).lazy(() =>
|
||||||
|
import('./routes/columns/_layout/interests.$id.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
|
const ColumnsLayoutGroupsIdRoute = ColumnsLayoutGroupsIdImport.update({
|
||||||
|
path: '/groups/$id',
|
||||||
|
getParentRoute: () => ColumnsLayoutRoute,
|
||||||
|
} as any).lazy(() =>
|
||||||
|
import('./routes/columns/_layout/groups.$id.lazy').then((d) => d.Route),
|
||||||
|
)
|
||||||
|
|
||||||
const ColumnsLayoutCreateNewsfeedUsersRoute =
|
const ColumnsLayoutCreateNewsfeedUsersRoute =
|
||||||
ColumnsLayoutCreateNewsfeedUsersImport.update({
|
ColumnsLayoutCreateNewsfeedUsersImport.update({
|
||||||
path: '/users',
|
path: '/users',
|
||||||
@@ -351,6 +352,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof LoadingImport
|
preLoaderRoute: typeof LoadingImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/set-group': {
|
||||||
|
id: '/set-group'
|
||||||
|
path: '/set-group'
|
||||||
|
fullPath: '/set-group'
|
||||||
|
preLoaderRoute: typeof SetGroupImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/set-interest': {
|
||||||
|
id: '/set-interest'
|
||||||
|
path: '/set-interest'
|
||||||
|
fullPath: '/set-interest'
|
||||||
|
preLoaderRoute: typeof SetInterestImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/new': {
|
'/new': {
|
||||||
id: '/new'
|
id: '/new'
|
||||||
path: '/new'
|
path: '/new'
|
||||||
@@ -484,13 +499,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AccountSettingsWalletImport
|
preLoaderRoute: typeof AccountSettingsWalletImport
|
||||||
parentRoute: typeof AccountSettingsLazyImport
|
parentRoute: typeof AccountSettingsLazyImport
|
||||||
}
|
}
|
||||||
'/columns/_layout/create-group': {
|
|
||||||
id: '/columns/_layout/create-group'
|
|
||||||
path: '/create-group'
|
|
||||||
fullPath: '/columns/create-group'
|
|
||||||
preLoaderRoute: typeof ColumnsLayoutCreateGroupImport
|
|
||||||
parentRoute: typeof ColumnsLayoutImport
|
|
||||||
}
|
|
||||||
'/columns/_layout/create-newsfeed': {
|
'/columns/_layout/create-newsfeed': {
|
||||||
id: '/columns/_layout/create-newsfeed'
|
id: '/columns/_layout/create-newsfeed'
|
||||||
path: '/create-newsfeed'
|
path: '/create-newsfeed'
|
||||||
@@ -498,13 +506,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ColumnsLayoutCreateNewsfeedImport
|
preLoaderRoute: typeof ColumnsLayoutCreateNewsfeedImport
|
||||||
parentRoute: typeof ColumnsLayoutImport
|
parentRoute: typeof ColumnsLayoutImport
|
||||||
}
|
}
|
||||||
'/columns/_layout/gallery': {
|
|
||||||
id: '/columns/_layout/gallery'
|
|
||||||
path: '/gallery'
|
|
||||||
fullPath: '/columns/gallery'
|
|
||||||
preLoaderRoute: typeof ColumnsLayoutGalleryImport
|
|
||||||
parentRoute: typeof ColumnsLayoutImport
|
|
||||||
}
|
|
||||||
'/columns/_layout/global': {
|
'/columns/_layout/global': {
|
||||||
id: '/columns/_layout/global'
|
id: '/columns/_layout/global'
|
||||||
path: '/global'
|
path: '/global'
|
||||||
@@ -512,11 +513,11 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ColumnsLayoutGlobalImport
|
preLoaderRoute: typeof ColumnsLayoutGlobalImport
|
||||||
parentRoute: typeof ColumnsLayoutImport
|
parentRoute: typeof ColumnsLayoutImport
|
||||||
}
|
}
|
||||||
'/columns/_layout/group': {
|
'/columns/_layout/newsfeed': {
|
||||||
id: '/columns/_layout/group'
|
id: '/columns/_layout/newsfeed'
|
||||||
path: '/group'
|
path: '/newsfeed'
|
||||||
fullPath: '/columns/group'
|
fullPath: '/columns/newsfeed'
|
||||||
preLoaderRoute: typeof ColumnsLayoutGroupImport
|
preLoaderRoute: typeof ColumnsLayoutNewsfeedImport
|
||||||
parentRoute: typeof ColumnsLayoutImport
|
parentRoute: typeof ColumnsLayoutImport
|
||||||
}
|
}
|
||||||
'/columns/_layout/stories': {
|
'/columns/_layout/stories': {
|
||||||
@@ -526,11 +527,11 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ColumnsLayoutStoriesImport
|
preLoaderRoute: typeof ColumnsLayoutStoriesImport
|
||||||
parentRoute: typeof ColumnsLayoutImport
|
parentRoute: typeof ColumnsLayoutImport
|
||||||
}
|
}
|
||||||
'/columns/_layout/newsfeed': {
|
'/columns/_layout/launchpad': {
|
||||||
id: '/columns/_layout/newsfeed'
|
id: '/columns/_layout/launchpad'
|
||||||
path: '/newsfeed'
|
path: '/launchpad'
|
||||||
fullPath: '/columns/newsfeed'
|
fullPath: '/columns/launchpad'
|
||||||
preLoaderRoute: typeof ColumnsLayoutNewsfeedLazyImport
|
preLoaderRoute: typeof ColumnsLayoutLaunchpadLazyImport
|
||||||
parentRoute: typeof ColumnsLayoutImport
|
parentRoute: typeof ColumnsLayoutImport
|
||||||
}
|
}
|
||||||
'/columns/_layout/notification': {
|
'/columns/_layout/notification': {
|
||||||
@@ -575,6 +576,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ColumnsLayoutCreateNewsfeedUsersImport
|
preLoaderRoute: typeof ColumnsLayoutCreateNewsfeedUsersImport
|
||||||
parentRoute: typeof ColumnsLayoutCreateNewsfeedImport
|
parentRoute: typeof ColumnsLayoutCreateNewsfeedImport
|
||||||
}
|
}
|
||||||
|
'/columns/_layout/groups/$id': {
|
||||||
|
id: '/columns/_layout/groups/$id'
|
||||||
|
path: '/groups/$id'
|
||||||
|
fullPath: '/columns/groups/$id'
|
||||||
|
preLoaderRoute: typeof ColumnsLayoutGroupsIdImport
|
||||||
|
parentRoute: typeof ColumnsLayoutImport
|
||||||
|
}
|
||||||
|
'/columns/_layout/interests/$id': {
|
||||||
|
id: '/columns/_layout/interests/$id'
|
||||||
|
path: '/interests/$id'
|
||||||
|
fullPath: '/columns/interests/$id'
|
||||||
|
preLoaderRoute: typeof ColumnsLayoutInterestsIdImport
|
||||||
|
parentRoute: typeof ColumnsLayoutImport
|
||||||
|
}
|
||||||
'/columns/_layout/events/$id': {
|
'/columns/_layout/events/$id': {
|
||||||
id: '/columns/_layout/events/$id'
|
id: '/columns/_layout/events/$id'
|
||||||
path: '/events/$id'
|
path: '/events/$id'
|
||||||
@@ -582,13 +597,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ColumnsLayoutEventsIdLazyImport
|
preLoaderRoute: typeof ColumnsLayoutEventsIdLazyImport
|
||||||
parentRoute: typeof ColumnsLayoutImport
|
parentRoute: typeof ColumnsLayoutImport
|
||||||
}
|
}
|
||||||
'/columns/_layout/hashtags/$content': {
|
|
||||||
id: '/columns/_layout/hashtags/$content'
|
|
||||||
path: '/hashtags/$content'
|
|
||||||
fullPath: '/columns/hashtags/$content'
|
|
||||||
preLoaderRoute: typeof ColumnsLayoutHashtagsContentLazyImport
|
|
||||||
parentRoute: typeof ColumnsLayoutImport
|
|
||||||
}
|
|
||||||
'/columns/_layout/replies/$id': {
|
'/columns/_layout/replies/$id': {
|
||||||
id: '/columns/_layout/replies/$id'
|
id: '/columns/_layout/replies/$id'
|
||||||
path: '/replies/$id'
|
path: '/replies/$id'
|
||||||
@@ -672,38 +680,36 @@ const ColumnsLayoutCreateNewsfeedRouteWithChildren =
|
|||||||
)
|
)
|
||||||
|
|
||||||
interface ColumnsLayoutRouteChildren {
|
interface ColumnsLayoutRouteChildren {
|
||||||
ColumnsLayoutCreateGroupRoute: typeof ColumnsLayoutCreateGroupRoute
|
|
||||||
ColumnsLayoutCreateNewsfeedRoute: typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
ColumnsLayoutCreateNewsfeedRoute: typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||||
ColumnsLayoutGalleryRoute: typeof ColumnsLayoutGalleryRoute
|
|
||||||
ColumnsLayoutGlobalRoute: typeof ColumnsLayoutGlobalRoute
|
ColumnsLayoutGlobalRoute: typeof ColumnsLayoutGlobalRoute
|
||||||
ColumnsLayoutGroupRoute: typeof ColumnsLayoutGroupRoute
|
ColumnsLayoutNewsfeedRoute: typeof ColumnsLayoutNewsfeedRoute
|
||||||
ColumnsLayoutStoriesRoute: typeof ColumnsLayoutStoriesRoute
|
ColumnsLayoutStoriesRoute: typeof ColumnsLayoutStoriesRoute
|
||||||
ColumnsLayoutNewsfeedLazyRoute: typeof ColumnsLayoutNewsfeedLazyRoute
|
ColumnsLayoutLaunchpadLazyRoute: typeof ColumnsLayoutLaunchpadLazyRoute
|
||||||
ColumnsLayoutNotificationLazyRoute: typeof ColumnsLayoutNotificationLazyRoute
|
ColumnsLayoutNotificationLazyRoute: typeof ColumnsLayoutNotificationLazyRoute
|
||||||
ColumnsLayoutOnboardingLazyRoute: typeof ColumnsLayoutOnboardingLazyRoute
|
ColumnsLayoutOnboardingLazyRoute: typeof ColumnsLayoutOnboardingLazyRoute
|
||||||
ColumnsLayoutSearchLazyRoute: typeof ColumnsLayoutSearchLazyRoute
|
ColumnsLayoutSearchLazyRoute: typeof ColumnsLayoutSearchLazyRoute
|
||||||
ColumnsLayoutTrendingLazyRoute: typeof ColumnsLayoutTrendingLazyRoute
|
ColumnsLayoutTrendingLazyRoute: typeof ColumnsLayoutTrendingLazyRoute
|
||||||
|
ColumnsLayoutGroupsIdRoute: typeof ColumnsLayoutGroupsIdRoute
|
||||||
|
ColumnsLayoutInterestsIdRoute: typeof ColumnsLayoutInterestsIdRoute
|
||||||
ColumnsLayoutEventsIdLazyRoute: typeof ColumnsLayoutEventsIdLazyRoute
|
ColumnsLayoutEventsIdLazyRoute: typeof ColumnsLayoutEventsIdLazyRoute
|
||||||
ColumnsLayoutHashtagsContentLazyRoute: typeof ColumnsLayoutHashtagsContentLazyRoute
|
|
||||||
ColumnsLayoutRepliesIdLazyRoute: typeof ColumnsLayoutRepliesIdLazyRoute
|
ColumnsLayoutRepliesIdLazyRoute: typeof ColumnsLayoutRepliesIdLazyRoute
|
||||||
ColumnsLayoutUsersIdLazyRoute: typeof ColumnsLayoutUsersIdLazyRoute
|
ColumnsLayoutUsersIdLazyRoute: typeof ColumnsLayoutUsersIdLazyRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const ColumnsLayoutRouteChildren: ColumnsLayoutRouteChildren = {
|
const ColumnsLayoutRouteChildren: ColumnsLayoutRouteChildren = {
|
||||||
ColumnsLayoutCreateGroupRoute: ColumnsLayoutCreateGroupRoute,
|
|
||||||
ColumnsLayoutCreateNewsfeedRoute:
|
ColumnsLayoutCreateNewsfeedRoute:
|
||||||
ColumnsLayoutCreateNewsfeedRouteWithChildren,
|
ColumnsLayoutCreateNewsfeedRouteWithChildren,
|
||||||
ColumnsLayoutGalleryRoute: ColumnsLayoutGalleryRoute,
|
|
||||||
ColumnsLayoutGlobalRoute: ColumnsLayoutGlobalRoute,
|
ColumnsLayoutGlobalRoute: ColumnsLayoutGlobalRoute,
|
||||||
ColumnsLayoutGroupRoute: ColumnsLayoutGroupRoute,
|
ColumnsLayoutNewsfeedRoute: ColumnsLayoutNewsfeedRoute,
|
||||||
ColumnsLayoutStoriesRoute: ColumnsLayoutStoriesRoute,
|
ColumnsLayoutStoriesRoute: ColumnsLayoutStoriesRoute,
|
||||||
ColumnsLayoutNewsfeedLazyRoute: ColumnsLayoutNewsfeedLazyRoute,
|
ColumnsLayoutLaunchpadLazyRoute: ColumnsLayoutLaunchpadLazyRoute,
|
||||||
ColumnsLayoutNotificationLazyRoute: ColumnsLayoutNotificationLazyRoute,
|
ColumnsLayoutNotificationLazyRoute: ColumnsLayoutNotificationLazyRoute,
|
||||||
ColumnsLayoutOnboardingLazyRoute: ColumnsLayoutOnboardingLazyRoute,
|
ColumnsLayoutOnboardingLazyRoute: ColumnsLayoutOnboardingLazyRoute,
|
||||||
ColumnsLayoutSearchLazyRoute: ColumnsLayoutSearchLazyRoute,
|
ColumnsLayoutSearchLazyRoute: ColumnsLayoutSearchLazyRoute,
|
||||||
ColumnsLayoutTrendingLazyRoute: ColumnsLayoutTrendingLazyRoute,
|
ColumnsLayoutTrendingLazyRoute: ColumnsLayoutTrendingLazyRoute,
|
||||||
|
ColumnsLayoutGroupsIdRoute: ColumnsLayoutGroupsIdRoute,
|
||||||
|
ColumnsLayoutInterestsIdRoute: ColumnsLayoutInterestsIdRoute,
|
||||||
ColumnsLayoutEventsIdLazyRoute: ColumnsLayoutEventsIdLazyRoute,
|
ColumnsLayoutEventsIdLazyRoute: ColumnsLayoutEventsIdLazyRoute,
|
||||||
ColumnsLayoutHashtagsContentLazyRoute: ColumnsLayoutHashtagsContentLazyRoute,
|
|
||||||
ColumnsLayoutRepliesIdLazyRoute: ColumnsLayoutRepliesIdLazyRoute,
|
ColumnsLayoutRepliesIdLazyRoute: ColumnsLayoutRepliesIdLazyRoute,
|
||||||
ColumnsLayoutUsersIdLazyRoute: ColumnsLayoutUsersIdLazyRoute,
|
ColumnsLayoutUsersIdLazyRoute: ColumnsLayoutUsersIdLazyRoute,
|
||||||
}
|
}
|
||||||
@@ -727,6 +733,8 @@ export interface FileRoutesByFullPath {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||||
'/loading': typeof LoadingRoute
|
'/loading': typeof LoadingRoute
|
||||||
|
'/set-group': typeof SetGroupRoute
|
||||||
|
'/set-interest': typeof SetInterestRoute
|
||||||
'/new': typeof NewLazyRoute
|
'/new': typeof NewLazyRoute
|
||||||
'/reset': typeof ResetLazyRoute
|
'/reset': typeof ResetLazyRoute
|
||||||
'/$account': typeof AccountSettingsLazyRouteWithChildren
|
'/$account': typeof AccountSettingsLazyRouteWithChildren
|
||||||
@@ -743,21 +751,20 @@ export interface FileRoutesByFullPath {
|
|||||||
'/$account/profile': typeof AccountSettingsProfileRoute
|
'/$account/profile': typeof AccountSettingsProfileRoute
|
||||||
'/$account/relay': typeof AccountSettingsRelayRoute
|
'/$account/relay': typeof AccountSettingsRelayRoute
|
||||||
'/$account/wallet': typeof AccountSettingsWalletRoute
|
'/$account/wallet': typeof AccountSettingsWalletRoute
|
||||||
'/columns/create-group': typeof ColumnsLayoutCreateGroupRoute
|
|
||||||
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||||
'/columns/gallery': typeof ColumnsLayoutGalleryRoute
|
|
||||||
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
||||||
'/columns/group': typeof ColumnsLayoutGroupRoute
|
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedRoute
|
||||||
'/columns/stories': typeof ColumnsLayoutStoriesRoute
|
'/columns/stories': typeof ColumnsLayoutStoriesRoute
|
||||||
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedLazyRoute
|
'/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute
|
||||||
'/columns/notification': typeof ColumnsLayoutNotificationLazyRoute
|
'/columns/notification': typeof ColumnsLayoutNotificationLazyRoute
|
||||||
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||||
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
|
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
|
||||||
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
|
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
|
||||||
'/columns/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
'/columns/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
||||||
'/columns/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
'/columns/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
||||||
|
'/columns/groups/$id': typeof ColumnsLayoutGroupsIdRoute
|
||||||
|
'/columns/interests/$id': typeof ColumnsLayoutInterestsIdRoute
|
||||||
'/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
'/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
||||||
'/columns/hashtags/$content': typeof ColumnsLayoutHashtagsContentLazyRoute
|
|
||||||
'/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
'/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
||||||
'/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
'/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
||||||
}
|
}
|
||||||
@@ -766,6 +773,8 @@ export interface FileRoutesByTo {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||||
'/loading': typeof LoadingRoute
|
'/loading': typeof LoadingRoute
|
||||||
|
'/set-group': typeof SetGroupRoute
|
||||||
|
'/set-interest': typeof SetInterestRoute
|
||||||
'/new': typeof NewLazyRoute
|
'/new': typeof NewLazyRoute
|
||||||
'/reset': typeof ResetLazyRoute
|
'/reset': typeof ResetLazyRoute
|
||||||
'/$account': typeof AccountSettingsLazyRouteWithChildren
|
'/$account': typeof AccountSettingsLazyRouteWithChildren
|
||||||
@@ -782,21 +791,20 @@ export interface FileRoutesByTo {
|
|||||||
'/$account/profile': typeof AccountSettingsProfileRoute
|
'/$account/profile': typeof AccountSettingsProfileRoute
|
||||||
'/$account/relay': typeof AccountSettingsRelayRoute
|
'/$account/relay': typeof AccountSettingsRelayRoute
|
||||||
'/$account/wallet': typeof AccountSettingsWalletRoute
|
'/$account/wallet': typeof AccountSettingsWalletRoute
|
||||||
'/columns/create-group': typeof ColumnsLayoutCreateGroupRoute
|
|
||||||
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
'/columns/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||||
'/columns/gallery': typeof ColumnsLayoutGalleryRoute
|
|
||||||
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
'/columns/global': typeof ColumnsLayoutGlobalRoute
|
||||||
'/columns/group': typeof ColumnsLayoutGroupRoute
|
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedRoute
|
||||||
'/columns/stories': typeof ColumnsLayoutStoriesRoute
|
'/columns/stories': typeof ColumnsLayoutStoriesRoute
|
||||||
'/columns/newsfeed': typeof ColumnsLayoutNewsfeedLazyRoute
|
'/columns/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute
|
||||||
'/columns/notification': typeof ColumnsLayoutNotificationLazyRoute
|
'/columns/notification': typeof ColumnsLayoutNotificationLazyRoute
|
||||||
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
'/columns/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||||
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
|
'/columns/search': typeof ColumnsLayoutSearchLazyRoute
|
||||||
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
|
'/columns/trending': typeof ColumnsLayoutTrendingLazyRoute
|
||||||
'/columns/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
'/columns/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
||||||
'/columns/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
'/columns/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
||||||
|
'/columns/groups/$id': typeof ColumnsLayoutGroupsIdRoute
|
||||||
|
'/columns/interests/$id': typeof ColumnsLayoutInterestsIdRoute
|
||||||
'/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
'/columns/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
||||||
'/columns/hashtags/$content': typeof ColumnsLayoutHashtagsContentLazyRoute
|
|
||||||
'/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
'/columns/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
||||||
'/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
'/columns/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
||||||
}
|
}
|
||||||
@@ -806,6 +814,8 @@ export interface FileRoutesById {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
'/bootstrap-relays': typeof BootstrapRelaysRoute
|
||||||
'/loading': typeof LoadingRoute
|
'/loading': typeof LoadingRoute
|
||||||
|
'/set-group': typeof SetGroupRoute
|
||||||
|
'/set-interest': typeof SetInterestRoute
|
||||||
'/new': typeof NewLazyRoute
|
'/new': typeof NewLazyRoute
|
||||||
'/reset': typeof ResetLazyRoute
|
'/reset': typeof ResetLazyRoute
|
||||||
'/$account': typeof AccountRouteWithChildren
|
'/$account': typeof AccountRouteWithChildren
|
||||||
@@ -825,21 +835,20 @@ export interface FileRoutesById {
|
|||||||
'/$account/_settings/profile': typeof AccountSettingsProfileRoute
|
'/$account/_settings/profile': typeof AccountSettingsProfileRoute
|
||||||
'/$account/_settings/relay': typeof AccountSettingsRelayRoute
|
'/$account/_settings/relay': typeof AccountSettingsRelayRoute
|
||||||
'/$account/_settings/wallet': typeof AccountSettingsWalletRoute
|
'/$account/_settings/wallet': typeof AccountSettingsWalletRoute
|
||||||
'/columns/_layout/create-group': typeof ColumnsLayoutCreateGroupRoute
|
|
||||||
'/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
'/columns/_layout/create-newsfeed': typeof ColumnsLayoutCreateNewsfeedRouteWithChildren
|
||||||
'/columns/_layout/gallery': typeof ColumnsLayoutGalleryRoute
|
|
||||||
'/columns/_layout/global': typeof ColumnsLayoutGlobalRoute
|
'/columns/_layout/global': typeof ColumnsLayoutGlobalRoute
|
||||||
'/columns/_layout/group': typeof ColumnsLayoutGroupRoute
|
'/columns/_layout/newsfeed': typeof ColumnsLayoutNewsfeedRoute
|
||||||
'/columns/_layout/stories': typeof ColumnsLayoutStoriesRoute
|
'/columns/_layout/stories': typeof ColumnsLayoutStoriesRoute
|
||||||
'/columns/_layout/newsfeed': typeof ColumnsLayoutNewsfeedLazyRoute
|
'/columns/_layout/launchpad': typeof ColumnsLayoutLaunchpadLazyRoute
|
||||||
'/columns/_layout/notification': typeof ColumnsLayoutNotificationLazyRoute
|
'/columns/_layout/notification': typeof ColumnsLayoutNotificationLazyRoute
|
||||||
'/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
'/columns/_layout/onboarding': typeof ColumnsLayoutOnboardingLazyRoute
|
||||||
'/columns/_layout/search': typeof ColumnsLayoutSearchLazyRoute
|
'/columns/_layout/search': typeof ColumnsLayoutSearchLazyRoute
|
||||||
'/columns/_layout/trending': typeof ColumnsLayoutTrendingLazyRoute
|
'/columns/_layout/trending': typeof ColumnsLayoutTrendingLazyRoute
|
||||||
'/columns/_layout/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
'/columns/_layout/create-newsfeed/f2f': typeof ColumnsLayoutCreateNewsfeedF2fRoute
|
||||||
'/columns/_layout/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
'/columns/_layout/create-newsfeed/users': typeof ColumnsLayoutCreateNewsfeedUsersRoute
|
||||||
|
'/columns/_layout/groups/$id': typeof ColumnsLayoutGroupsIdRoute
|
||||||
|
'/columns/_layout/interests/$id': typeof ColumnsLayoutInterestsIdRoute
|
||||||
'/columns/_layout/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
'/columns/_layout/events/$id': typeof ColumnsLayoutEventsIdLazyRoute
|
||||||
'/columns/_layout/hashtags/$content': typeof ColumnsLayoutHashtagsContentLazyRoute
|
|
||||||
'/columns/_layout/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
'/columns/_layout/replies/$id': typeof ColumnsLayoutRepliesIdLazyRoute
|
||||||
'/columns/_layout/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
'/columns/_layout/users/$id': typeof ColumnsLayoutUsersIdLazyRoute
|
||||||
}
|
}
|
||||||
@@ -850,6 +859,8 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/bootstrap-relays'
|
| '/bootstrap-relays'
|
||||||
| '/loading'
|
| '/loading'
|
||||||
|
| '/set-group'
|
||||||
|
| '/set-interest'
|
||||||
| '/new'
|
| '/new'
|
||||||
| '/reset'
|
| '/reset'
|
||||||
| '/$account'
|
| '/$account'
|
||||||
@@ -866,21 +877,20 @@ export interface FileRouteTypes {
|
|||||||
| '/$account/profile'
|
| '/$account/profile'
|
||||||
| '/$account/relay'
|
| '/$account/relay'
|
||||||
| '/$account/wallet'
|
| '/$account/wallet'
|
||||||
| '/columns/create-group'
|
|
||||||
| '/columns/create-newsfeed'
|
| '/columns/create-newsfeed'
|
||||||
| '/columns/gallery'
|
|
||||||
| '/columns/global'
|
| '/columns/global'
|
||||||
| '/columns/group'
|
|
||||||
| '/columns/stories'
|
|
||||||
| '/columns/newsfeed'
|
| '/columns/newsfeed'
|
||||||
|
| '/columns/stories'
|
||||||
|
| '/columns/launchpad'
|
||||||
| '/columns/notification'
|
| '/columns/notification'
|
||||||
| '/columns/onboarding'
|
| '/columns/onboarding'
|
||||||
| '/columns/search'
|
| '/columns/search'
|
||||||
| '/columns/trending'
|
| '/columns/trending'
|
||||||
| '/columns/create-newsfeed/f2f'
|
| '/columns/create-newsfeed/f2f'
|
||||||
| '/columns/create-newsfeed/users'
|
| '/columns/create-newsfeed/users'
|
||||||
|
| '/columns/groups/$id'
|
||||||
|
| '/columns/interests/$id'
|
||||||
| '/columns/events/$id'
|
| '/columns/events/$id'
|
||||||
| '/columns/hashtags/$content'
|
|
||||||
| '/columns/replies/$id'
|
| '/columns/replies/$id'
|
||||||
| '/columns/users/$id'
|
| '/columns/users/$id'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
@@ -888,6 +898,8 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/bootstrap-relays'
|
| '/bootstrap-relays'
|
||||||
| '/loading'
|
| '/loading'
|
||||||
|
| '/set-group'
|
||||||
|
| '/set-interest'
|
||||||
| '/new'
|
| '/new'
|
||||||
| '/reset'
|
| '/reset'
|
||||||
| '/$account'
|
| '/$account'
|
||||||
@@ -904,21 +916,20 @@ export interface FileRouteTypes {
|
|||||||
| '/$account/profile'
|
| '/$account/profile'
|
||||||
| '/$account/relay'
|
| '/$account/relay'
|
||||||
| '/$account/wallet'
|
| '/$account/wallet'
|
||||||
| '/columns/create-group'
|
|
||||||
| '/columns/create-newsfeed'
|
| '/columns/create-newsfeed'
|
||||||
| '/columns/gallery'
|
|
||||||
| '/columns/global'
|
| '/columns/global'
|
||||||
| '/columns/group'
|
|
||||||
| '/columns/stories'
|
|
||||||
| '/columns/newsfeed'
|
| '/columns/newsfeed'
|
||||||
|
| '/columns/stories'
|
||||||
|
| '/columns/launchpad'
|
||||||
| '/columns/notification'
|
| '/columns/notification'
|
||||||
| '/columns/onboarding'
|
| '/columns/onboarding'
|
||||||
| '/columns/search'
|
| '/columns/search'
|
||||||
| '/columns/trending'
|
| '/columns/trending'
|
||||||
| '/columns/create-newsfeed/f2f'
|
| '/columns/create-newsfeed/f2f'
|
||||||
| '/columns/create-newsfeed/users'
|
| '/columns/create-newsfeed/users'
|
||||||
|
| '/columns/groups/$id'
|
||||||
|
| '/columns/interests/$id'
|
||||||
| '/columns/events/$id'
|
| '/columns/events/$id'
|
||||||
| '/columns/hashtags/$content'
|
|
||||||
| '/columns/replies/$id'
|
| '/columns/replies/$id'
|
||||||
| '/columns/users/$id'
|
| '/columns/users/$id'
|
||||||
id:
|
id:
|
||||||
@@ -926,6 +937,8 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/bootstrap-relays'
|
| '/bootstrap-relays'
|
||||||
| '/loading'
|
| '/loading'
|
||||||
|
| '/set-group'
|
||||||
|
| '/set-interest'
|
||||||
| '/new'
|
| '/new'
|
||||||
| '/reset'
|
| '/reset'
|
||||||
| '/$account'
|
| '/$account'
|
||||||
@@ -945,21 +958,20 @@ export interface FileRouteTypes {
|
|||||||
| '/$account/_settings/profile'
|
| '/$account/_settings/profile'
|
||||||
| '/$account/_settings/relay'
|
| '/$account/_settings/relay'
|
||||||
| '/$account/_settings/wallet'
|
| '/$account/_settings/wallet'
|
||||||
| '/columns/_layout/create-group'
|
|
||||||
| '/columns/_layout/create-newsfeed'
|
| '/columns/_layout/create-newsfeed'
|
||||||
| '/columns/_layout/gallery'
|
|
||||||
| '/columns/_layout/global'
|
| '/columns/_layout/global'
|
||||||
| '/columns/_layout/group'
|
|
||||||
| '/columns/_layout/stories'
|
|
||||||
| '/columns/_layout/newsfeed'
|
| '/columns/_layout/newsfeed'
|
||||||
|
| '/columns/_layout/stories'
|
||||||
|
| '/columns/_layout/launchpad'
|
||||||
| '/columns/_layout/notification'
|
| '/columns/_layout/notification'
|
||||||
| '/columns/_layout/onboarding'
|
| '/columns/_layout/onboarding'
|
||||||
| '/columns/_layout/search'
|
| '/columns/_layout/search'
|
||||||
| '/columns/_layout/trending'
|
| '/columns/_layout/trending'
|
||||||
| '/columns/_layout/create-newsfeed/f2f'
|
| '/columns/_layout/create-newsfeed/f2f'
|
||||||
| '/columns/_layout/create-newsfeed/users'
|
| '/columns/_layout/create-newsfeed/users'
|
||||||
|
| '/columns/_layout/groups/$id'
|
||||||
|
| '/columns/_layout/interests/$id'
|
||||||
| '/columns/_layout/events/$id'
|
| '/columns/_layout/events/$id'
|
||||||
| '/columns/_layout/hashtags/$content'
|
|
||||||
| '/columns/_layout/replies/$id'
|
| '/columns/_layout/replies/$id'
|
||||||
| '/columns/_layout/users/$id'
|
| '/columns/_layout/users/$id'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
@@ -969,6 +981,8 @@ export interface RootRouteChildren {
|
|||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
BootstrapRelaysRoute: typeof BootstrapRelaysRoute
|
BootstrapRelaysRoute: typeof BootstrapRelaysRoute
|
||||||
LoadingRoute: typeof LoadingRoute
|
LoadingRoute: typeof LoadingRoute
|
||||||
|
SetGroupRoute: typeof SetGroupRoute
|
||||||
|
SetInterestRoute: typeof SetInterestRoute
|
||||||
NewLazyRoute: typeof NewLazyRoute
|
NewLazyRoute: typeof NewLazyRoute
|
||||||
ResetLazyRoute: typeof ResetLazyRoute
|
ResetLazyRoute: typeof ResetLazyRoute
|
||||||
AccountRoute: typeof AccountRouteWithChildren
|
AccountRoute: typeof AccountRouteWithChildren
|
||||||
@@ -984,6 +998,8 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
BootstrapRelaysRoute: BootstrapRelaysRoute,
|
BootstrapRelaysRoute: BootstrapRelaysRoute,
|
||||||
LoadingRoute: LoadingRoute,
|
LoadingRoute: LoadingRoute,
|
||||||
|
SetGroupRoute: SetGroupRoute,
|
||||||
|
SetInterestRoute: SetInterestRoute,
|
||||||
NewLazyRoute: NewLazyRoute,
|
NewLazyRoute: NewLazyRoute,
|
||||||
ResetLazyRoute: ResetLazyRoute,
|
ResetLazyRoute: ResetLazyRoute,
|
||||||
AccountRoute: AccountRouteWithChildren,
|
AccountRoute: AccountRouteWithChildren,
|
||||||
@@ -1010,6 +1026,8 @@ export const routeTree = rootRoute
|
|||||||
"/",
|
"/",
|
||||||
"/bootstrap-relays",
|
"/bootstrap-relays",
|
||||||
"/loading",
|
"/loading",
|
||||||
|
"/set-group",
|
||||||
|
"/set-interest",
|
||||||
"/new",
|
"/new",
|
||||||
"/reset",
|
"/reset",
|
||||||
"/$account",
|
"/$account",
|
||||||
@@ -1030,6 +1048,12 @@ export const routeTree = rootRoute
|
|||||||
"/loading": {
|
"/loading": {
|
||||||
"filePath": "loading.tsx"
|
"filePath": "loading.tsx"
|
||||||
},
|
},
|
||||||
|
"/set-group": {
|
||||||
|
"filePath": "set-group.tsx"
|
||||||
|
},
|
||||||
|
"/set-interest": {
|
||||||
|
"filePath": "set-interest.tsx"
|
||||||
|
},
|
||||||
"/new": {
|
"/new": {
|
||||||
"filePath": "new.lazy.tsx"
|
"filePath": "new.lazy.tsx"
|
||||||
},
|
},
|
||||||
@@ -1065,19 +1089,18 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "columns/_layout.tsx",
|
"filePath": "columns/_layout.tsx",
|
||||||
"parent": "/columns",
|
"parent": "/columns",
|
||||||
"children": [
|
"children": [
|
||||||
"/columns/_layout/create-group",
|
|
||||||
"/columns/_layout/create-newsfeed",
|
"/columns/_layout/create-newsfeed",
|
||||||
"/columns/_layout/gallery",
|
|
||||||
"/columns/_layout/global",
|
"/columns/_layout/global",
|
||||||
"/columns/_layout/group",
|
|
||||||
"/columns/_layout/stories",
|
|
||||||
"/columns/_layout/newsfeed",
|
"/columns/_layout/newsfeed",
|
||||||
|
"/columns/_layout/stories",
|
||||||
|
"/columns/_layout/launchpad",
|
||||||
"/columns/_layout/notification",
|
"/columns/_layout/notification",
|
||||||
"/columns/_layout/onboarding",
|
"/columns/_layout/onboarding",
|
||||||
"/columns/_layout/search",
|
"/columns/_layout/search",
|
||||||
"/columns/_layout/trending",
|
"/columns/_layout/trending",
|
||||||
|
"/columns/_layout/groups/$id",
|
||||||
|
"/columns/_layout/interests/$id",
|
||||||
"/columns/_layout/events/$id",
|
"/columns/_layout/events/$id",
|
||||||
"/columns/_layout/hashtags/$content",
|
|
||||||
"/columns/_layout/replies/$id",
|
"/columns/_layout/replies/$id",
|
||||||
"/columns/_layout/users/$id"
|
"/columns/_layout/users/$id"
|
||||||
]
|
]
|
||||||
@@ -1132,10 +1155,6 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "$account/_settings/wallet.tsx",
|
"filePath": "$account/_settings/wallet.tsx",
|
||||||
"parent": "/$account/_settings"
|
"parent": "/$account/_settings"
|
||||||
},
|
},
|
||||||
"/columns/_layout/create-group": {
|
|
||||||
"filePath": "columns/_layout/create-group.tsx",
|
|
||||||
"parent": "/columns/_layout"
|
|
||||||
},
|
|
||||||
"/columns/_layout/create-newsfeed": {
|
"/columns/_layout/create-newsfeed": {
|
||||||
"filePath": "columns/_layout/create-newsfeed.tsx",
|
"filePath": "columns/_layout/create-newsfeed.tsx",
|
||||||
"parent": "/columns/_layout",
|
"parent": "/columns/_layout",
|
||||||
@@ -1144,24 +1163,20 @@ export const routeTree = rootRoute
|
|||||||
"/columns/_layout/create-newsfeed/users"
|
"/columns/_layout/create-newsfeed/users"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/columns/_layout/gallery": {
|
|
||||||
"filePath": "columns/_layout/gallery.tsx",
|
|
||||||
"parent": "/columns/_layout"
|
|
||||||
},
|
|
||||||
"/columns/_layout/global": {
|
"/columns/_layout/global": {
|
||||||
"filePath": "columns/_layout/global.tsx",
|
"filePath": "columns/_layout/global.tsx",
|
||||||
"parent": "/columns/_layout"
|
"parent": "/columns/_layout"
|
||||||
},
|
},
|
||||||
"/columns/_layout/group": {
|
"/columns/_layout/newsfeed": {
|
||||||
"filePath": "columns/_layout/group.tsx",
|
"filePath": "columns/_layout/newsfeed.tsx",
|
||||||
"parent": "/columns/_layout"
|
"parent": "/columns/_layout"
|
||||||
},
|
},
|
||||||
"/columns/_layout/stories": {
|
"/columns/_layout/stories": {
|
||||||
"filePath": "columns/_layout/stories.tsx",
|
"filePath": "columns/_layout/stories.tsx",
|
||||||
"parent": "/columns/_layout"
|
"parent": "/columns/_layout"
|
||||||
},
|
},
|
||||||
"/columns/_layout/newsfeed": {
|
"/columns/_layout/launchpad": {
|
||||||
"filePath": "columns/_layout/newsfeed.lazy.tsx",
|
"filePath": "columns/_layout/launchpad.lazy.tsx",
|
||||||
"parent": "/columns/_layout"
|
"parent": "/columns/_layout"
|
||||||
},
|
},
|
||||||
"/columns/_layout/notification": {
|
"/columns/_layout/notification": {
|
||||||
@@ -1188,12 +1203,16 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "columns/_layout/create-newsfeed.users.tsx",
|
"filePath": "columns/_layout/create-newsfeed.users.tsx",
|
||||||
"parent": "/columns/_layout/create-newsfeed"
|
"parent": "/columns/_layout/create-newsfeed"
|
||||||
},
|
},
|
||||||
"/columns/_layout/events/$id": {
|
"/columns/_layout/groups/$id": {
|
||||||
"filePath": "columns/_layout/events.$id.lazy.tsx",
|
"filePath": "columns/_layout/groups.$id.tsx",
|
||||||
"parent": "/columns/_layout"
|
"parent": "/columns/_layout"
|
||||||
},
|
},
|
||||||
"/columns/_layout/hashtags/$content": {
|
"/columns/_layout/interests/$id": {
|
||||||
"filePath": "columns/_layout/hashtags.$content.lazy.tsx",
|
"filePath": "columns/_layout/interests.$id.tsx",
|
||||||
|
"parent": "/columns/_layout"
|
||||||
|
},
|
||||||
|
"/columns/_layout/events/$id": {
|
||||||
|
"filePath": "columns/_layout/events.$id.lazy.tsx",
|
||||||
"parent": "/columns/_layout"
|
"parent": "/columns/_layout"
|
||||||
},
|
},
|
||||||
"/columns/_layout/replies/$id": {
|
"/columns/_layout/replies/$id": {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { appSettings, cn } from "@/commons";
|
import { cn } from "@/commons";
|
||||||
import { User } from "@/components/user";
|
import { User } from "@/components/user";
|
||||||
import { LumeWindow } from "@/system";
|
import { LumeWindow } from "@/system";
|
||||||
import { CaretDown, Feather, MagnifyingGlass } from "@phosphor-icons/react";
|
import { CaretDown, Feather, MagnifyingGlass } from "@phosphor-icons/react";
|
||||||
import { Outlet, createLazyFileRoute } from "@tanstack/react-router";
|
import { Outlet, createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { useStore } from "@tanstack/react-store";
|
|
||||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
import { memo, useCallback } from "react";
|
import { memo, useCallback } from "react";
|
||||||
@@ -14,7 +13,6 @@ export const Route = createLazyFileRoute("/$account/_app")({
|
|||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const context = Route.useRouteContext();
|
const context = Route.useRouteContext();
|
||||||
const transparent = useStore(appSettings, (state) => state.transparent);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-screen h-screen">
|
<div className="flex flex-col w-screen h-screen">
|
||||||
@@ -54,14 +52,7 @@ function Screen() {
|
|||||||
className="relative z-[200] flex-1 flex items-center justify-end gap-1"
|
className="relative z-[200] flex-1 flex items-center justify-end gap-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="flex-1 bg-neutral-100 dark:bg-neutral-900 border-t-[.5px] border-black/20 dark:border-white/20">
|
||||||
className={cn(
|
|
||||||
"flex-1",
|
|
||||||
transparent
|
|
||||||
? ""
|
|
||||||
: "bg-white dark:bg-black border-t border-black/20 dark:border-white/20",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
import type { LumeColumn } from "@/types";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { resolveResource } from "@tauri-apps/api/path";
|
|
||||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account/_app")({
|
export const Route = createFileRoute("/$account/_app")();
|
||||||
beforeLoad: async () => {
|
|
||||||
const systemPath = "resources/columns.json";
|
|
||||||
const resourcePath = await resolveResource(systemPath);
|
|
||||||
const resourceFile = await readTextFile(resourcePath);
|
|
||||||
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
|
|
||||||
|
|
||||||
return { systemColumns };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { appColumns } from "@/commons";
|
||||||
import { Spinner } from "@/components";
|
import { Spinner } from "@/components";
|
||||||
import { Column } from "@/components/column";
|
import { Column } from "@/components/column";
|
||||||
import { LumeWindow } from "@/system";
|
import { LumeWindow } from "@/system";
|
||||||
import type { ColumnEvent, LumeColumn } from "@/types";
|
import type { ColumnEvent, LumeColumn } from "@/types";
|
||||||
import { ArrowLeft, ArrowRight, Plus, StackPlus } from "@phosphor-icons/react";
|
import { ArrowLeft, ArrowRight, Plus, StackPlus } from "@phosphor-icons/react";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { useStore } from "@tanstack/react-store";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||||
|
import { resolveResource } from "@tauri-apps/api/path";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||||
import useEmblaCarousel from "embla-carousel-react";
|
import useEmblaCarousel from "embla-carousel-react";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import {
|
import {
|
||||||
@@ -25,9 +28,9 @@ export const Route = createLazyFileRoute("/$account/_app/home")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { initialColumns } = Route.useRouteContext();
|
const params = Route.useParams();
|
||||||
|
const columns = useStore(appColumns, (state) => state);
|
||||||
|
|
||||||
const [columns, setColumns] = useState<LumeColumn[]>([]);
|
|
||||||
const [emblaRef, emblaApi] = useEmblaCarousel({
|
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||||
watchDrag: false,
|
watchDrag: false,
|
||||||
loop: false,
|
loop: false,
|
||||||
@@ -51,11 +54,11 @@ function Screen() {
|
|||||||
|
|
||||||
const add = useDebouncedCallback((column: LumeColumn) => {
|
const add = useDebouncedCallback((column: LumeColumn) => {
|
||||||
column.label = `${column.label}-${nanoid()}`; // update col label
|
column.label = `${column.label}-${nanoid()}`; // update col label
|
||||||
setColumns((prev) => [column, ...prev]);
|
appColumns.setState((prev) => [column, ...prev]);
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
const remove = useDebouncedCallback((label: string) => {
|
const remove = useDebouncedCallback((label: string) => {
|
||||||
setColumns((prev) => prev.filter((t) => t.label !== label));
|
appColumns.setState((prev) => prev.filter((t) => t.label !== label));
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
const move = useDebouncedCallback(
|
const move = useDebouncedCallback(
|
||||||
@@ -70,12 +73,12 @@ function Screen() {
|
|||||||
if (direction === "left") newCols.splice(colIndex - 1, 0, col);
|
if (direction === "left") newCols.splice(colIndex - 1, 0, col);
|
||||||
if (direction === "right") newCols.splice(colIndex + 1, 0, col);
|
if (direction === "right") newCols.splice(colIndex + 1, 0, col);
|
||||||
|
|
||||||
setColumns(newCols);
|
appColumns.setState(() => newCols);
|
||||||
},
|
},
|
||||||
150,
|
150,
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateName = useDebouncedCallback((label: string, title: string) => {
|
const update = useDebouncedCallback((label: string, title: string) => {
|
||||||
const currentColIndex = columns.findIndex((col) => col.label === label);
|
const currentColIndex = columns.findIndex((col) => col.label === label);
|
||||||
|
|
||||||
const updatedCol = Object.assign({}, columns[currentColIndex]);
|
const updatedCol = Object.assign({}, columns[currentColIndex]);
|
||||||
@@ -84,10 +87,10 @@ function Screen() {
|
|||||||
const newCols = columns.slice();
|
const newCols = columns.slice();
|
||||||
newCols[currentColIndex] = updatedCol;
|
newCols[currentColIndex] = updatedCol;
|
||||||
|
|
||||||
setColumns(newCols);
|
appColumns.setState(() => newCols);
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
const reset = useDebouncedCallback(() => setColumns([]), 150);
|
const reset = useDebouncedCallback(() => appColumns.setState(() => []), 150);
|
||||||
|
|
||||||
const handleKeyDown = useDebouncedCallback((event) => {
|
const handleKeyDown = useDebouncedCallback((event) => {
|
||||||
if (event.defaultPrevented) return;
|
if (event.defaultPrevented) return;
|
||||||
@@ -106,18 +109,6 @@ function Screen() {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
const saveAllColumns = useDebouncedCallback(async () => {
|
|
||||||
const key = "lume_v4:columns";
|
|
||||||
const content = JSON.stringify(columns);
|
|
||||||
const res = await commands.setLumeStore(key, content);
|
|
||||||
|
|
||||||
if (res.status === "ok") {
|
|
||||||
return res.data;
|
|
||||||
} else {
|
|
||||||
console.log(res.error);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (emblaApi) {
|
if (emblaApi) {
|
||||||
emblaApi.on("scroll", emitScrollEvent);
|
emblaApi.on("scroll", emitScrollEvent);
|
||||||
@@ -132,14 +123,6 @@ function Screen() {
|
|||||||
};
|
};
|
||||||
}, [emblaApi, emitScrollEvent, emitResizeEvent]);
|
}, [emblaApi, emitScrollEvent, emitResizeEvent]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (columns) saveAllColumns();
|
|
||||||
}, [columns]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setColumns(initialColumns);
|
|
||||||
}, [initialColumns]);
|
|
||||||
|
|
||||||
// Listen for keyboard event
|
// Listen for keyboard event
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
@@ -158,7 +141,7 @@ function Screen() {
|
|||||||
if (data.payload.type === "move")
|
if (data.payload.type === "move")
|
||||||
move(data.payload.label, data.payload.direction);
|
move(data.payload.label, data.payload.direction);
|
||||||
if (data.payload.type === "set_title")
|
if (data.payload.type === "set_title")
|
||||||
updateName(data.payload.label, data.payload.title);
|
update(data.payload.label, data.payload.title);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -166,6 +149,35 @@ function Screen() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getSystemColumns() {
|
||||||
|
const systemPath = "resources/columns.json";
|
||||||
|
const resourcePath = await resolveResource(systemPath);
|
||||||
|
const resourceFile = await readTextFile(resourcePath);
|
||||||
|
const cols: LumeColumn[] = JSON.parse(resourceFile);
|
||||||
|
|
||||||
|
appColumns.setState(() => cols.filter((col) => col.default));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!columns.length) {
|
||||||
|
const prevColumns = window.localStorage.getItem(
|
||||||
|
`${params.account}_columns`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!prevColumns) {
|
||||||
|
getSystemColumns();
|
||||||
|
} else {
|
||||||
|
const parsed: LumeColumn[] = JSON.parse(prevColumns);
|
||||||
|
appColumns.setState(() => parsed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
`${params.account}_columns`,
|
||||||
|
JSON.stringify(columns),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [columns.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full">
|
<div className="size-full">
|
||||||
<div ref={emblaRef} className="overflow-hidden size-full">
|
<div ref={emblaRef} className="overflow-hidden size-full">
|
||||||
@@ -179,8 +191,8 @@ function Screen() {
|
|||||||
<Column key={column.label} column={column} />
|
<Column key={column.label} column={column} />
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
<div className="shrink-0 p-2 h-full w-[450px]">
|
<div className="shrink-0 p-2 h-full w-[440px]">
|
||||||
<div className="size-full bg-black/5 dark:bg-white/15 rounded-xl flex items-center justify-center">
|
<div className="size-full flex items-center justify-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => LumeWindow.openColumnsGallery()}
|
onClick={() => LumeWindow.openColumnsGallery()}
|
||||||
@@ -220,16 +232,16 @@ function ManageButton() {
|
|||||||
|
|
||||||
const menuItems = await Promise.all([
|
const menuItems = await Promise.all([
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Open Columns Gallery",
|
text: "Open Launchpad",
|
||||||
action: () => LumeWindow.openColumnsGallery(),
|
action: () => LumeWindow.openColumnsGallery(),
|
||||||
}),
|
}),
|
||||||
PredefinedMenuItem.new({ item: "Separator" }),
|
PredefinedMenuItem.new({ item: "Separator" }),
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Add local feeds",
|
text: "Open Newsfeed",
|
||||||
action: () => LumeWindow.openLocalFeeds(),
|
action: () => LumeWindow.openLocalFeeds(),
|
||||||
}),
|
}),
|
||||||
MenuItem.new({
|
MenuItem.new({
|
||||||
text: "Add notification",
|
text: "Open Notification",
|
||||||
action: () => LumeWindow.openNotification(),
|
action: () => LumeWindow.openNotification(),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,20 +1,3 @@
|
|||||||
import { commands } from "@/commands.gen";
|
|
||||||
import type { LumeColumn } from "@/types";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute("/$account/_app/home")({
|
export const Route = createFileRoute("/$account/_app/home")();
|
||||||
beforeLoad: async ({ context }) => {
|
|
||||||
const key = "lume_v4:columns";
|
|
||||||
const defaultColumns = context.systemColumns.filter((col) => col.default);
|
|
||||||
const query = await commands.getLumeStore(key);
|
|
||||||
|
|
||||||
let initialColumns: LumeColumn[] = defaultColumns;
|
|
||||||
|
|
||||||
if (query.status === "ok") {
|
|
||||||
initialColumns = JSON.parse(query.data);
|
|
||||||
return { initialColumns };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { initialColumns };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function Screen() {
|
|||||||
const updateSettings = () => {
|
const updateSettings = () => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const newSettings = JSON.stringify(appSettings.state);
|
const newSettings = JSON.stringify(appSettings.state);
|
||||||
const res = await commands.setSettings(newSettings);
|
const res = await commands.setUserSettings(newSettings);
|
||||||
|
|
||||||
if (res.status === "error") {
|
if (res.status === "error") {
|
||||||
await message(res.error, { kind: "error" });
|
await message(res.error, { kind: "error" });
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
|||||||
|
|
||||||
export const Route = createFileRoute("/$account/_settings/general")({
|
export const Route = createFileRoute("/$account/_settings/general")({
|
||||||
beforeLoad: async () => {
|
beforeLoad: async () => {
|
||||||
const res = await commands.getSettings();
|
const res = await commands.getUserSettings();
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
appSettings.setState((state) => {
|
appSettings.setState((state) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { appSettings } from "@/commons";
|
|||||||
import { Spinner } from "@/components";
|
import { Spinner } from "@/components";
|
||||||
import type { QueryClient } from "@tanstack/react-query";
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import type { OsType } from "@tauri-apps/plugin-os";
|
import type { OsType } from "@tauri-apps/plugin-os";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
@@ -17,6 +18,8 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
|
const { queryClient } = Route.useRouteContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = events.newSettings.listen((data) => {
|
const unlisten = events.newSettings.listen((data) => {
|
||||||
appSettings.setState((state) => {
|
appSettings.setState((state) => {
|
||||||
@@ -29,6 +32,16 @@ function Screen() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlisten = listen("synchronized", async () => {
|
||||||
|
await queryClient.invalidateQueries();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((f) => f());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const Route = createFileRoute("/columns/_layout")({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeLoad: async () => {
|
beforeLoad: async () => {
|
||||||
const res = await commands.getSettings();
|
const res = await commands.getUserSettings();
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
appSettings.setState((state) => {
|
appSettings.setState((state) => {
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
import { commands } from "@/commands.gen";
|
|
||||||
import { Spinner } from "@/components";
|
|
||||||
import { User } from "@/components/user";
|
|
||||||
import { Plus, X } from "@phosphor-icons/react";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { message } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { useState, useTransition } from "react";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/columns/_layout/create-group")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
const REYA_NPUB =
|
|
||||||
"npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445";
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const contacts = Route.useLoaderData();
|
|
||||||
const search = Route.useSearch();
|
|
||||||
const navigate = Route.useNavigate();
|
|
||||||
const { queryClient } = Route.useRouteContext();
|
|
||||||
|
|
||||||
const [title, setTitle] = useState("");
|
|
||||||
const [npub, setNpub] = useState("");
|
|
||||||
const [users, setUsers] = useState<string[]>([REYA_NPUB]);
|
|
||||||
const [isPending, startTransition] = useTransition();
|
|
||||||
|
|
||||||
const toggleUser = (pubkey: string) => {
|
|
||||||
setUsers((prev) =>
|
|
||||||
prev.includes(pubkey)
|
|
||||||
? prev.filter((i) => i !== pubkey)
|
|
||||||
: [...prev, pubkey],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addUser = () => {
|
|
||||||
if (!npub.startsWith("npub1")) return;
|
|
||||||
if (users.includes(npub)) return;
|
|
||||||
|
|
||||||
setUsers((prev) => [...prev, npub]);
|
|
||||||
setNpub("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
startTransition(async () => {
|
|
||||||
const key = `lume_v4:group:${search.label}`;
|
|
||||||
const res = await commands.setLumeStore(key, JSON.stringify(users));
|
|
||||||
|
|
||||||
if (res.status === "ok") {
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [search.label, search.account],
|
|
||||||
});
|
|
||||||
navigate({ to: search.redirect, search: { ...search, name: title } });
|
|
||||||
} else {
|
|
||||||
await message(res.error, {
|
|
||||||
title: "Create Group",
|
|
||||||
kind: "error",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full gap-4">
|
|
||||||
<div className="flex flex-col items-center justify-center text-center">
|
|
||||||
<h1 className="font-serif text-2xl font-medium">Create a group</h1>
|
|
||||||
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
|
|
||||||
For the people that you want to keep up.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-4/5 max-w-full gap-3">
|
|
||||||
<div className="flex items-center w-full rounded-lg h-9 shrink-0 bg-black/5 dark:bg-white/5">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="w-16 text-sm font-semibold text-center border-r border-black/10 dark:border-white/10 shrink-0"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="name"
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
|
||||||
placeholder="Enter a name for this group"
|
|
||||||
className="h-full px-3 text-sm bg-transparent border-none placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center w-full gap-3">
|
|
||||||
<div className="overflow-y-auto scrollbar-none p-2 w-full h-[450px] flex flex-col gap-3 bg-black/5 dark:bg-white/5 rounded-xl">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
name="npub"
|
|
||||||
value={npub}
|
|
||||||
onChange={(e) => setNpub(e.target.value)}
|
|
||||||
placeholder="npub1..."
|
|
||||||
className="w-full px-3 text-sm border-none rounded-lg h-9 bg-black/10 dark:bg-white/10 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => addUser()}
|
|
||||||
className="inline-flex items-center justify-center text-white rounded-lg size-9 bg-black/20 dark:bg-white/20 shrink-0 hover:bg-blue-500"
|
|
||||||
>
|
|
||||||
<Plus className="size-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<span className="text-sm font-semibold">Added</span>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{users.length ? (
|
|
||||||
users.map((item: string) => (
|
|
||||||
<button
|
|
||||||
key={item}
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggleUser(item)}
|
|
||||||
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={item}>
|
|
||||||
<User.Root className="flex items-center gap-2.5">
|
|
||||||
<User.Avatar className="rounded-full size-8" />
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<User.Name className="text-sm font-medium" />
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
<X className="size-4" />
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center text-sm rounded-lg bg-black/5 dark:bg-white/5 h-14">
|
|
||||||
Empty.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<span className="text-sm font-semibold">Contacts</span>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{contacts.length ? (
|
|
||||||
contacts.map((item: string) => (
|
|
||||||
<button
|
|
||||||
key={item}
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggleUser(item)}
|
|
||||||
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
|
|
||||||
>
|
|
||||||
<User.Provider pubkey={item}>
|
|
||||||
<User.Root className="flex items-center gap-2.5">
|
|
||||||
<User.Avatar className="rounded-full size-8" />
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<User.Name className="text-sm font-medium" />
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center text-sm rounded-lg bg-black/5 dark:bg-white/5 h-14">
|
|
||||||
<p>
|
|
||||||
Find more user at{" "}
|
|
||||||
<a
|
|
||||||
href="https://www.nostr.directory/"
|
|
||||||
target="_blank"
|
|
||||||
className="text-blue-600 after:content-['_↗']"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Nostr Directory
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => submit()}
|
|
||||||
disabled={isPending || users.length < 1}
|
|
||||||
className="inline-flex items-center justify-center text-sm font-medium text-white bg-blue-500 rounded-full w-36 h-9 hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isPending ? <Spinner /> : "Confirm"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -29,9 +29,12 @@ function Screen() {
|
|||||||
<ScrollArea.Root
|
<ScrollArea.Root
|
||||||
type={"scroll"}
|
type={"scroll"}
|
||||||
scrollHideDelay={300}
|
scrollHideDelay={300}
|
||||||
className="overflow-hidden size-full flex-1"
|
className="overflow-hidden size-full px-3"
|
||||||
>
|
>
|
||||||
<ScrollArea.Viewport ref={ref} className="h-full pt-1 px-3 pb-3">
|
<ScrollArea.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
>
|
||||||
<Virtualizer scrollRef={ref}>
|
<Virtualizer scrollRef={ref}>
|
||||||
<RootEvent />
|
<RootEvent />
|
||||||
<ReplyList />
|
<ReplyList />
|
||||||
@@ -54,37 +57,33 @@ function RootEvent() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white flex items-center justify-center h-32 dark:bg-black/10 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<Spinner />
|
||||||
<Spinner />
|
Loading...
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white flex items-center justify-center h-32 dark:bg-black/10 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5">
|
<div className="flex items-center gap-2 text-sm text-red-500">
|
||||||
<div className="flex items-center gap-2 text-sm text-red-500">
|
{error.message}
|
||||||
{error.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
<Note.Root className="bg-white dark:bg-white/10 rounded-xl shadow-primary dark:shadow-none">
|
<Note.Root className="border-b-[.5px] border-neutral-300 dark:border-neutral-700">
|
||||||
<div className="flex items-center justify-between px-3 h-14">
|
<div className="flex items-center justify-between px-3 h-14">
|
||||||
<Note.User />
|
<Note.User />
|
||||||
<Note.Menu />
|
<Note.Menu />
|
||||||
</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>
|
||||||
@@ -217,7 +216,7 @@ function ReplyList() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="px-3">
|
||||||
<div className="flex items-center text-sm font-semibold h-14 text-neutral-600 dark:text-white/30">
|
<div className="flex items-center text-sm font-semibold h-14 text-neutral-600 dark:text-white/30">
|
||||||
All replies
|
All replies
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import type { LumeColumn } from "@/types";
|
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
||||||
import Avatar from "boring-avatars";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/columns/_layout/gallery")({
|
|
||||||
component: Screen,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Screen() {
|
|
||||||
const { columns } = Route.useRouteContext();
|
|
||||||
|
|
||||||
const install = async (column: LumeColumn) => {
|
|
||||||
const mainWindow = getCurrentWindow();
|
|
||||||
await mainWindow.emit("columns", { type: "add", column });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea.Root
|
|
||||||
type={"scroll"}
|
|
||||||
scrollHideDelay={300}
|
|
||||||
className="overflow-hidden size-full"
|
|
||||||
>
|
|
||||||
<ScrollArea.Viewport className="relative h-full px-3">
|
|
||||||
{columns.map((column) => (
|
|
||||||
<div
|
|
||||||
key={column.label}
|
|
||||||
className="mb-3 group flex px-2 items-center justify-between h-16 rounded-xl bg-white dark:bg-white/20 shadow-sm shadow-neutral-500/10"
|
|
||||||
>
|
|
||||||
<div className="inline-flex items-center gap-2">
|
|
||||||
<div className="size-11 bg-neutral-200 rounded-lg overflow-hidden">
|
|
||||||
<Avatar
|
|
||||||
name={column.name}
|
|
||||||
size={44}
|
|
||||||
square={true}
|
|
||||||
variant="pixel"
|
|
||||||
colors={[
|
|
||||||
"#84cc16",
|
|
||||||
"#22c55e",
|
|
||||||
"#0ea5e9",
|
|
||||||
"#3b82f6",
|
|
||||||
"#6366f1",
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">
|
|
||||||
<div className="mb-px leading-tight font-semibold">
|
|
||||||
{column.name}
|
|
||||||
</div>
|
|
||||||
<div className="leading-tight text-neutral-500 dark:text-neutral-400">
|
|
||||||
{column.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => install(column)}
|
|
||||||
className="text-xs uppercase font-semibold w-16 h-7 hidden group-hover:inline-flex items-center justify-center rounded-full bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-black/10"
|
|
||||||
>
|
|
||||||
Open
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</ScrollArea.Viewport>
|
|
||||||
<ScrollArea.Scrollbar
|
|
||||||
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
|
||||||
orientation="vertical"
|
|
||||||
>
|
|
||||||
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
|
||||||
</ScrollArea.Scrollbar>
|
|
||||||
<ScrollArea.Corner className="bg-transparent" />
|
|
||||||
</ScrollArea.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { LumeColumn } from "@/types";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
|
||||||
import { resolveResource } from "@tauri-apps/api/path";
|
|
||||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/columns/_layout/gallery")({
|
|
||||||
beforeLoad: async () => {
|
|
||||||
const systemPath = "resources/columns.json";
|
|
||||||
const resourcePath = await resolveResource(systemPath);
|
|
||||||
const resourceFile = await readTextFile(resourcePath);
|
|
||||||
|
|
||||||
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
|
|
||||||
const columns = systemColumns.filter((col) => !col.default);
|
|
||||||
|
|
||||||
return {
|
|
||||||
columns,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { toLumeEvents } from "@/commons";
|
import { toLumeEvents } from "@/commons";
|
||||||
import { Quote, RepostNote, Spinner, TextNote } from "@/components";
|
import { RepostNote, Spinner, TextNote } from "@/components";
|
||||||
import type { LumeEvent } from "@/system";
|
import type { LumeEvent } from "@/system";
|
||||||
import { Kind } from "@/types";
|
import { Kind } from "@/types";
|
||||||
import { ArrowCircleRight } from "@phosphor-icons/react";
|
import { ArrowDown } from "@phosphor-icons/react";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
@@ -48,14 +48,21 @@ export function Screen() {
|
|||||||
if (!event) return;
|
if (!event) return;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
return <RepostNote key={event.id} event={event} className="mb-3" />;
|
return (
|
||||||
default: {
|
<RepostNote
|
||||||
if (event.isQuote) {
|
key={event.id}
|
||||||
return <Quote key={event.id} event={event} className="mb-3" />;
|
event={event}
|
||||||
} else {
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
return <TextNote key={event.id} event={event} className="mb-3" />;
|
/>
|
||||||
}
|
);
|
||||||
}
|
default:
|
||||||
|
return (
|
||||||
|
<TextNote
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[data],
|
[data],
|
||||||
@@ -65,9 +72,12 @@ export function Screen() {
|
|||||||
<ScrollArea.Root
|
<ScrollArea.Root
|
||||||
type={"scroll"}
|
type={"scroll"}
|
||||||
scrollHideDelay={300}
|
scrollHideDelay={300}
|
||||||
className="overflow-hidden size-full"
|
className="overflow-hidden size-full px-3"
|
||||||
>
|
>
|
||||||
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
|
<ScrollArea.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
>
|
||||||
<Virtualizer scrollRef={ref}>
|
<Virtualizer scrollRef={ref}>
|
||||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||||
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
||||||
@@ -79,11 +89,11 @@ export function Screen() {
|
|||||||
) : null}
|
) : null}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center w-full h-16 gap-2">
|
<div className="flex items-center justify-center w-full h-16 gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-4" />
|
||||||
<span className="text-sm font-medium">Loading...</span>
|
<span className="text-sm font-medium">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
) : !data.length ? (
|
) : !data.length ? (
|
||||||
<div className="mb-3 flex items-center justify-center h-20 text-sm rounded-xl bg-black/5 dark:bg-white/5">
|
<div className="mb-3 flex items-center justify-center h-20 text-sm">
|
||||||
🎉 Yo. You're catching up on all latest notes.
|
🎉 Yo. You're catching up on all latest notes.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -95,13 +105,13 @@ export function Screen() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => fetchNextPage()}
|
onClick={() => fetchNextPage()}
|
||||||
disabled={isFetchingNextPage || isLoading}
|
disabled={isFetchingNextPage || isLoading}
|
||||||
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
|
className="inline-flex items-center justify-center w-full gap-2 px-3 text-sm font-medium text-blue-500 h-11 focus:outline-none"
|
||||||
>
|
>
|
||||||
{isFetchingNextPage ? (
|
{isFetchingNextPage ? (
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-4" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ArrowCircleRight className="size-5" />
|
<ArrowDown className="size-4" />
|
||||||
Load more
|
Load more
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { commands } from "@/commands.gen";
|
|
||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/columns/_layout/group")({
|
|
||||||
beforeLoad: async ({ search }) => {
|
|
||||||
const key = `lume_v4:group:${search.label}`;
|
|
||||||
const res = await commands.getLumeStore(key);
|
|
||||||
|
|
||||||
if (res.status === "ok") {
|
|
||||||
const groups: string[] = JSON.parse(res.data);
|
|
||||||
|
|
||||||
if (groups.length) {
|
|
||||||
return { groups };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw redirect({
|
|
||||||
to: "/columns/create-group",
|
|
||||||
search: {
|
|
||||||
...search,
|
|
||||||
redirect: "/columns/group",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+34
-22
@@ -1,22 +1,23 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { toLumeEvents } from "@/commons";
|
import { toLumeEvents } from "@/commons";
|
||||||
import { Quote, RepostNote, Spinner, TextNote } from "@/components";
|
import { RepostNote, Spinner, TextNote } from "@/components";
|
||||||
import type { LumeEvent } from "@/system";
|
import type { LumeEvent } from "@/system";
|
||||||
import { Kind } from "@/types";
|
import { Kind } from "@/types";
|
||||||
import { ArrowCircleRight } from "@phosphor-icons/react";
|
import { ArrowDown } from "@phosphor-icons/react";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { Virtualizer } from "virtua";
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/columns/_layout/group")({
|
export const Route = createLazyFileRoute("/columns/_layout/groups/$id")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
const { label, account } = Route.useSearch();
|
const group = Route.useLoaderData();
|
||||||
const { groups } = Route.useRouteContext();
|
const params = Route.useParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -25,11 +26,11 @@ export function Screen() {
|
|||||||
hasNextPage,
|
hasNextPage,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
} = useInfiniteQuery({
|
} = useInfiniteQuery({
|
||||||
queryKey: [label, account],
|
queryKey: ["groups", params.id],
|
||||||
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.getGroupEvents(groups, until);
|
const res = await commands.getAllEventsByAuthors(group, until);
|
||||||
|
|
||||||
if (res.status === "error") {
|
if (res.status === "error") {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
@@ -39,6 +40,7 @@ export function Screen() {
|
|||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
getNextPageParam: (lastPage) => lastPage?.at(-1)?.created_at - 1,
|
||||||
select: (data) => data?.pages.flat(),
|
select: (data) => data?.pages.flat(),
|
||||||
|
enabled: group?.length > 0,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,14 +51,21 @@ export function Screen() {
|
|||||||
if (!event) return;
|
if (!event) return;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
return <RepostNote key={event.id} event={event} className="mb-3" />;
|
return (
|
||||||
default: {
|
<RepostNote
|
||||||
if (event.isQuote) {
|
key={event.id}
|
||||||
return <Quote key={event.id} event={event} className="mb-3" />;
|
event={event}
|
||||||
} else {
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
return <TextNote key={event.id} event={event} className="mb-3" />;
|
/>
|
||||||
}
|
);
|
||||||
}
|
default:
|
||||||
|
return (
|
||||||
|
<TextNote
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[data],
|
[data],
|
||||||
@@ -66,9 +75,12 @@ export function Screen() {
|
|||||||
<ScrollArea.Root
|
<ScrollArea.Root
|
||||||
type={"scroll"}
|
type={"scroll"}
|
||||||
scrollHideDelay={300}
|
scrollHideDelay={300}
|
||||||
className="overflow-hidden size-full"
|
className="overflow-hidden size-full px-3"
|
||||||
>
|
>
|
||||||
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
|
<ScrollArea.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
>
|
||||||
<Virtualizer scrollRef={ref}>
|
<Virtualizer scrollRef={ref}>
|
||||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||||
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
||||||
@@ -80,11 +92,11 @@ export function Screen() {
|
|||||||
) : null}
|
) : null}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center w-full h-16 gap-2">
|
<div className="flex items-center justify-center w-full h-16 gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-4" />
|
||||||
<span className="text-sm font-medium">Loading...</span>
|
<span className="text-sm font-medium">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
) : !data.length ? (
|
) : !data.length ? (
|
||||||
<div className="mb-3 flex items-center justify-center h-20 text-sm rounded-xl bg-black/5 dark:bg-white/5">
|
<div className="mb-3 flex items-center justify-center h-20 text-sm">
|
||||||
🎉 Yo. You're catching up on all latest notes.
|
🎉 Yo. You're catching up on all latest notes.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -96,13 +108,13 @@ export function Screen() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => fetchNextPage()}
|
onClick={() => fetchNextPage()}
|
||||||
disabled={isFetchingNextPage || isLoading}
|
disabled={isFetchingNextPage || isLoading}
|
||||||
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
|
className="inline-flex items-center justify-center w-full gap-2 px-3 text-sm font-medium text-blue-500 h-11 focus:outline-none"
|
||||||
>
|
>
|
||||||
{isFetchingNextPage ? (
|
{isFetchingNextPage ? (
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-4" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ArrowCircleRight className="size-5" />
|
<ArrowDown className="size-4" />
|
||||||
Load more
|
Load more
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
|
import type { NostrEvent } from "@/types";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/columns/_layout/groups/$id")({
|
||||||
|
loader: async ({ params }) => {
|
||||||
|
const res = await commands.getGroup(params.id);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
const event: NostrEvent = JSON.parse(res.data);
|
||||||
|
const tag = event.tags
|
||||||
|
.filter((tag) => tag[0] === "p")
|
||||||
|
.map((tag) => tag[1]);
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
+34
-23
@@ -1,22 +1,23 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { toLumeEvents } from "@/commons";
|
import { toLumeEvents } from "@/commons";
|
||||||
import { Quote, RepostNote, Spinner, TextNote } from "@/components";
|
import { RepostNote, Spinner, TextNote } from "@/components";
|
||||||
import type { LumeEvent } from "@/system";
|
import type { LumeEvent } from "@/system";
|
||||||
import { Kind } from "@/types";
|
import { Kind } from "@/types";
|
||||||
import { ArrowCircleRight } from "@phosphor-icons/react";
|
import { ArrowDown } from "@phosphor-icons/react";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { Virtualizer } from "virtua";
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/columns/_layout/hashtags/$content")({
|
export const Route = createLazyFileRoute("/columns/_layout/interests/$id")({
|
||||||
component: Screen,
|
component: Screen,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
const { label, account } = Route.useSearch();
|
const hashtags = Route.useLoaderData();
|
||||||
const { content } = Route.useParams();
|
const params = Route.useParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -25,12 +26,12 @@ export function Screen() {
|
|||||||
hasNextPage,
|
hasNextPage,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
} = useInfiniteQuery({
|
} = useInfiniteQuery({
|
||||||
queryKey: [label, account],
|
queryKey: ["hashtags", params.id],
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
const hashtags = content.split("_");
|
const tags = hashtags.map((tag) => tag.toLowerCase().replace("#", ""));
|
||||||
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
const until = pageParam > 0 ? pageParam.toString() : undefined;
|
||||||
const res = await commands.getHashtagEvents(hashtags, until);
|
const res = await commands.getAllEventsByHashtags(tags, until);
|
||||||
|
|
||||||
if (res.status === "error") {
|
if (res.status === "error") {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
@@ -50,14 +51,21 @@ export function Screen() {
|
|||||||
if (!event) return;
|
if (!event) return;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
return <RepostNote key={event.id} event={event} className="mb-3" />;
|
return (
|
||||||
default: {
|
<RepostNote
|
||||||
if (event.isQuote) {
|
key={event.id}
|
||||||
return <Quote key={event.id} event={event} className="mb-3" />;
|
event={event}
|
||||||
} else {
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
return <TextNote key={event.id} event={event} className="mb-3" />;
|
/>
|
||||||
}
|
);
|
||||||
}
|
default:
|
||||||
|
return (
|
||||||
|
<TextNote
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[data],
|
[data],
|
||||||
@@ -67,9 +75,12 @@ export function Screen() {
|
|||||||
<ScrollArea.Root
|
<ScrollArea.Root
|
||||||
type={"scroll"}
|
type={"scroll"}
|
||||||
scrollHideDelay={300}
|
scrollHideDelay={300}
|
||||||
className="overflow-hidden size-full"
|
className="overflow-hidden size-full px-3"
|
||||||
>
|
>
|
||||||
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
|
<ScrollArea.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
>
|
||||||
<Virtualizer scrollRef={ref}>
|
<Virtualizer scrollRef={ref}>
|
||||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||||
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
||||||
@@ -81,11 +92,11 @@ export function Screen() {
|
|||||||
) : null}
|
) : null}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center w-full h-16 gap-2">
|
<div className="flex items-center justify-center w-full h-16 gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-4" />
|
||||||
<span className="text-sm font-medium">Loading...</span>
|
<span className="text-sm font-medium">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
) : !data.length ? (
|
) : !data.length ? (
|
||||||
<div className="mb-3 flex items-center justify-center h-20 text-sm rounded-xl bg-black/5 dark:bg-white/5">
|
<div className="mb-3 flex items-center justify-center h-20 text-sm">
|
||||||
🎉 Yo. You're catching up on all latest notes.
|
🎉 Yo. You're catching up on all latest notes.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -97,13 +108,13 @@ export function Screen() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => fetchNextPage()}
|
onClick={() => fetchNextPage()}
|
||||||
disabled={isFetchingNextPage || isLoading}
|
disabled={isFetchingNextPage || isLoading}
|
||||||
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
|
className="inline-flex items-center justify-center w-full gap-2 px-3 text-sm font-medium text-blue-500 h-11 focus:outline-none"
|
||||||
>
|
>
|
||||||
{isFetchingNextPage ? (
|
{isFetchingNextPage ? (
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-4" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ArrowCircleRight className="size-5" />
|
<ArrowDown className="size-4" />
|
||||||
Load more
|
Load more
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
|
import type { NostrEvent } from "@/types";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/columns/_layout/interests/$id")({
|
||||||
|
loader: async ({ params }) => {
|
||||||
|
const res = await commands.getInterest(params.id);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
const event: NostrEvent = JSON.parse(res.data);
|
||||||
|
const tag = event.tags
|
||||||
|
.filter((tag) => tag[0] === "t")
|
||||||
|
.map((tag) => tag[1]);
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
|
import { cn, toLumeEvents } from "@/commons";
|
||||||
|
import { Spinner, User } from "@/components";
|
||||||
|
import { LumeWindow } from "@/system";
|
||||||
|
import type { LumeColumn, NostrEvent } from "@/types";
|
||||||
|
import { ArrowClockwise, Plus } from "@phosphor-icons/react";
|
||||||
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { resolveResource } from "@tauri-apps/api/path";
|
||||||
|
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/columns/_layout/launchpad")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
return (
|
||||||
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="overflow-hidden size-full"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport className="relative h-full px-3 pb-3">
|
||||||
|
<Groups />
|
||||||
|
<Interests />
|
||||||
|
<Core />
|
||||||
|
</ScrollArea.Viewport>
|
||||||
|
<ScrollArea.Scrollbar
|
||||||
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||||
|
</ScrollArea.Scrollbar>
|
||||||
|
<ScrollArea.Corner className="bg-transparent" />
|
||||||
|
</ScrollArea.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Core() {
|
||||||
|
const { isLoading, data } = useQuery({
|
||||||
|
queryKey: ["core"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const systemPath = "resources/columns.json";
|
||||||
|
const resourcePath = await resolveResource(systemPath);
|
||||||
|
const resourceFile = await readTextFile(resourcePath);
|
||||||
|
|
||||||
|
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
|
||||||
|
const columns = systemColumns.filter((col) => !col.default);
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex items-center justify-between px-2">
|
||||||
|
<h3 className="font-semibold">Core</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="inline-flex items-center gap-1.5">
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data.map((column) => (
|
||||||
|
<div
|
||||||
|
key={column.label}
|
||||||
|
className="group flex px-4 items-center justify-between h-16 rounded-xl bg-white dark:bg-black border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="mb-px leading-tight font-semibold">
|
||||||
|
{column.name}
|
||||||
|
</div>
|
||||||
|
<div className="leading-tight text-neutral-500 dark:text-neutral-400">
|
||||||
|
{column.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => LumeWindow.openColumn(column)}
|
||||||
|
className="text-xs uppercase font-semibold w-16 h-7 hidden group-hover:inline-flex items-center justify-center rounded-full bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-black/10"
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Groups() {
|
||||||
|
const { account } = Route.useSearch();
|
||||||
|
const { isLoading, data, refetch, isRefetching } = useQuery({
|
||||||
|
queryKey: ["groups", account],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await commands.getAllGroups();
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
const data = toLumeEvents(res.data);
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select: (data) =>
|
||||||
|
data.filter(
|
||||||
|
(item) => item.tags.filter((tag) => tag[0] === "p")?.length > 0,
|
||||||
|
),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = useCallback(
|
||||||
|
(item: NostrEvent) => {
|
||||||
|
const name =
|
||||||
|
item.tags.find((tag) => tag[0] === "title")?.[1] || "Unnamed";
|
||||||
|
const label = item.tags.find((tag) => tag[0] === "d")?.[1] || nanoid();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="group flex flex-col rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50 border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
>
|
||||||
|
<div className="p-3 h-16 flex flex-wrap items-center justify-center gap-2 overflow-y-auto">
|
||||||
|
{item.tags
|
||||||
|
.filter((tag) => tag[0] === "p")
|
||||||
|
.map((tag) => (
|
||||||
|
<div key={tag[1]}>
|
||||||
|
<User.Provider pubkey={tag[1]}>
|
||||||
|
<User.Root>
|
||||||
|
<User.Avatar className="size-8 rounded-full" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="p-3 flex items-center justify-between">
|
||||||
|
<div className="text-sm font-medium">{name}</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
LumeWindow.openColumn({
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
url: `/columns/groups/${item.id}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-blue-600 hover:bg-blue-500 text-white"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-12 flex flex-col gap-3">
|
||||||
|
<div className="flex items-center justify-between px-2">
|
||||||
|
<h3 className="font-semibold">Groups</h3>
|
||||||
|
<div className="inline-flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => refetch()}
|
||||||
|
className={cn(
|
||||||
|
"size-7 inline-flex items-center justify-center rounded-full",
|
||||||
|
isRefetching ? "animate-spin" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ArrowClockwise className="size-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
LumeWindow.openPopup("New group", `/set-group?account=${account}`)
|
||||||
|
}
|
||||||
|
className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
|
||||||
|
>
|
||||||
|
<Plus className="size-3" weight="bold" />
|
||||||
|
New
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="inline-flex items-center gap-1.5">
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : !data.length ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
|
||||||
|
<p className="text-center">You don't have any groups yet.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data.map((item) => renderItem(item))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Interests() {
|
||||||
|
const { account } = Route.useSearch();
|
||||||
|
const { isLoading, data, refetch, isRefetching } = useQuery({
|
||||||
|
queryKey: ["interests", account],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await commands.getAllInterests();
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
const data = toLumeEvents(res.data);
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select: (data) =>
|
||||||
|
data.filter(
|
||||||
|
(item) => item.tags.filter((tag) => tag[0] === "t")?.length > 0,
|
||||||
|
),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = useCallback(
|
||||||
|
(item: NostrEvent) => {
|
||||||
|
const name =
|
||||||
|
item.tags.find((tag) => tag[0] === "title")?.[1] || "Unnamed";
|
||||||
|
const label =
|
||||||
|
item.tags.find((tag) => tag[0] === "label")?.[1] || nanoid();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="group flex flex-col rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50 border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
>
|
||||||
|
<div className="p-3 h-16 flex flex-wrap items-center justify-center gap-2 overflow-y-auto">
|
||||||
|
{item.tags
|
||||||
|
.filter((tag) => tag[0] === "t")
|
||||||
|
.map((tag) => (
|
||||||
|
<div key={tag[1]} className="text-sm font-medium">
|
||||||
|
{tag[1]}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="p-3 flex items-center justify-between">
|
||||||
|
<div className="text-sm font-medium">{name}</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
LumeWindow.openColumn({
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
url: `/columns/interests/${item.id}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="h-6 w-16 inline-flex items-center justify-center gap-1 text-xs font-semibold rounded-full bg-blue-600 hover:bg-blue-500 text-white"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-12 flex flex-col gap-3">
|
||||||
|
<div className="flex items-center justify-between px-2">
|
||||||
|
<h3 className="font-semibold">Interests</h3>
|
||||||
|
<div className="inline-flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => refetch()}
|
||||||
|
className={cn(
|
||||||
|
"size-7 inline-flex items-center justify-center rounded-full",
|
||||||
|
isRefetching ? "animate-spin" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ArrowClockwise className="size-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
LumeWindow.openPopup(
|
||||||
|
"New interest",
|
||||||
|
`/set-interest?account=${account}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="h-7 w-max px-2 inline-flex items-center justify-center gap-1 text-sm font-medium rounded-full bg-neutral-300 dark:bg-neutral-700 hover:bg-blue-500 hover:text-white"
|
||||||
|
>
|
||||||
|
<Plus className="size-3" weight="bold" />
|
||||||
|
New
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="inline-flex items-center gap-1.5">
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : !data.length ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-16 w-full rounded-xl overflow-hidden bg-neutral-200/50 dark:bg-neutral-800/50">
|
||||||
|
<p className="text-center">You don't have any interests yet.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data.map((item) => renderItem(item))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,17 +1,12 @@
|
|||||||
import { events, commands } from "@/commands.gen";
|
import { events, commands } from "@/commands.gen";
|
||||||
import { toLumeEvents } from "@/commons";
|
import { toLumeEvents } from "@/commons";
|
||||||
import { Quote, RepostNote, Spinner, TextNote } from "@/components";
|
import { RepostNote, Spinner, TextNote } from "@/components";
|
||||||
import { LumeEvent } from "@/system";
|
import { LumeEvent } from "@/system";
|
||||||
import { Kind, type Meta } from "@/types";
|
import { Kind, type Meta } from "@/types";
|
||||||
import { ArrowCircleRight, ArrowUp } from "@phosphor-icons/react";
|
import { ArrowDown, ArrowUp } from "@phosphor-icons/react";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { type InfiniteData, useInfiniteQuery } from "@tanstack/react-query";
|
import { type InfiniteData, useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import {
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
Navigate,
|
|
||||||
createLazyFileRoute,
|
|
||||||
useLocation,
|
|
||||||
} from "@tanstack/react-router";
|
|
||||||
import { listen } from "@tauri-apps/api/event";
|
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
@@ -33,12 +28,11 @@ export const Route = createLazyFileRoute("/columns/_layout/newsfeed")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function Screen() {
|
export function Screen() {
|
||||||
const { queryClient } = Route.useRouteContext();
|
const contacts = Route.useLoaderData();
|
||||||
const { label, account } = Route.useSearch();
|
const { label, account } = Route.useSearch();
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
|
||||||
isFetching,
|
isFetching,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
@@ -48,7 +42,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.getLocalEvents(until);
|
const res = await commands.getAllEventsByAuthors(contacts, until);
|
||||||
|
|
||||||
if (res.status === "error") {
|
if (res.status === "error") {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
@@ -58,9 +52,9 @@ export function Screen() {
|
|||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => lastPage?.at?.(-1)?.created_at - 1,
|
getNextPageParam: (lastPage) => lastPage?.at?.(-1)?.created_at - 1,
|
||||||
select: (data) => data?.pages.flat(),
|
select: (data) => data?.pages.flat(),
|
||||||
|
enabled: contacts?.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const location = useLocation();
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
@@ -68,46 +62,37 @@ export function Screen() {
|
|||||||
if (!event) return;
|
if (!event) return;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
return <RepostNote key={event.id} event={event} className="mb-3" />;
|
return (
|
||||||
default: {
|
<RepostNote
|
||||||
if (event.isQuote) {
|
key={event.id}
|
||||||
return <Quote key={event.id} event={event} className="mb-3" />;
|
event={event}
|
||||||
} else {
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
return <TextNote key={event.id} event={event} className="mb-3" />;
|
/>
|
||||||
}
|
);
|
||||||
}
|
default:
|
||||||
|
return (
|
||||||
|
<TextNote
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[data],
|
[data],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unlisten = listen("synchronized", async () => {
|
|
||||||
await queryClient.invalidateQueries({ queryKey: [label, account] });
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unlisten.then((f) => f());
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
return (
|
|
||||||
<Navigate
|
|
||||||
to="/columns/create-newsfeed/users"
|
|
||||||
search={{ label, account, redirect: location.href }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea.Root
|
<ScrollArea.Root
|
||||||
type={"scroll"}
|
type={"scroll"}
|
||||||
scrollHideDelay={300}
|
scrollHideDelay={300}
|
||||||
className="overflow-hidden size-full"
|
className="overflow-hidden size-full px-3"
|
||||||
>
|
>
|
||||||
<ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3">
|
<ScrollArea.Viewport
|
||||||
<Listerner />
|
ref={ref}
|
||||||
|
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
>
|
||||||
|
<Listener />
|
||||||
<Virtualizer scrollRef={ref}>
|
<Virtualizer scrollRef={ref}>
|
||||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||||
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
<div className="z-50 fixed top-0 left-0 w-full h-14 flex items-center justify-center px-3">
|
||||||
@@ -119,11 +104,11 @@ export function Screen() {
|
|||||||
) : null}
|
) : null}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center w-full h-16 gap-2">
|
<div className="flex items-center justify-center w-full h-16 gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-4" />
|
||||||
<span className="text-sm font-medium">Loading...</span>
|
<span className="text-sm font-medium">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
) : !data.length ? (
|
) : !data.length ? (
|
||||||
<div className="mb-3 flex items-center justify-center h-20 text-sm rounded-xl bg-black/5 dark:bg-white/5">
|
<div className="mb-3 flex items-center justify-center h-20 text-sm">
|
||||||
🎉 Yo. You're catching up on all latest notes.
|
🎉 Yo. You're catching up on all latest notes.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -135,13 +120,13 @@ export function Screen() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => fetchNextPage()}
|
onClick={() => fetchNextPage()}
|
||||||
disabled={isFetchingNextPage || isLoading}
|
disabled={isFetchingNextPage || isLoading}
|
||||||
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
|
className="inline-flex items-center justify-center w-full gap-2 px-3 text-sm font-medium text-blue-500 h-11 focus:outline-none"
|
||||||
>
|
>
|
||||||
{isFetchingNextPage ? (
|
{isFetchingNextPage ? (
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-4" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ArrowCircleRight className="size-5" />
|
<ArrowDown className="size-4" />
|
||||||
Load more
|
Load more
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -161,7 +146,7 @@ export function Screen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Listerner = memo(function Listerner() {
|
const Listener = memo(function Listerner() {
|
||||||
const { queryClient } = Route.useRouteContext();
|
const { queryClient } = Route.useRouteContext();
|
||||||
const { label, account } = Route.useSearch();
|
const { label, account } = Route.useSearch();
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute("/columns/_layout/create-group")({
|
export const Route = createFileRoute("/columns/_layout/newsfeed")({
|
||||||
loader: async () => {
|
loader: async () => {
|
||||||
const res = await commands.getContactList();
|
const res = await commands.getContactList();
|
||||||
|
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
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, RepostIcon, Spinner, User } from "@/components";
|
||||||
import { LumeEvent, LumeWindow, 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 } from "@phosphor-icons/react";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import * as Tabs from "@radix-ui/react-tabs";
|
import * as Tabs from "@radix-ui/react-tabs";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { type ReactNode, useEffect, useRef } from "react";
|
import { type ReactNode, useEffect, useMemo, useRef } from "react";
|
||||||
import { Virtualizer } from "virtua";
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/columns/_layout/notification")({
|
export const Route = createLazyFileRoute("/columns/_layout/notification")({
|
||||||
@@ -99,110 +99,104 @@ function Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Root defaultValue="replies" className="flex flex-col h-full">
|
<div className="px-3 h-full overflow-y-auto">
|
||||||
<Tabs.List className="h-8 shrink-0 flex items-center">
|
<Tabs.Root
|
||||||
<Tabs.Trigger
|
defaultValue="replies"
|
||||||
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50"
|
className="flex flex-col h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
value="replies"
|
|
||||||
>
|
|
||||||
Replies
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger
|
|
||||||
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50"
|
|
||||||
value="reactions"
|
|
||||||
>
|
|
||||||
Reactions
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger
|
|
||||||
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50"
|
|
||||||
value="zaps"
|
|
||||||
>
|
|
||||||
Zaps
|
|
||||||
</Tabs.Trigger>
|
|
||||||
</Tabs.List>
|
|
||||||
<ScrollArea.Root
|
|
||||||
type={"scroll"}
|
|
||||||
scrollHideDelay={300}
|
|
||||||
className="min-h-0 flex-1 overflow-x-hidden"
|
|
||||||
>
|
>
|
||||||
<Tab value="replies">
|
<Tabs.List className="h-11 shrink-0 flex items-center">
|
||||||
{data.texts.map((event, index) => (
|
<Tabs.Trigger
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
className="flex-1 inline-flex h-11 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-neutral-100 dark:border-neutral-900 data-[state=active]:border-neutral-200 dark:data-[state=active]:border-neutral-800 data-[state=inactive]:opacity-50"
|
||||||
<TextNote key={event.id + index} event={event} />
|
value="replies"
|
||||||
))}
|
>
|
||||||
</Tab>
|
Replies
|
||||||
<Tab value="reactions">
|
</Tabs.Trigger>
|
||||||
{[...data.reactions.entries()].map(([root, events]) => (
|
<Tabs.Trigger
|
||||||
<div
|
className="flex-1 inline-flex h-11 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-neutral-100 dark:border-neutral-900 data-[state=active]:border-neutral-200 dark:data-[state=active]:border-neutral-800 data-[state=inactive]:opacity-50"
|
||||||
key={root}
|
value="reactions"
|
||||||
className="flex flex-col gap-1 p-3 mb-3 bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5"
|
>
|
||||||
>
|
Reactions
|
||||||
<div className="flex flex-col flex-1 min-w-0 gap-2">
|
</Tabs.Trigger>
|
||||||
<div className="flex items-center gap-2 pb-2 border-b border-black/5 dark:border-white/5">
|
<Tabs.Trigger
|
||||||
<RootNote id={root} />
|
className="flex-1 inline-flex h-11 items-center justify-center gap-2 px-3 text-sm font-medium border-b border-neutral-100 dark:border-neutral-900 data-[state=active]:border-neutral-200 dark:data-[state=active]:border-neutral-800 data-[state=inactive]:opacity-50"
|
||||||
</div>
|
value="zaps"
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
>
|
||||||
{events.map((event) => (
|
Zaps
|
||||||
<User.Provider key={event.id} pubkey={event.pubkey}>
|
</Tabs.Trigger>
|
||||||
<User.Root className="shrink-0 flex rounded-full h-7 bg-black/10 dark:bg-white/10 p-[2px]">
|
</Tabs.List>
|
||||||
<User.Avatar className="flex-1 rounded-full size-6" />
|
<ScrollArea.Root
|
||||||
<div className="inline-flex items-center justify-center flex-1 text-xs truncate rounded-full size-7">
|
type={"scroll"}
|
||||||
{event.kind === Kind.Reaction ? (
|
scrollHideDelay={300}
|
||||||
event.content === "+" ? (
|
className="min-h-0 flex-1 overflow-x-hidden"
|
||||||
"👍"
|
|
||||||
) : (
|
|
||||||
event.content
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Repeat className="text-teal-400 size-4 dark:text-teal-600" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Tab>
|
|
||||||
<Tab value="zaps">
|
|
||||||
{[...data.zaps.entries()].map(([root, events]) => (
|
|
||||||
<div
|
|
||||||
key={root}
|
|
||||||
className="flex flex-col gap-1 p-3 mb-3 bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col flex-1 min-w-0 gap-2">
|
|
||||||
<div className="flex items-center gap-2 pb-2 border-b border-black/5 dark:border-white/5">
|
|
||||||
<RootNote id={root} />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
|
||||||
{events.map((event) => (
|
|
||||||
<User.Provider
|
|
||||||
key={event.id}
|
|
||||||
pubkey={event.tags.find((tag) => tag[0] === "P")[1]}
|
|
||||||
>
|
|
||||||
<User.Root className="shrink-0 flex gap-1.5 rounded-full h-7 bg-black/10 dark:bg-white/10 p-[2px]">
|
|
||||||
<User.Avatar className="rounded-full size-6" />
|
|
||||||
<div className="flex-1 h-6 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-xs font-semibold truncate">
|
|
||||||
₿ {decodeZapInvoice(event.tags).bitcoinFormatted}
|
|
||||||
</div>
|
|
||||||
</User.Root>
|
|
||||||
</User.Provider>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Tab>
|
|
||||||
<ScrollArea.Scrollbar
|
|
||||||
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
|
||||||
orientation="vertical"
|
|
||||||
>
|
>
|
||||||
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
<Tab value="replies">
|
||||||
</ScrollArea.Scrollbar>
|
{data.texts.map((event, index) => (
|
||||||
<ScrollArea.Corner className="bg-transparent" />
|
<TextNote key={event.id + index} event={event} />
|
||||||
</ScrollArea.Root>
|
))}
|
||||||
</Tabs.Root>
|
</Tab>
|
||||||
|
<Tab value="reactions">
|
||||||
|
{[...data.reactions.entries()].map(([root, events]) => (
|
||||||
|
<div
|
||||||
|
key={root}
|
||||||
|
className="p-3 w-full border-b-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col flex-1 min-w-0 gap-2">
|
||||||
|
<div className="flex items-center gap-2 pb-2">
|
||||||
|
<RootNote id={root} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
{events.map((event) => (
|
||||||
|
<User.Provider key={event.id} pubkey={event.pubkey}>
|
||||||
|
<User.Root className="shrink-0 flex rounded-full h-7 bg-neutral-100 dark:bg-neutral-900 p-[2px]">
|
||||||
|
<User.Avatar className="flex-1 rounded-full size-6" />
|
||||||
|
<div className="inline-flex items-center justify-center flex-1 text-xs truncate rounded-full size-6">
|
||||||
|
{event.kind === Kind.Reaction ? (
|
||||||
|
event.content === "+" ? (
|
||||||
|
"👍"
|
||||||
|
) : (
|
||||||
|
event.content
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<RepostIcon className="text-teal-400 size-4 dark:text-teal-600" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Tab>
|
||||||
|
<Tab value="zaps">
|
||||||
|
{[...data.zaps.entries()].map(([root, events]) => (
|
||||||
|
<div
|
||||||
|
key={root}
|
||||||
|
className="p-3 w-full border-b-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col flex-1 min-w-0 gap-2">
|
||||||
|
<div className="flex items-center gap-2 pb-2">
|
||||||
|
<RootNote id={root} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
{events.map((event) => (
|
||||||
|
<ZapReceipt key={event.id} event={event} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Tab>
|
||||||
|
<ScrollArea.Scrollbar
|
||||||
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||||
|
</ScrollArea.Scrollbar>
|
||||||
|
<ScrollArea.Corner className="bg-transparent" />
|
||||||
|
</ScrollArea.Root>
|
||||||
|
</Tabs.Root>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +205,7 @@ function Tab({ value, children }: { value: string; children: ReactNode[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.Content value={value} className="size-full">
|
<Tabs.Content value={value} className="size-full">
|
||||||
<ScrollArea.Viewport ref={ref} className="h-full p-3">
|
<ScrollArea.Viewport ref={ref} className="h-full">
|
||||||
<Virtualizer scrollRef={ref}>{children}</Virtualizer>
|
<Virtualizer scrollRef={ref}>{children}</Virtualizer>
|
||||||
</ScrollArea.Viewport>
|
</ScrollArea.Viewport>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
@@ -267,10 +261,10 @@ function TextNote({ event }: { event: LumeEvent }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => LumeWindow.openEvent(event)}
|
onClick={() => LumeWindow.openEvent(event)}
|
||||||
className="w-full rounded-xl hover:ring-1 ring-blue-500 mb-3"
|
className="p-3 w-full border-b-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
|
||||||
>
|
>
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
<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">
|
<Note.Root className="flex flex-col">
|
||||||
<User.Provider pubkey={event.pubkey}>
|
<User.Provider pubkey={event.pubkey}>
|
||||||
<User.Root className="inline-flex items-center gap-2">
|
<User.Root className="inline-flex items-center gap-2">
|
||||||
<User.Avatar className="rounded-full size-9" />
|
<User.Avatar className="rounded-full size-9" />
|
||||||
@@ -307,3 +301,36 @@ function TextNote({ event }: { event: LumeEvent }) {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ZapReceipt({ event }: { event: LumeEvent }) {
|
||||||
|
const amount = useMemo(
|
||||||
|
() => decodeZapInvoice(event.tags).bitcoinFormatted ?? "0",
|
||||||
|
[event.id],
|
||||||
|
);
|
||||||
|
const sender = useMemo(
|
||||||
|
() => event.tags.find((tag) => tag[0] === "P")?.[1],
|
||||||
|
[event.id],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sender) {
|
||||||
|
return (
|
||||||
|
<div className="shrink-0 flex gap-1.5 rounded-full h-7 bg-black/10 dark:bg-white/10 p-[2px]">
|
||||||
|
<div className="rounded-full size-6 bg-blue-500" />
|
||||||
|
<div className="flex-1 h-6 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-xs font-semibold truncate">
|
||||||
|
₿ {amount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<User.Provider pubkey={sender}>
|
||||||
|
<User.Root className="shrink-0 flex gap-1.5 rounded-full h-7 bg-black/10 dark:bg-white/10 p-[2px]">
|
||||||
|
<User.Avatar className="rounded-full size-6" />
|
||||||
|
<div className="flex-1 h-6 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-xs font-semibold truncate">
|
||||||
|
₿ {amount}
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/columns/_layout/onboarding")({
|
export const Route = createLazyFileRoute("/columns/_layout/onboarding")({
|
||||||
@@ -6,107 +7,97 @@ export const Route = createLazyFileRoute("/columns/_layout/onboarding")({
|
|||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col py-6 gap-6 overflow-y-auto scrollbar-none">
|
<ScrollArea.Root
|
||||||
<div className="text-center flex flex-col items-center justify-center">
|
type={"scroll"}
|
||||||
<h1 className="text-2xl font-serif font-medium">Welcome to Lume</h1>
|
scrollHideDelay={300}
|
||||||
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
|
className="overflow-hidden size-full"
|
||||||
Here are a few suggestions to help you get started.
|
>
|
||||||
</p>
|
<ScrollArea.Viewport className="relative h-full px-3 pb-3">
|
||||||
</div>
|
<div className="my-10 text-center flex flex-col items-center justify-center">
|
||||||
<div className="px-3 flex flex-col gap-3">
|
<h1 className="text-2xl font-serif font-medium">Welcome to Lume</h1>
|
||||||
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
|
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
|
||||||
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
|
Here are a few suggestions to help you get started.
|
||||||
01.
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="relative flex flex-col items-center justify-center rounded-xl bg-neutral-200 dark:bg-neutral-800">
|
||||||
|
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
|
||||||
|
01.
|
||||||
|
</div>
|
||||||
|
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
|
||||||
|
Navigate between columns.
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 w-3/4 h-full pb-10">
|
||||||
|
<video
|
||||||
|
className="h-auto w-full rounded-lg shadow-md"
|
||||||
|
controls
|
||||||
|
muted
|
||||||
|
preload="none"
|
||||||
|
poster="https://image.nostr.build/143354665d94b20013fde14ad05e53e958e11eec568a11b273921d1808c410cc.png"
|
||||||
|
>
|
||||||
|
<source
|
||||||
|
src="https://video.nostr.build/8fc3598ef85a1be292cee4ad6ad85b2c8bbf86da0aefd693e60416b56ec96e5f.mp4"
|
||||||
|
type="video/mp4"
|
||||||
|
/>
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
|
<div className="relative flex flex-col items-center justify-center rounded-xl bg-neutral-200 dark:bg-neutral-800">
|
||||||
Navigate between columns.
|
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
|
||||||
|
02.
|
||||||
|
</div>
|
||||||
|
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
|
||||||
|
Add a new column.
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 w-3/4 h-full pb-10">
|
||||||
|
<video
|
||||||
|
className="h-auto w-full rounded-lg shadow-md"
|
||||||
|
controls
|
||||||
|
muted
|
||||||
|
preload="none"
|
||||||
|
poster="https://image.nostr.build/216015bc81931725c23171700b4d2d73556ecfe3efe662afd7eea6627574b506.png"
|
||||||
|
>
|
||||||
|
<source
|
||||||
|
src="https://video.nostr.build/cdb842a1ffc6864bab009a668f68acb0a672e04e9538dbe7e2ac44182722d956.mp4"
|
||||||
|
type="video/mp4"
|
||||||
|
/>
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 w-3/4 h-full pb-10">
|
<div className="relative flex flex-col items-center justify-center rounded-xl bg-neutral-200 dark:bg-neutral-800">
|
||||||
<video
|
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
|
||||||
className="h-auto w-full aspect-square rounded-lg shadow-md transform"
|
03.
|
||||||
controls
|
</div>
|
||||||
muted
|
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
|
||||||
preload="none"
|
View a thread.
|
||||||
poster="/poster_1.jpeg"
|
</div>
|
||||||
>
|
<div className="flex-1 w-3/4 h-full pb-10">
|
||||||
<source
|
<video
|
||||||
src="https://video.nostr.build/692f71e2be47ecfc29edcbdaa198cc5979bfb9c900f05d78682895dd546d8d4f.mp4"
|
className="h-auto w-full rounded-lg shadow-md"
|
||||||
type="video/mp4"
|
controls
|
||||||
/>
|
muted
|
||||||
Your browser does not support the video tag.
|
preload="none"
|
||||||
</video>
|
poster="https://image.nostr.build/3a3ead93bc64224e6397df4097998708b5aef4c6e7104f8e28c77db47ad1625a.png"
|
||||||
|
>
|
||||||
|
<source
|
||||||
|
src="https://video.nostr.build/088636b03976a4e54c5053c718300816a7c3f9f361a1fe7d8e7a0f663ab6a582.mp4"
|
||||||
|
type="video/mp4"
|
||||||
|
/>
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
|
</ScrollArea.Viewport>
|
||||||
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
|
<ScrollArea.Scrollbar
|
||||||
02.
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
</div>
|
orientation="vertical"
|
||||||
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
|
>
|
||||||
Switch between accounts.
|
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||||
</div>
|
</ScrollArea.Scrollbar>
|
||||||
<div className="flex-1 w-3/4 h-full pb-10">
|
<ScrollArea.Corner className="bg-transparent" />
|
||||||
<video
|
</ScrollArea.Root>
|
||||||
className="h-auto w-full aspect-square rounded-lg shadow-md transform"
|
|
||||||
controls
|
|
||||||
muted
|
|
||||||
preload="none"
|
|
||||||
poster="/poster_2.jpeg"
|
|
||||||
>
|
|
||||||
<source
|
|
||||||
src="https://video.nostr.build/d33962520506d86acfb4b55a7b265821e10ae637f5ec830a173b7e6092b16ec8.mp4"
|
|
||||||
type="video/mp4"
|
|
||||||
/>
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
|
|
||||||
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
|
|
||||||
03.
|
|
||||||
</div>
|
|
||||||
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
|
|
||||||
Open Lume Store.
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 w-3/4 h-full pb-10">
|
|
||||||
<video
|
|
||||||
className="h-auto w-full aspect-square rounded-lg shadow-md transform"
|
|
||||||
controls
|
|
||||||
muted
|
|
||||||
preload="none"
|
|
||||||
poster="/poster_3.jpeg"
|
|
||||||
>
|
|
||||||
<source
|
|
||||||
src="https://video.nostr.build/927abbfde2097e470ac751181b1db456b7e4b9149550408efff1a966a7ffb9a8.mp4"
|
|
||||||
type="video/mp4"
|
|
||||||
/>
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative flex flex-col items-center justify-center rounded-xl bg-black/10 dark:bg-white/10">
|
|
||||||
<div className="absolute top-2 left-3 text-2xl font-semibold font-serif text-neutral-600 dark:text-neutral-400">
|
|
||||||
04.
|
|
||||||
</div>
|
|
||||||
<div className="h-16 flex items-center justify-center shrink-0 px-3 text-lg select-text">
|
|
||||||
Use the Tray Menu.
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 w-3/4 h-full pb-10">
|
|
||||||
<video
|
|
||||||
className="h-auto w-full rounded-lg shadow-md transform"
|
|
||||||
controls
|
|
||||||
muted
|
|
||||||
preload="none"
|
|
||||||
poster="/poster_4.jpeg"
|
|
||||||
>
|
|
||||||
<source
|
|
||||||
src="https://video.nostr.build/513de4824b6abaf7e9698c1dad2f68096574356848c0c200bc8cb8074df29410.mp4"
|
|
||||||
type="video/mp4"
|
|
||||||
/>
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,37 +19,41 @@ function Screen() {
|
|||||||
const { events } = useRouterState({ select: (s) => s.location.state });
|
const { events } = useRouterState({ select: (s) => s.location.state });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="px-3 h-full">
|
||||||
<div className="h-10 shrink-0 border-b border-black/5 dark:border-white/5 flex items-center justify-between px-2">
|
<div className="size-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700">
|
||||||
<button
|
<div className="h-full flex flex-col">
|
||||||
type="button"
|
<div className="h-10 shrink-0 border-b border-black/5 dark:border-white/5 flex items-center justify-between px-2">
|
||||||
onClick={() => router.history.back()}
|
<button
|
||||||
className="inline-flex items-center justify-center gap-1.5 h-7 w-max px-1 text-sm font-semibold hover:bg-black/10 dark:hover:bg-white/10 rounded-md"
|
type="button"
|
||||||
>
|
onClick={() => router.history.back()}
|
||||||
<ArrowLeft className="size-4" />
|
className="inline-flex items-center justify-center gap-1.5 h-7 w-max px-1 text-sm font-semibold hover:bg-black/10 dark:hover:bg-white/10 rounded-md"
|
||||||
Back
|
>
|
||||||
</button>
|
<ArrowLeft className="size-4" />
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="overflow-hidden size-full flex-1"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport ref={ref} className="h-full p-3">
|
||||||
|
<Virtualizer scrollRef={ref}>
|
||||||
|
{events.map((event) => (
|
||||||
|
<ReplyNote key={event.id} event={event} />
|
||||||
|
))}
|
||||||
|
</Virtualizer>
|
||||||
|
</ScrollArea.Viewport>
|
||||||
|
<ScrollArea.Scrollbar
|
||||||
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||||
|
</ScrollArea.Scrollbar>
|
||||||
|
<ScrollArea.Corner className="bg-transparent" />
|
||||||
|
</ScrollArea.Root>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea.Root
|
|
||||||
type={"scroll"}
|
|
||||||
scrollHideDelay={300}
|
|
||||||
className="overflow-hidden size-full flex-1"
|
|
||||||
>
|
|
||||||
<ScrollArea.Viewport ref={ref} className="h-full p-3">
|
|
||||||
<Virtualizer scrollRef={ref}>
|
|
||||||
{events.map((event) => (
|
|
||||||
<ReplyNote key={event.id} event={event} />
|
|
||||||
))}
|
|
||||||
</Virtualizer>
|
|
||||||
</ScrollArea.Viewport>
|
|
||||||
<ScrollArea.Scrollbar
|
|
||||||
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
|
||||||
orientation="vertical"
|
|
||||||
>
|
|
||||||
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
|
||||||
</ScrollArea.Scrollbar>
|
|
||||||
<ScrollArea.Corner className="bg-transparent" />
|
|
||||||
</ScrollArea.Root>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,18 @@ function Screen() {
|
|||||||
|
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Text:
|
case Kind.Text:
|
||||||
return <TextNote key={event.id} event={event} className="mb-3" />;
|
return (
|
||||||
|
<TextNote
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
/>
|
||||||
|
);
|
||||||
case Kind.Metadata:
|
case Kind.Metadata:
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={event.id}
|
key={event.id}
|
||||||
className="p-3 mb-3 bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5"
|
className="p-3 border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
>
|
>
|
||||||
<User.Provider pubkey={event.pubkey}>
|
<User.Provider pubkey={event.pubkey}>
|
||||||
<User.Root className="flex flex-col w-full h-full gap-2">
|
<User.Root className="flex flex-col w-full h-full gap-2">
|
||||||
@@ -61,7 +67,13 @@ function Screen() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return <TextNote key={event.id} event={event} className="mb-3" />;
|
return (
|
||||||
|
<TextNote
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[events],
|
[events],
|
||||||
@@ -71,7 +83,7 @@ function Screen() {
|
|||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
if (!query.length) return;
|
if (!query.length) return;
|
||||||
|
|
||||||
const res = await commands.search(query, null);
|
const res = await commands.search(query);
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const data = toLumeEvents(res.data);
|
const data = toLumeEvents(res.data);
|
||||||
@@ -84,59 +96,61 @@ function Screen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 size-full overflow-hidden">
|
<div className="px-3 h-full">
|
||||||
<div className="h-9 shrink-0 px-3 flex items-center gap-2">
|
<div className="flex flex-col gap-3 size-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700">
|
||||||
<input
|
<div className="h-9 shrink-0 pt-6 px-3 flex items-center gap-2">
|
||||||
name="search"
|
<input
|
||||||
placeholder="Search nostr ..."
|
name="search"
|
||||||
value={query}
|
placeholder="Search nostr ..."
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
value={query}
|
||||||
onKeyDown={(event) => {
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
if (event.key === "Enter") search();
|
onKeyDown={(event) => {
|
||||||
}}
|
if (event.key === "Enter") search();
|
||||||
className="h-9 px-5 flex-1 rounded-full border-none bg-black/5 dark:bg-white/10 placeholder:text-neutral-500 dark:placeholder:text-neutral-400 focus:bg-black/10 dark:focus:bg-white/10 focus:outline-none focus:ring-0"
|
}}
|
||||||
/>
|
className="h-9 px-5 flex-1 rounded-full border-none bg-black/5 dark:bg-white/10 placeholder:text-neutral-500 dark:placeholder:text-neutral-400 focus:bg-black/10 dark:focus:bg-white/10 focus:outline-none focus:ring-0"
|
||||||
<button
|
/>
|
||||||
type="button"
|
<button
|
||||||
disabled={!query.length || isPending}
|
type="button"
|
||||||
className="size-9 shrink-0 inline-flex items-center justify-center rounded-full bg-black/5 dark:bg-white/10"
|
disabled={!query.length || isPending}
|
||||||
>
|
className="size-9 shrink-0 inline-flex items-center justify-center rounded-full bg-black/5 dark:bg-white/10"
|
||||||
{isPending ? (
|
>
|
||||||
<Spinner className="size-4" />
|
|
||||||
) : (
|
|
||||||
<MagnifyingGlass className="size-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ScrollArea.Root
|
|
||||||
type={"scroll"}
|
|
||||||
scrollHideDelay={300}
|
|
||||||
className="overflow-hidden size-full flex-1"
|
|
||||||
>
|
|
||||||
<ScrollArea.Viewport ref={ref} className="relative h-full px-3">
|
|
||||||
<Virtualizer scrollRef={ref}>
|
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<div className="w-full h-[200px] flex gap-2 items-center justify-center">
|
<Spinner className="size-4" />
|
||||||
<Spinner />
|
|
||||||
Searching...
|
|
||||||
</div>
|
|
||||||
) : !events.length ? (
|
|
||||||
<div className="w-full h-[200px] flex gap-2 items-center justify-center">
|
|
||||||
Type somethings to search.
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
events.map((event) => renderItem(event))
|
<MagnifyingGlass className="size-4" />
|
||||||
)}
|
)}
|
||||||
</Virtualizer>
|
</button>
|
||||||
</ScrollArea.Viewport>
|
</div>
|
||||||
<ScrollArea.Scrollbar
|
<ScrollArea.Root
|
||||||
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
type={"scroll"}
|
||||||
orientation="vertical"
|
scrollHideDelay={300}
|
||||||
|
className="overflow-hidden size-full flex-1"
|
||||||
>
|
>
|
||||||
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
<ScrollArea.Viewport ref={ref} className="relative h-full">
|
||||||
</ScrollArea.Scrollbar>
|
<Virtualizer scrollRef={ref}>
|
||||||
<ScrollArea.Corner className="bg-transparent" />
|
{isPending ? (
|
||||||
</ScrollArea.Root>
|
<div className="w-full h-[200px] flex gap-2 items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
Searching...
|
||||||
|
</div>
|
||||||
|
) : !events.length ? (
|
||||||
|
<div className="w-full h-[200px] flex gap-2 items-center justify-center">
|
||||||
|
Type somethings to search.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
events.map((event) => renderItem(event))
|
||||||
|
)}
|
||||||
|
</Virtualizer>
|
||||||
|
</ScrollArea.Viewport>
|
||||||
|
<ScrollArea.Scrollbar
|
||||||
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||||
|
</ScrollArea.Scrollbar>
|
||||||
|
<ScrollArea.Corner className="bg-transparent" />
|
||||||
|
</ScrollArea.Root>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { replyTime, toLumeEvents } from "@/commons";
|
import { replyTime, toLumeEvents } from "@/commons";
|
||||||
import { Note, Spinner, User } from "@/components";
|
import { Note, Spinner, User } from "@/components";
|
||||||
|
import { Hashtag } from "@/components/note/mentions/hashtag";
|
||||||
|
import { MentionUser } from "@/components/note/mentions/user";
|
||||||
import { type LumeEvent, LumeWindow } from "@/system";
|
import { type LumeEvent, LumeWindow } from "@/system";
|
||||||
import { ColumnsPlusLeft } from "@phosphor-icons/react";
|
import { Kind } from "@/types";
|
||||||
|
import { ArrowRight } from "@phosphor-icons/react";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
import { memo, useRef } from "react";
|
import { nip19 } from "nostr-tools";
|
||||||
|
import { type ReactNode, memo, useMemo, useRef } from "react";
|
||||||
|
import reactStringReplace from "react-string-replace";
|
||||||
import { Virtualizer } from "virtua";
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/columns/_layout/stories")({
|
export const Route = createLazyFileRoute("/columns/_layout/stories")({
|
||||||
@@ -14,7 +19,7 @@ export const Route = createLazyFileRoute("/columns/_layout/stories")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { contacts } = Route.useRouteContext();
|
const contacts = Route.useLoaderData();
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,9 +30,15 @@ function Screen() {
|
|||||||
>
|
>
|
||||||
<ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3">
|
<ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3">
|
||||||
<Virtualizer scrollRef={ref} overscan={0}>
|
<Virtualizer scrollRef={ref} overscan={0}>
|
||||||
{contacts.map((contact) => (
|
{!contacts ? (
|
||||||
<StoryItem key={contact} contact={contact} />
|
<div className="w-full h-24 flex items-center justify-center">
|
||||||
))}
|
<Spinner className="size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
contacts.map((contact) => (
|
||||||
|
<StoryItem key={contact} contact={contact} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
</Virtualizer>
|
</Virtualizer>
|
||||||
</ScrollArea.Viewport>
|
</ScrollArea.Viewport>
|
||||||
<ScrollArea.Scrollbar
|
<ScrollArea.Scrollbar
|
||||||
@@ -50,7 +61,7 @@ function StoryItem({ contact }: { contact: string }) {
|
|||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["stories", contact],
|
queryKey: ["stories", contact],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await commands.getEventsBy(contact, 10);
|
const res = await commands.getAllEventsByAuthor(contact, 10);
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const data = toLumeEvents(res.data);
|
const data = toLumeEvents(res.data);
|
||||||
@@ -59,13 +70,14 @@ function StoryItem({ contact }: { contact: string }) {
|
|||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
select: (data) => data.filter((ev) => ev.kind === Kind.Text),
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 flex flex-col w-full h-[300px] bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5">
|
<div className="mb-3 flex flex-col w-full h-[300px] bg-white dark:bg-black rounded-xl border-[.5px] border-neutral-300 dark:border-neutral-700">
|
||||||
<div className="h-12 shrink-0 px-2 flex items-center justify-between border-b border-neutral-100 dark:border-white/5">
|
<div className="h-12 shrink-0 px-2 flex items-center justify-between border-b border-neutral-100 dark:border-white/5">
|
||||||
<User.Provider pubkey={contact}>
|
<User.Provider pubkey={contact}>
|
||||||
<User.Root className="inline-flex items-center gap-2">
|
<User.Root className="inline-flex items-center gap-2">
|
||||||
@@ -77,9 +89,10 @@ function StoryItem({ contact }: { contact: string }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => LumeWindow.openProfile(contact)}
|
onClick={() => LumeWindow.openProfile(contact)}
|
||||||
className="size-7 inline-flex items-center justify-center rounded-lg text-neutral-500 hover:bg-neutral-100 dark:hover:bg-white/20"
|
className="h-7 w-max px-2.5 inline-flex gap-1 items-center justify-center rounded-full text-sm font-medium hover:bg-neutral-100 dark:hover:bg-white/20"
|
||||||
>
|
>
|
||||||
<ColumnsPlusLeft className="size-4" />
|
Open
|
||||||
|
<ArrowRight className="size-3" weight="bold" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,9 +142,10 @@ const StoryEvent = memo(function StoryEvent({ event }: { event: LumeEvent }) {
|
|||||||
className="shrink-0 inline font-medium text-blue-500"
|
className="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="pl-2 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">
|
||||||
@@ -148,3 +162,64 @@ const StoryEvent = memo(function StoryEvent({ 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>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import { commands } from "@/commands.gen";
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute("/columns/_layout/stories")({
|
export const Route = createFileRoute("/columns/_layout/stories")({
|
||||||
beforeLoad: async () => {
|
loader: async () => {
|
||||||
const res = await commands.getContactList();
|
const res = await commands.getContactList();
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const contacts = res.data;
|
return res.data;
|
||||||
return { contacts };
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Quote, RepostNote, Spinner, TextNote } from "@/components";
|
import { RepostNote, Spinner, TextNote } from "@/components";
|
||||||
import { LumeEvent } from "@/system";
|
import { LumeEvent } from "@/system";
|
||||||
import { Kind, type NostrEvent } from "@/types";
|
import { Kind, type NostrEvent } from "@/types";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
@@ -12,7 +12,7 @@ export const Route = createLazyFileRoute("/columns/_layout/trending")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const { isLoading, isError, data } = useQuery({
|
const { isLoading, data } = useQuery({
|
||||||
queryKey: ["trending-notes"],
|
queryKey: ["trending-notes"],
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
const res = await fetch("https://api.nostr.band/v0/trending/notes", {
|
const res = await fetch("https://api.nostr.band/v0/trending/notes", {
|
||||||
@@ -43,14 +43,21 @@ function Screen() {
|
|||||||
if (!event) return;
|
if (!event) return;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
return <RepostNote key={event.id} event={event} className="mb-3" />;
|
return (
|
||||||
default: {
|
<RepostNote
|
||||||
if (event.isQuote) {
|
key={event.id}
|
||||||
return <Quote key={event.id} event={event} className="mb-3" />;
|
event={event}
|
||||||
} else {
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
return <TextNote key={event.id} event={event} className="mb-3" />;
|
/>
|
||||||
}
|
);
|
||||||
}
|
default:
|
||||||
|
return (
|
||||||
|
<TextNote
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[data],
|
[data],
|
||||||
@@ -60,22 +67,21 @@ function Screen() {
|
|||||||
<ScrollArea.Root
|
<ScrollArea.Root
|
||||||
type={"scroll"}
|
type={"scroll"}
|
||||||
scrollHideDelay={300}
|
scrollHideDelay={300}
|
||||||
className="overflow-hidden size-full"
|
className="overflow-hidden size-full px-3"
|
||||||
>
|
>
|
||||||
<ScrollArea.Viewport ref={ref} className="h-full px-3">
|
<ScrollArea.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
>
|
||||||
<Virtualizer scrollRef={ref} overscan={1}>
|
<Virtualizer scrollRef={ref} overscan={1}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex flex-col items-center justify-center w-full h-20 gap-1">
|
<div className="flex items-center justify-center w-full h-16 gap-2">
|
||||||
<div className="inline-flex items-center gap-2 text-sm font-medium">
|
<Spinner className="size-4" />
|
||||||
<Spinner className="size-5" />
|
<span className="text-sm font-medium">Loading...</span>
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : isError ? (
|
) : !data.length ? (
|
||||||
<div className="flex flex-col items-center justify-center w-full h-20 gap-1">
|
<div className="mb-3 flex items-center justify-center h-20 text-sm">
|
||||||
<div className="inline-flex items-center gap-2 text-sm font-medium">
|
🎉 Yo. You're catching up on all latest notes.
|
||||||
Error.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.map((item) => renderItem(item))
|
data.map((item) => renderItem(item))
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import { toLumeEvents } from "@/commons";
|
import { toLumeEvents } from "@/commons";
|
||||||
import { Spinner } from "@/components";
|
import { RepostNote, Spinner, TextNote, User } from "@/components";
|
||||||
import { Quote } from "@/components/quote";
|
|
||||||
import { RepostNote } from "@/components/repost";
|
|
||||||
import { TextNote } from "@/components/text";
|
|
||||||
import { User } from "@/components/user";
|
|
||||||
import type { LumeEvent } from "@/system";
|
import type { LumeEvent } from "@/system";
|
||||||
import { Kind } from "@/types";
|
import { Kind } from "@/types";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
@@ -22,7 +18,7 @@ function Screen() {
|
|||||||
const { isLoading, data: events } = useQuery({
|
const { isLoading, data: events } = useQuery({
|
||||||
queryKey: ["stories", id],
|
queryKey: ["stories", id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await commands.getEventsBy(id, 20);
|
const res = await commands.getAllEventsByAuthor(id, 100);
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const data = toLumeEvents(res.data);
|
const data = toLumeEvents(res.data);
|
||||||
@@ -41,13 +37,21 @@ function Screen() {
|
|||||||
if (!event) return;
|
if (!event) return;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case Kind.Repost:
|
case Kind.Repost:
|
||||||
return <RepostNote key={event.id} event={event} className="mb-3" />;
|
return (
|
||||||
default: {
|
<RepostNote
|
||||||
if (event.isQuote) {
|
key={event.id}
|
||||||
return <Quote key={event.id} event={event} className="mb-3" />;
|
event={event}
|
||||||
}
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
return <TextNote key={event.id} event={event} className="mb-3" />;
|
/>
|
||||||
}
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<TextNote
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[events],
|
[events],
|
||||||
@@ -57,21 +61,24 @@ function Screen() {
|
|||||||
<ScrollArea.Root
|
<ScrollArea.Root
|
||||||
type={"scroll"}
|
type={"scroll"}
|
||||||
scrollHideDelay={300}
|
scrollHideDelay={300}
|
||||||
className="overflow-hidden size-full"
|
className="overflow-hidden size-full px-3"
|
||||||
>
|
>
|
||||||
<ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3">
|
<ScrollArea.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className="relative h-full bg-white dark:bg-black rounded-t-xl shadow shadow-neutral-300/50 dark:shadow-none border-[.5px] border-neutral-300 dark:border-neutral-700"
|
||||||
|
>
|
||||||
<Virtualizer scrollRef={ref} overscan={0}>
|
<Virtualizer scrollRef={ref} overscan={0}>
|
||||||
<User.Provider pubkey={id}>
|
<User.Provider pubkey={id}>
|
||||||
<User.Root className="relative">
|
<User.Root className="relative">
|
||||||
<User.Cover className="object-cover w-full h-44 rounded-t-lg gradient-mask-b-0" />
|
<User.Cover className="object-cover w-full h-44 rounded-t-lg gradient-mask-b-0" />
|
||||||
<User.Button className="z-10 absolute top-4 right-4 inline-flex items-center justify-center w-20 text-xs font-medium text-white shadow-md bg-black hover:bg-black/80 rounded-full h-7" />
|
<User.Button className="z-10 absolute top-4 right-4 inline-flex items-center justify-center w-20 text-xs font-medium text-white shadow-md bg-black hover:bg-black/80 rounded-full h-7" />
|
||||||
<div className="z-10 relative flex flex-col items-center gap-1.5 -mt-16">
|
<div className="z-10 relative flex flex-col gap-1.5 -mt-16 px-4">
|
||||||
<User.Avatar className="rounded-full size-14" />
|
<User.Avatar className="rounded-full size-14" />
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex gap-1">
|
||||||
<User.Name className="text-lg font-semibold leading-tight" />
|
<User.Name className="text-lg font-semibold leading-tight" />
|
||||||
<User.NIP05 />
|
<User.NIP05 />
|
||||||
</div>
|
</div>
|
||||||
<User.About className="text-center" />
|
<User.About className="text-sm" />
|
||||||
</div>
|
</div>
|
||||||
</User.Root>
|
</User.Root>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
|
|||||||
+36
-33
@@ -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);
|
||||||
|
|
||||||
@@ -58,7 +84,7 @@ function Screen() {
|
|||||||
const res = await commands.login(value, password);
|
const res = await commands.login(value, password);
|
||||||
|
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
const settings = await commands.getSettings();
|
const settings = await commands.getUserSettings();
|
||||||
|
|
||||||
if (settings.status === "ok") {
|
if (settings.status === "ok") {
|
||||||
appSettings.setState(() => settings.data);
|
appSettings.setState(() => settings.data);
|
||||||
@@ -69,12 +95,14 @@ function Screen() {
|
|||||||
if (status) {
|
if (status) {
|
||||||
navigate({
|
navigate({
|
||||||
to: "/$account/home",
|
to: "/$account/home",
|
||||||
|
// @ts-ignore, this is tanstack router bug
|
||||||
params: { account: res.data },
|
params: { account: res.data },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
navigate({
|
navigate({
|
||||||
to: "/loading",
|
to: "/loading",
|
||||||
|
// @ts-ignore, this is tanstack router bug
|
||||||
search: { account: res.data },
|
search: { account: res.data },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
@@ -86,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();
|
||||||
@@ -204,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">
|
||||||
@@ -216,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: accounts.filter((account) => !account.endsWith("Lume")) };
|
return {
|
||||||
|
accounts: accounts.filter((account) => !account.endsWith("Lume")),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+22
-11
@@ -1,4 +1,5 @@
|
|||||||
import { Spinner } from "@/components";
|
import { commands } from "@/commands.gen";
|
||||||
|
import { Frame, Spinner } from "@/components";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -21,12 +22,19 @@ function Screen() {
|
|||||||
const search = Route.useSearch();
|
const search = Route.useSearch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = listen("synchronized", () => {
|
const unlisten = listen("neg_synchronized", async () => {
|
||||||
navigate({
|
const status = await commands.createSyncFile(search.account);
|
||||||
to: "/$account/home",
|
|
||||||
params: { account: search.account },
|
if (status) {
|
||||||
replace: true,
|
navigate({
|
||||||
});
|
to: "/$account/home",
|
||||||
|
// @ts-ignore, this is tanstack router bug
|
||||||
|
params: { account: search.account },
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("System error.");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -36,12 +44,15 @@ function Screen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full flex items-center justify-center">
|
<div className="size-full flex items-center justify-center">
|
||||||
<div className="flex flex-col gap-2 items-center justify-center text-center">
|
<Frame
|
||||||
|
className="p-6 h-36 flex flex-col gap-2 items-center justify-center text-center rounded-xl overflow-hidden"
|
||||||
|
shadow
|
||||||
|
>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<p className="text-sm">
|
<p className="text-sm text-neutral-600 dark:text-neutral-40">
|
||||||
Fetching necessary data for the first time login...
|
Syncing all necessary data for the first time login...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</Frame>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-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,75 @@ 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 font-medium">Continue with Nostr Connect</h3>
|
||||||
</Link>
|
<p className="text-xs text-neutral-500 dark:text-neutral-600">
|
||||||
<div className="w-full h-px bg-black/5 dark:bg-white/5" />
|
Your account will be handled by a remote signer. Lume will not
|
||||||
|
store your account keys.
|
||||||
|
</p>
|
||||||
|
</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 font-medium">Continue with Secret Key</h3>
|
||||||
|
<p className="text-xs 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>
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
|
import { Spinner } from "@/components";
|
||||||
|
import { User } from "@/components/user";
|
||||||
|
import { Plus, X } from "@phosphor-icons/react";
|
||||||
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/set-group")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const contacts = Route.useLoaderData();
|
||||||
|
const { account } = Route.useSearch();
|
||||||
|
const { queryClient } = Route.useRouteContext();
|
||||||
|
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [npub, setNpub] = useState("");
|
||||||
|
const [users, setUsers] = useState<string[]>([]);
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const toggleUser = (pubkey: string) => {
|
||||||
|
setUsers((prev) =>
|
||||||
|
prev.includes(pubkey)
|
||||||
|
? prev.filter((i) => i !== pubkey)
|
||||||
|
: [...prev, pubkey],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addUser = () => {
|
||||||
|
if (!npub.startsWith("npub1")) return;
|
||||||
|
if (users.includes(npub)) return;
|
||||||
|
|
||||||
|
setUsers((prev) => [...prev, npub]);
|
||||||
|
setNpub("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
const res = await commands.setGroup(title, null, null, users);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
const window = getCurrentWindow();
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["mygroups", account],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create column in the main window
|
||||||
|
await window.emitTo("main", "columns", {
|
||||||
|
type: "add",
|
||||||
|
column: {
|
||||||
|
label: res.data,
|
||||||
|
name: title,
|
||||||
|
url: `/columns/groups/${res.data}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close current popup
|
||||||
|
await window.close();
|
||||||
|
} else {
|
||||||
|
await message(res.error, { kind: "error" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col size-full">
|
||||||
|
<div data-tauri-drag-region className="shrink-0 h-11" />
|
||||||
|
<div className="shrink-0 h-14 px-3 flex items-center gap-2 justify-between border-b border-black/5 dark:border-white/5">
|
||||||
|
<div className="flex items-center flex-1 rounded-lg h-9 shrink-0 bg-black/10 dark:bg-white/10">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="w-16 text-sm font-semibold text-center border-r border-neutral-300 dark:border-neutral-700 shrink-0"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="family, bff, devs,..."
|
||||||
|
className="h-full px-3 text-sm bg-transparent border-none placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => submit()}
|
||||||
|
disabled={isPending || users.length < 1}
|
||||||
|
className="shrink-0 inline-flex items-center justify-center text-sm font-medium text-white bg-blue-500 rounded-lg w-20 h-9 hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isPending ? <Spinner /> : "Create"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="flex-1 overflow-hidden"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport className="bg-white dark:bg-black h-full p-3">
|
||||||
|
<div className="mb-3 flex flex-col gap-2">
|
||||||
|
<h3 className="text-sm font-semibold">Added</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
name="npub"
|
||||||
|
value={npub}
|
||||||
|
onChange={(e) => setNpub(e.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") addUser();
|
||||||
|
}}
|
||||||
|
placeholder="npub1..."
|
||||||
|
className="w-full px-3 text-sm border-none rounded-lg h-9 bg-neutral-100 dark:bg-neutral-900 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => addUser()}
|
||||||
|
className="inline-flex items-center justify-center text-neutral-500 rounded-lg size-9 bg-neutral-200 dark:bg-neutral-800 shrink-0 hover:bg-blue-500 hover:text-white"
|
||||||
|
>
|
||||||
|
<Plus className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{users.length ? (
|
||||||
|
users.map((item: string) => (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleUser(item)}
|
||||||
|
className="inline-flex items-center justify-between px-3 py-2 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
|
||||||
|
>
|
||||||
|
<User.Provider pubkey={item}>
|
||||||
|
<User.Root className="flex items-center gap-2.5">
|
||||||
|
<User.Avatar className="rounded-full size-8" />
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<User.Name className="text-sm font-medium" />
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
<X className="size-4" />
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center text-sm rounded-lg bg-neutral-100 dark:bg-neutral-900 h-14">
|
||||||
|
Please add some user to your group.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h3 className="text-sm font-semibold">Contacts</h3>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{contacts.length ? (
|
||||||
|
contacts
|
||||||
|
.filter((c) => !users.includes(c))
|
||||||
|
.map((item: string) => (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleUser(item)}
|
||||||
|
className="inline-flex items-center justify-between px-3 py-2 rounded-lg border-[.5px] border-neutral-300 dark:border-neutral-700 hover:border-blue-500"
|
||||||
|
>
|
||||||
|
<User.Provider pubkey={item}>
|
||||||
|
<User.Root className="flex items-center gap-2.5">
|
||||||
|
<User.Avatar className="rounded-full size-8" />
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<User.Name className="text-sm font-medium" />
|
||||||
|
</div>
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center text-sm rounded-lg bg-black/5 dark:bg-white/5 h-14">
|
||||||
|
<p>
|
||||||
|
Find more user at{" "}
|
||||||
|
<a
|
||||||
|
href="https://www.nostr.directory/"
|
||||||
|
target="_blank"
|
||||||
|
className="text-blue-600 after:content-['_↗']"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Nostr Directory
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea.Viewport>
|
||||||
|
<ScrollArea.Scrollbar
|
||||||
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||||
|
</ScrollArea.Scrollbar>
|
||||||
|
<ScrollArea.Corner className="bg-transparent" />
|
||||||
|
</ScrollArea.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
type RouteSearch = {
|
||||||
|
account: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/set-group")({
|
||||||
|
validateSearch: (search: Record<string, string>): RouteSearch => {
|
||||||
|
return {
|
||||||
|
account: search.account,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
loader: async () => {
|
||||||
|
const res = await commands.getContactList();
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
return res.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
|
import { cn } from "@/commons";
|
||||||
|
import { Spinner } from "@/components";
|
||||||
|
import { Plus, X } from "@phosphor-icons/react";
|
||||||
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import { message } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
|
||||||
|
const TOPICS = [
|
||||||
|
{
|
||||||
|
title: "Popular hashtags",
|
||||||
|
content: [
|
||||||
|
"#nostr",
|
||||||
|
"#introductions",
|
||||||
|
"#grownostr",
|
||||||
|
"#zap",
|
||||||
|
"#meme",
|
||||||
|
"#asknostr",
|
||||||
|
"#bitcoin",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/set-interest")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [hashtag, setHashtag] = useState("");
|
||||||
|
const [hashtags, setHashtags] = useState<string[]>([]);
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const { account } = Route.useSearch();
|
||||||
|
const { queryClient } = Route.useRouteContext();
|
||||||
|
|
||||||
|
const toggleHashtag = (tag: string) => {
|
||||||
|
setHashtags((prev) =>
|
||||||
|
prev.includes(tag) ? prev.filter((i) => i !== tag) : [...prev, tag],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addHashtag = () => {
|
||||||
|
if (!hashtag.startsWith("#")) return;
|
||||||
|
if (hashtags.includes(hashtag)) return;
|
||||||
|
|
||||||
|
setHashtags((prev) => [...prev, hashtag]);
|
||||||
|
setHashtag("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
const content = hashtags.map((tag) =>
|
||||||
|
tag.toLowerCase().replace(" ", "-").replace("#", ""),
|
||||||
|
);
|
||||||
|
const res = await commands.setInterest(title, null, null, content);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
const window = getCurrentWindow();
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["myinterests", account],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create column in the main window
|
||||||
|
await window.emitTo("main", "columns", {
|
||||||
|
type: "add",
|
||||||
|
column: {
|
||||||
|
label: res.data,
|
||||||
|
name: title,
|
||||||
|
url: `/columns/interests/${res.data}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close current popup
|
||||||
|
await window.close();
|
||||||
|
} else {
|
||||||
|
await message(res.error, { kind: "error" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col size-full">
|
||||||
|
<div data-tauri-drag-region className="shrink-0 h-11" />
|
||||||
|
<div className="shrink-0 h-14 px-3 flex items-center gap-2 justify-between border-b border-black/5 dark:border-white/5">
|
||||||
|
<div className="flex items-center flex-1 rounded-lg h-9 shrink-0 bg-black/10 dark:bg-white/10">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="w-16 text-sm font-semibold text-center border-r border-neutral-300 dark:border-neutral-700 shrink-0"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="anime, sport, art,..."
|
||||||
|
className="h-full px-3 text-sm bg-transparent border-none placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => submit()}
|
||||||
|
className="shrink-0 inline-flex items-center justify-center text-sm font-medium text-white bg-blue-500 rounded-lg w-20 h-9 hover:bg-blue-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isPending ? <Spinner /> : "Create"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="flex-1 overflow-hidden"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport className="bg-white dark:bg-black h-full p-3">
|
||||||
|
<div className="mb-3 flex flex-col gap-2">
|
||||||
|
<span className="text-sm font-semibold">Added</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
name="hashtag"
|
||||||
|
placeholder="#nostr"
|
||||||
|
onChange={(e) => setHashtag(e.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") addHashtag();
|
||||||
|
}}
|
||||||
|
className="w-full px-3 text-sm border-none rounded-lg h-9 bg-neutral-100 dark:bg-neutral-900 placeholder:text-neutral-600 focus:border-neutral-500 focus:ring-0 dark:placeholder:text-neutral-400"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => addHashtag()}
|
||||||
|
className="inline-flex items-center justify-center text-neutral-500 rounded-lg size-9 bg-neutral-200 dark:bg-neutral-800 shrink-0 hover:bg-blue-500 hover:text-white"
|
||||||
|
>
|
||||||
|
<Plus className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{hashtags.length ? (
|
||||||
|
hashtags.map((item: string) => (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleHashtag(item)}
|
||||||
|
className="inline-flex items-center justify-between p-3 bg-white rounded-lg dark:bg-black/20 shadow-primary dark:ring-1 ring-neutral-800/50"
|
||||||
|
>
|
||||||
|
<p>{item}</p>
|
||||||
|
<X className="size-4" />
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center text-sm rounded-lg bg-neutral-100 dark:bg-neutral-900 h-14">
|
||||||
|
Please add some hashtag.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{TOPICS.map((topic) => (
|
||||||
|
<div key={topic.title} className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-semibold">{topic.title}</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{topic.content.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleHashtag(item)}
|
||||||
|
className={cn(
|
||||||
|
"text-sm p-2 rounded-full",
|
||||||
|
hashtags.includes(item)
|
||||||
|
? "bg-blue-500 text-white"
|
||||||
|
: "bg-neutral-100 dark:bg-neutral-900",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea.Viewport>
|
||||||
|
<ScrollArea.Scrollbar
|
||||||
|
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
|
||||||
|
orientation="vertical"
|
||||||
|
>
|
||||||
|
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
|
||||||
|
</ScrollArea.Scrollbar>
|
||||||
|
<ScrollArea.Corner className="bg-transparent" />
|
||||||
|
</ScrollArea.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
type RouteSearch = {
|
||||||
|
account: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/set-interest")({
|
||||||
|
validateSearch: (search: Record<string, string>): RouteSearch => {
|
||||||
|
return {
|
||||||
|
account: search.account,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -4,11 +4,22 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { LumeEvent } from "../event";
|
import { LumeEvent } from "../event";
|
||||||
|
|
||||||
export function useEvent(id: string) {
|
export function useEvent(id: string, repost?: string) {
|
||||||
const { isLoading, isError, error, data } = useQuery({
|
const { isLoading, isError, error, data } = useQuery({
|
||||||
queryKey: ["event", id],
|
queryKey: ["event", id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
|
if (repost?.length) {
|
||||||
|
const nostrEvent: NostrEvent = JSON.parse(repost);
|
||||||
|
const res = await commands.getEventMeta(nostrEvent.content);
|
||||||
|
|
||||||
|
if (res.status === "ok") {
|
||||||
|
nostrEvent.meta = res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LumeEvent(nostrEvent);
|
||||||
|
}
|
||||||
|
|
||||||
// Validate ID
|
// Validate ID
|
||||||
let normalizeId: string = id
|
let normalizeId: string = id
|
||||||
.replace("nostr:", "")
|
.replace("nostr:", "")
|
||||||
|
|||||||
+23
-8
@@ -1,6 +1,7 @@
|
|||||||
import { commands } from "@/commands.gen";
|
import { commands } from "@/commands.gen";
|
||||||
import type { LumeColumn, NostrEvent } from "@/types";
|
import type { LumeColumn, NostrEvent } from "@/types";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
import type { LumeEvent } from "./event";
|
import type { LumeEvent } from "./event";
|
||||||
|
|
||||||
export const LumeWindow = {
|
export const LumeWindow = {
|
||||||
@@ -14,9 +15,9 @@ export const LumeWindow = {
|
|||||||
await getCurrentWindow().emit("columns", {
|
await getCurrentWindow().emit("columns", {
|
||||||
type: "add",
|
type: "add",
|
||||||
column: {
|
column: {
|
||||||
label: "columns_gallery",
|
label: "launchpad",
|
||||||
name: "Columns Gallery",
|
name: "Launchpad",
|
||||||
url: "/columns/gallery",
|
url: "/columns/launchpad",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -24,8 +25,8 @@ export const LumeWindow = {
|
|||||||
await getCurrentWindow().emit("columns", {
|
await getCurrentWindow().emit("columns", {
|
||||||
type: "add",
|
type: "add",
|
||||||
column: {
|
column: {
|
||||||
label: "local_feeds",
|
label: "newsfeed",
|
||||||
name: "Local Feeds",
|
name: "Newsfeed",
|
||||||
url: "/columns/newsfeed",
|
url: "/columns/newsfeed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -150,8 +151,22 @@ export const LumeWindow = {
|
|||||||
throw new Error(query.error);
|
throw new Error(query.error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openMainWindow: async () => {
|
openPopup: async (title: string, url: string) => {
|
||||||
const query = await commands.reopenLume();
|
const query = await commands.openWindow({
|
||||||
return query;
|
label: `popup-${nanoid()}`,
|
||||||
|
url,
|
||||||
|
title,
|
||||||
|
width: 400,
|
||||||
|
height: 500,
|
||||||
|
maximizable: false,
|
||||||
|
minimizable: false,
|
||||||
|
hidden_title: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (query.status === "ok") {
|
||||||
|
return query.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(query.error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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