Release v4.1 (#229)
* refactor: remove custom icon packs * fix: command not work on windows * fix: make open_window command async * feat: improve commands * feat: improve * refactor: column * feat: improve thread column * feat: improve * feat: add stories column * feat: improve * feat: add search column * feat: add reset password * feat: add subscription * refactor: settings * chore: improve commands * fix: crash on production * feat: use tauri store plugin for cache * feat: new icon * chore: update icon for windows * chore: improve some columns * chore: polish code
@@ -22,6 +22,8 @@
|
||||
"@tanstack/query-persist-client-core": "^5.51.21",
|
||||
"@tanstack/react-query": "^5.51.23",
|
||||
"@tanstack/react-router": "^1.48.1",
|
||||
"@tanstack/react-store": "^0.5.5",
|
||||
"@tanstack/store": "^0.5.5",
|
||||
"@tauri-apps/api": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-dialog": "2.0.0-rc.0",
|
||||
@@ -30,10 +32,12 @@
|
||||
"@tauri-apps/plugin-os": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-process": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-store": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-updater": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-upload": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-window-state": "2.0.0-rc.0",
|
||||
"bitcoin-units": "^1.0.0",
|
||||
"boring-avatars": "^1.10.2",
|
||||
"dayjs": "^1.11.12",
|
||||
"embla-carousel-react": "^8.1.8",
|
||||
"i18next": "^23.13.0",
|
||||
|
||||
24
pnpm-lock.yaml
generated
@@ -44,6 +44,12 @@ importers:
|
||||
'@tanstack/react-router':
|
||||
specifier: ^1.48.1
|
||||
version: 1.48.1(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)
|
||||
'@tanstack/react-store':
|
||||
specifier: ^0.5.5
|
||||
version: 0.5.5(react-dom@19.0.0-rc-d025ddd3-20240722(react@19.0.0-rc-d025ddd3-20240722))(react@19.0.0-rc-d025ddd3-20240722)
|
||||
'@tanstack/store':
|
||||
specifier: ^0.5.5
|
||||
version: 0.5.5
|
||||
'@tauri-apps/api':
|
||||
specifier: 2.0.0-rc.1
|
||||
version: 2.0.0-rc.1
|
||||
@@ -68,6 +74,9 @@ importers:
|
||||
'@tauri-apps/plugin-shell':
|
||||
specifier: 2.0.0-rc.0
|
||||
version: 2.0.0-rc.0
|
||||
'@tauri-apps/plugin-store':
|
||||
specifier: 2.0.0-rc.0
|
||||
version: 2.0.0-rc.0
|
||||
'@tauri-apps/plugin-updater':
|
||||
specifier: 2.0.0-rc.0
|
||||
version: 2.0.0-rc.0
|
||||
@@ -80,6 +89,9 @@ importers:
|
||||
bitcoin-units:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
boring-avatars:
|
||||
specifier: ^1.10.2
|
||||
version: 1.10.2
|
||||
dayjs:
|
||||
specifier: ^1.11.12
|
||||
version: 1.11.12
|
||||
@@ -1214,6 +1226,9 @@ packages:
|
||||
'@tauri-apps/plugin-shell@2.0.0-rc.0':
|
||||
resolution: {integrity: sha512-bhUcQcrqZoK8H1DFXapr5r1Z75oh6Kd5Tltz97XpZFLREEqp+KhN2Fvyh8r/fKAyenYsTYUIsDsyGdjdueuF9g==}
|
||||
|
||||
'@tauri-apps/plugin-store@2.0.0-rc.0':
|
||||
resolution: {integrity: sha512-KqiEzq6EdRwxrl0/FwyNLwumDBM91xTchdu2a8vfkNub30GuP9z7RskP9ifVRI1gbxfa5TUDi0hKFk/SP7TANQ==}
|
||||
|
||||
'@tauri-apps/plugin-updater@2.0.0-rc.0':
|
||||
resolution: {integrity: sha512-EKajf/sBpFif0cwXhTo3BmNvTZ2t2DDLRyhA8FFKugZNoOeqU97bHhPT5DIqMUPRE1tyDk9o7sXm8dKf7oz+EA==}
|
||||
|
||||
@@ -1340,6 +1355,9 @@ packages:
|
||||
bitcoin-units@1.0.0:
|
||||
resolution: {integrity: sha512-brac+Ttz7ovf/8D0jQHSWHnN2hmdjxDRBStxhjO752URLJlQIFpfZxzUteSZ81UYnRNiMkvsW9WsYPDuxHfnYA==}
|
||||
|
||||
boring-avatars@1.10.2:
|
||||
resolution: {integrity: sha512-uQyvmNeW6loz4Yytj7aDo0IhhuByW/YyOHtqwb3kQ/x48hum22hROA2Wn3qzrLR5JsoZ+FHjPO6z6LrXrWjegg==}
|
||||
|
||||
brace-expansion@2.0.1:
|
||||
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||
|
||||
@@ -3160,6 +3178,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.0.0-rc.1
|
||||
|
||||
'@tauri-apps/plugin-store@2.0.0-rc.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.0.0-rc.1
|
||||
|
||||
'@tauri-apps/plugin-updater@2.0.0-rc.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.0.0-rc.1
|
||||
@@ -3304,6 +3326,8 @@ snapshots:
|
||||
dependencies:
|
||||
big.js: 6.2.1
|
||||
|
||||
boring-avatars@1.10.2: {}
|
||||
|
||||
brace-expansion@2.0.1:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
104
src-tauri/Cargo.lock
generated
@@ -8,6 +8,49 @@ version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
|
||||
[[package]]
|
||||
name = "Lume"
|
||||
version = "4.0.0"
|
||||
dependencies = [
|
||||
"border",
|
||||
"cocoa 0.25.0",
|
||||
"futures",
|
||||
"keyring",
|
||||
"keyring-search",
|
||||
"linkify",
|
||||
"monitor",
|
||||
"nostr-sdk",
|
||||
"objc",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
"specta-typescript",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-nspanel",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-decorum",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-prevent-default",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-theme",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-upload",
|
||||
"tauri-plugin-window-state",
|
||||
"tauri-specta",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.22.0"
|
||||
@@ -2936,48 +2979,6 @@ dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lume"
|
||||
version = "4.0.0"
|
||||
dependencies = [
|
||||
"border",
|
||||
"cocoa 0.25.0",
|
||||
"futures",
|
||||
"keyring",
|
||||
"keyring-search",
|
||||
"linkify",
|
||||
"monitor",
|
||||
"nostr-sdk",
|
||||
"objc",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
"specta-typescript",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-nspanel",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-decorum",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-prevent-default",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-theme",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-upload",
|
||||
"tauri-plugin-window-state",
|
||||
"tauri-specta",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@@ -5678,9 +5679,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-prevent-default"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23ee986aa5872bfa37762e06d86e60325f721d602a436165f4be34a16ff8ae3e"
|
||||
checksum = "02858534169f6f6276ba723a00b6a4582b6606c4da47451406fc01f212423aec"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"itertools",
|
||||
@@ -5722,6 +5723,21 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-store"
|
||||
version = "2.0.0-rc.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfc658c2fe037e25e8af70d72ce2a757203be3fd8db13893fe872fa9847f9cac"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-theme"
|
||||
version = "0.4.1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "lume"
|
||||
name = "Lume"
|
||||
version = "4.0.0"
|
||||
description = "nostr client"
|
||||
authors = ["npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445"]
|
||||
@@ -34,9 +34,10 @@ tauri-plugin-process = "2.0.0-rc"
|
||||
tauri-plugin-shell = "2.0.0-rc"
|
||||
tauri-plugin-updater = "2.0.0-rc"
|
||||
tauri-plugin-upload = "2.0.0-rc"
|
||||
tauri-plugin-store = "2.0.0-rc"
|
||||
tauri-plugin-theme = "0.4.1"
|
||||
tauri-plugin-decorum = "1.0.0"
|
||||
tauri-plugin-prevent-default = "0.3"
|
||||
tauri-plugin-prevent-default = "0.4"
|
||||
tauri-specta = { version = "2.0.0-rc.15", features = ["derive", "typescript"] }
|
||||
specta = "^2.0.0-rc.20"
|
||||
specta-typescript = "0.0.7"
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
"core:menu:allow-popup",
|
||||
"http:default",
|
||||
"shell:allow-open",
|
||||
"store:allow-get",
|
||||
"store:allow-set",
|
||||
"store:allow-delete",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"identifier": "desktop-capability",
|
||||
"description": "Capability for the desktop",
|
||||
"platforms": [
|
||||
"linux",
|
||||
"macOS",
|
||||
"windows"
|
||||
],
|
||||
@@ -63,6 +62,9 @@
|
||||
"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",
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","panel","settings","search-*","zap-*","event-*","user-*","editor-*"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","core:window:allow-create","core:window:allow-close","core:window:allow-destroy","core:window:allow-set-focus","core:window:allow-center","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-set-size","core:window:allow-set-focus","core:window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-set-webview-size","core:webview:allow-set-webview-position","core:webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","process:allow-exit","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","core:menu:allow-new","core:menu:allow-popup","shell:allow-open","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":["linux","macOS","windows"]}}
|
||||
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","panel","settings","search-*","zap-*","event-*","user-*","editor-*"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","core:window:allow-create","core:window:allow-close","core:window:allow-destroy","core:window:allow-set-focus","core:window:allow-center","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-set-size","core:window:allow-set-focus","core:window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-set-webview-size","core:webview:allow-set-webview-position","core:webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","process:allow-exit","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","core:menu:allow-new","core:menu:allow-popup","shell:allow-open","store:allow-get","store:allow-set","store:allow-delete","prevent-default:default",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["macOS","windows"]}}
|
||||
@@ -7201,6 +7201,181 @@
|
||||
"shell:deny-stdin-write"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:default -> This permission set configures what kind of\noperations are available from the store plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:default"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-clear -> Enables the clear command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-clear"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-delete -> Enables the delete command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-entries -> Enables the entries command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-entries"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-get -> Enables the get command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-has -> Enables the has command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-has"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-keys -> Enables the keys command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-keys"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-length -> Enables the length command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-length"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-load -> Enables the load command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-load"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-reset -> Enables the reset command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-reset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-save -> Enables the save command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-save"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-set -> Enables the set command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-set"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-values -> Enables the values command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-clear -> Denies the clear command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-clear"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-delete -> Denies the delete command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-entries -> Denies the entries command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-entries"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-get -> Denies the get command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-has -> Denies the has command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-has"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-keys -> Denies the keys command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-keys"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-length -> Denies the length command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-length"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-load -> Denies the load command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-load"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-reset -> Denies the reset command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-reset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-save -> Denies the save command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-save"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-set -> Denies the set command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-set"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-values -> Denies the values command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -7201,6 +7201,181 @@
|
||||
"shell:deny-stdin-write"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:default -> This permission set configures what kind of\noperations are available from the store plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:default"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-clear -> Enables the clear command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-clear"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-delete -> Enables the delete command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-entries -> Enables the entries command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-entries"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-get -> Enables the get command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-has -> Enables the has command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-has"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-keys -> Enables the keys command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-keys"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-length -> Enables the length command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-length"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-load -> Enables the load command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-load"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-reset -> Enables the reset command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-reset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-save -> Enables the save command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-save"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-set -> Enables the set command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-set"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:allow-values -> Enables the values command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:allow-values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-clear -> Denies the clear command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-clear"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-delete -> Denies the delete command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-entries -> Denies the entries command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-entries"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-get -> Denies the get command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-has -> Denies the has command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-has"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-keys -> Denies the keys command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-keys"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-length -> Denies the length command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-length"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-load -> Denies the load command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-load"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-reset -> Denies the reset command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-reset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-save -> Denies the save command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-save"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-set -> Denies the set command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-set"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "store:deny-values -> Denies the values command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"store:deny-values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 984 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 546 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
83
src-tauri/resources/columns.json
Normal file
@@ -0,0 +1,83 @@
|
||||
[
|
||||
{
|
||||
"default": true,
|
||||
"official": true,
|
||||
"label": "onboarding",
|
||||
"name": "Onboarding",
|
||||
"description": "Tips for Mastering Lume.",
|
||||
"url": "/columns/onboarding",
|
||||
"picture": ""
|
||||
},
|
||||
{
|
||||
"default": true,
|
||||
"official": true,
|
||||
"label": "columns_gallery",
|
||||
"name": "Columns Gallery",
|
||||
"description": "Expand your experiences.",
|
||||
"url": "/columns/gallery",
|
||||
"picture": ""
|
||||
},
|
||||
{
|
||||
"default": false,
|
||||
"official": true,
|
||||
"label": "local_feeds",
|
||||
"name": "Local Feeds",
|
||||
"description": "All notes from your follows.",
|
||||
"url": "/columns/newsfeed",
|
||||
"picture": ""
|
||||
},
|
||||
{
|
||||
"default": false,
|
||||
"official": true,
|
||||
"label": "notification",
|
||||
"name": "Notification",
|
||||
"description": "All things around you.",
|
||||
"url": "/columns/notification",
|
||||
"picture": ""
|
||||
},
|
||||
{
|
||||
"default": false,
|
||||
"official": true,
|
||||
"label": "search",
|
||||
"name": "Search",
|
||||
"description": "Find anything.",
|
||||
"url": "/columns/search",
|
||||
"picture": ""
|
||||
},
|
||||
{
|
||||
"default": false,
|
||||
"official": true,
|
||||
"label": "stories",
|
||||
"name": "Stories",
|
||||
"description": "Keep up to date with your follows.",
|
||||
"url": "/columns/stories",
|
||||
"picture": ""
|
||||
},
|
||||
{
|
||||
"default": false,
|
||||
"official": true,
|
||||
"label": "global_feeds",
|
||||
"name": "Global Feeds",
|
||||
"description": "Discover all global notes.",
|
||||
"url": "/columns/global",
|
||||
"picture": ""
|
||||
},
|
||||
{
|
||||
"default": false,
|
||||
"official": true,
|
||||
"label": "group_feeds",
|
||||
"name": "Group",
|
||||
"description": "Custom feeds for group of people.",
|
||||
"url": "/columns/group",
|
||||
"picture": ""
|
||||
},
|
||||
{
|
||||
"default": false,
|
||||
"official": true,
|
||||
"label": "trending",
|
||||
"name": "Trending",
|
||||
"description": "Discover all trending notes.",
|
||||
"url": "/columns/trending",
|
||||
"picture": ""
|
||||
}
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
[
|
||||
{
|
||||
"label": "lZfXLFgPPR4NNrgjlWDxn",
|
||||
"name": "Local Feeds",
|
||||
"content": "/newsfeed",
|
||||
"cover": "/newsfeed.png",
|
||||
"coverRetina": "/newsfeed@2x.png"
|
||||
},
|
||||
{
|
||||
"label": "GLFm44za8rhJDP04LMr3M",
|
||||
"name": "Global Feeds",
|
||||
"content": "/global",
|
||||
"cover": "/global.png",
|
||||
"coverRetina": "/global@2x.png"
|
||||
},
|
||||
{
|
||||
"label": "fve9fk2fVyFWORPBkjd79",
|
||||
"name": "Group Feeds",
|
||||
"content": "/group",
|
||||
"cover": "/group.png",
|
||||
"coverRetina": "/group@2x.png"
|
||||
},
|
||||
{
|
||||
"label": "rRtguZwIpd5G8Wt54OTb7",
|
||||
"name": "Topic",
|
||||
"content": "/topic",
|
||||
"cover": "/foryou.png",
|
||||
"coverRetina": "/foryou@2x.png"
|
||||
},
|
||||
{
|
||||
"label": "gxtcIbgD8YNPbeI5o92I8",
|
||||
"name": "Trending",
|
||||
"content": "/trending/notes",
|
||||
"cover": "/trending.png",
|
||||
"coverRetina": "/trending@2x.png"
|
||||
}
|
||||
]
|
||||
@@ -1,2 +1,4 @@
|
||||
wss://relay.damus.io,
|
||||
wss://relay.nostr.net,
|
||||
wss://purplepag.es/,
|
||||
wss://directory.yabu.me/,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
[
|
||||
{ "label": "onboarding", "name": "Onboarding", "content": "/onboarding" },
|
||||
{ "label": "lume_newsfeed", "name": "Newsfeed", "content": "/newsfeed" },
|
||||
{ "label": "lume_topic", "name": "Topic", "content": "/topic" }
|
||||
]
|
||||
@@ -4,14 +4,11 @@ use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||
use tauri::{Emitter, EventTarget, Manager, State};
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
use tauri::{Emitter, Manager, State};
|
||||
|
||||
// #[cfg(target_os = "macos")]
|
||||
// use crate::commands::tray::create_tray_panel;
|
||||
use crate::{
|
||||
common::{get_user_settings, init_nip65, parse_event},
|
||||
Nostr, RichEvent, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT,
|
||||
common::{get_user_settings, init_nip65},
|
||||
Nostr, NEWSFEED_NEG_LIMIT, NOTIFICATION_NEG_LIMIT, NOTIFICATION_SUB_ID,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
@@ -165,6 +162,37 @@ pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result<Str
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn reset_password(key: String, password: String) -> Result<(), String> {
|
||||
let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||
let keys = Keys::new(secret_key.clone());
|
||||
let npub = keys.public_key().to_bech32().unwrap();
|
||||
|
||||
let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||
|
||||
let keyring = Entry::new("Lume Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||
let account = Account {
|
||||
password: enc_bech32,
|
||||
nostr_connect: None,
|
||||
};
|
||||
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||
let _ = keyring.set_password(&j);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_private_key(id: String) -> Result<String, String> {
|
||||
let keyring = Entry::new("Lume Secret Storage", &id).map_err(|e| e.to_string())?;
|
||||
let password = keyring.get_password().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(password)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn delete_account(id: String) -> Result<(), String> {
|
||||
@@ -180,9 +208,8 @@ pub async fn login(
|
||||
account: String,
|
||||
password: String,
|
||||
state: State<'_, Nostr>,
|
||||
app: tauri::AppHandle,
|
||||
handle: tauri::AppHandle,
|
||||
) -> Result<String, String> {
|
||||
let handle = app.clone();
|
||||
let client = &state.client;
|
||||
let keyring = Entry::new("Lume Secret Storage", &account).map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -229,55 +256,47 @@ pub async fn login(
|
||||
// Connect to user's relay (NIP-65)
|
||||
init_nip65(client).await;
|
||||
|
||||
// Create tray (macOS)
|
||||
// #[cfg(target_os = "macos")]
|
||||
// create_tray_panel(&public_key.to_bech32().unwrap(), &handle);
|
||||
|
||||
// Get user's contact list
|
||||
if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await {
|
||||
*state.contact_list.lock().unwrap() = contacts
|
||||
let mut contacts_state = state.contact_list.lock().await;
|
||||
*contacts_state = contacts;
|
||||
};
|
||||
|
||||
// Get user's settings
|
||||
if let Ok(settings) = get_user_settings(client).await {
|
||||
*state.settings.lock().unwrap() = settings
|
||||
let mut settings_state = state.settings.lock().await;
|
||||
*settings_state = settings;
|
||||
};
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let window = handle.get_window("main").unwrap();
|
||||
|
||||
let state = window.state::<Nostr>();
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
let contact_list = state.contact_list.lock().unwrap().clone();
|
||||
let contact_list = state.contact_list.lock().await;
|
||||
|
||||
let signer = client.signer().await.unwrap();
|
||||
let public_key = signer.public_key().await.unwrap();
|
||||
|
||||
if !contact_list.is_empty() {
|
||||
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect();
|
||||
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
||||
|
||||
match client
|
||||
.reconcile(
|
||||
Filter::new()
|
||||
.authors(authors)
|
||||
if !contact_list.is_empty() {
|
||||
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
|
||||
let sync = Filter::new()
|
||||
.authors(authors.clone())
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(NEWSFEED_NEG_LIMIT),
|
||||
NegentropyOptions::default(),
|
||||
)
|
||||
.limit(NEWSFEED_NEG_LIMIT);
|
||||
|
||||
if client
|
||||
.reconcile(sync, NegentropyOptions::default())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Ok(_) => {
|
||||
if handle.emit_to(EventTarget::Any, "synced", true).is_err() {
|
||||
println!("Emit event failed.")
|
||||
}
|
||||
}
|
||||
Err(_) => println!("Sync newsfeed failed."),
|
||||
handle.emit("newsfeed_synchronized", ()).unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
match client
|
||||
.reconcile(
|
||||
Filter::new()
|
||||
drop(contact_list);
|
||||
|
||||
let sync = Filter::new()
|
||||
.pubkey(public_key)
|
||||
.kinds(vec![
|
||||
Kind::TextNote,
|
||||
@@ -285,21 +304,18 @@ pub async fn login(
|
||||
Kind::Reaction,
|
||||
Kind::ZapReceipt,
|
||||
])
|
||||
.limit(NOTIFICATION_NEG_LIMIT),
|
||||
NegentropyOptions::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
if handle.emit_to(EventTarget::Any, "synced", true).is_err() {
|
||||
println!("Emit event failed.")
|
||||
}
|
||||
}
|
||||
Err(_) => println!("Sync notification failed."),
|
||||
};
|
||||
.limit(NOTIFICATION_NEG_LIMIT);
|
||||
|
||||
let subscription_id = SubscriptionId::new("notification");
|
||||
let subscription = Filter::new()
|
||||
// Sync notification with negentropy
|
||||
if client
|
||||
.reconcile(sync, NegentropyOptions::default())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
handle.emit("notification_synchronized", ()).unwrap();
|
||||
}
|
||||
|
||||
let notification = Filter::new()
|
||||
.pubkey(public_key)
|
||||
.kinds(vec![
|
||||
Kind::TextNote,
|
||||
@@ -310,146 +326,12 @@ pub async fn login(
|
||||
.since(Timestamp::now());
|
||||
|
||||
// Subscribing for new notification...
|
||||
let _ = client
|
||||
.subscribe_with_id(subscription_id, vec![subscription], None)
|
||||
.await;
|
||||
|
||||
// Handle notifications
|
||||
client
|
||||
.handle_notifications(|notification| async {
|
||||
if let RelayPoolNotification::Message { message, .. } = notification {
|
||||
if let RelayMessage::Event {
|
||||
subscription_id,
|
||||
event,
|
||||
} = message
|
||||
{
|
||||
let id = subscription_id.to_string();
|
||||
|
||||
if id.starts_with("notification") {
|
||||
if app
|
||||
.emit_to(
|
||||
EventTarget::window("panel"),
|
||||
"notification",
|
||||
event.as_json(),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
println!("Emit new notification failed.")
|
||||
}
|
||||
|
||||
let handle = app.app_handle();
|
||||
let author = client.metadata(event.pubkey).await.unwrap();
|
||||
|
||||
match event.kind() {
|
||||
Kind::TextNote => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Mentioned you in a thread.")
|
||||
.title(
|
||||
author
|
||||
.display_name
|
||||
.unwrap_or_else(|| "Lume".to_string()),
|
||||
)
|
||||
.show()
|
||||
{
|
||||
println!("Failed to show notification: {:?}", e);
|
||||
}
|
||||
}
|
||||
Kind::Repost => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Reposted your note.")
|
||||
.title(
|
||||
author
|
||||
.display_name
|
||||
.unwrap_or_else(|| "Lume".to_string()),
|
||||
)
|
||||
.show()
|
||||
{
|
||||
println!("Failed to show notification: {:?}", e);
|
||||
}
|
||||
}
|
||||
Kind::Reaction => {
|
||||
let content = event.content();
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body(content)
|
||||
.title(
|
||||
author
|
||||
.display_name
|
||||
.unwrap_or_else(|| "Lume".to_string()),
|
||||
)
|
||||
.show()
|
||||
{
|
||||
println!("Failed to show notification: {:?}", e);
|
||||
}
|
||||
}
|
||||
Kind::ZapReceipt => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Zapped you.")
|
||||
.title(
|
||||
author
|
||||
.display_name
|
||||
.unwrap_or_else(|| "Lume".to_string()),
|
||||
)
|
||||
.show()
|
||||
{
|
||||
println!("Failed to show notification: {:?}", e);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if id.starts_with("event-") {
|
||||
let raw = event.as_json();
|
||||
let parsed = if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if app
|
||||
.emit_to(
|
||||
EventTarget::window(id),
|
||||
"new_reply",
|
||||
RichEvent { raw, parsed },
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
println!("Emit new notification failed.")
|
||||
}
|
||||
} else if id.starts_with("column-") {
|
||||
let raw = event.as_json();
|
||||
let parsed = if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if app
|
||||
.emit_to(
|
||||
EventTarget::window(id),
|
||||
"new_event",
|
||||
RichEvent { raw, parsed },
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
println!("Emit new notification failed.")
|
||||
}
|
||||
} else {
|
||||
println!("new event: {}", event.as_json())
|
||||
}
|
||||
} else {
|
||||
println!("new message: {}", message.as_json())
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
})
|
||||
if let Err(e) = client
|
||||
.subscribe_with_id(notification_id, vec![notification], None)
|
||||
.await
|
||||
{
|
||||
println!("Error: {}", e)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(public_key)
|
||||
|
||||
@@ -5,7 +5,7 @@ use specta::Type;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use tauri::State;
|
||||
|
||||
use crate::common::{create_event_tags, dedup_event, parse_event, Meta};
|
||||
use crate::common::{create_event_tags, filter_converstation, parse_event, Meta};
|
||||
use crate::{Nostr, FETCH_LIMIT};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Type)]
|
||||
@@ -16,31 +16,21 @@ pub struct RichEvent {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_event_meta(content: &str) -> Result<Meta, ()> {
|
||||
let meta = parse_event(content).await;
|
||||
pub async fn get_event_meta(content: String) -> Result<Meta, ()> {
|
||||
let meta = parse_event(&content).await;
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<RichEvent, String> {
|
||||
pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent, String> {
|
||||
let client = &state.client;
|
||||
|
||||
let event_id = match Nip19::from_bech32(id) {
|
||||
Ok(val) => match val {
|
||||
Nip19::EventId(id) => id,
|
||||
Nip19::Event(event) => event.event_id,
|
||||
_ => return Err("Event ID is not valid.".into()),
|
||||
},
|
||||
Err(_) => match EventId::from_hex(id) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return Err("Event ID is not valid.".into()),
|
||||
},
|
||||
};
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new().id(event_id);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![Filter::new().id(event_id)],
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
@@ -66,30 +56,49 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<RichEvent, S
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_event_from(
|
||||
id: &str,
|
||||
relay_hint: &str,
|
||||
id: String,
|
||||
relay_hint: String,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<RichEvent, String> {
|
||||
let client = &state.client;
|
||||
let settings = state
|
||||
.settings
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.clone();
|
||||
|
||||
let event_id = match Nip19::from_bech32(id) {
|
||||
Ok(val) => match val {
|
||||
Nip19::EventId(id) => id,
|
||||
Nip19::Event(event) => event.event_id,
|
||||
_ => return Err("Event ID is not valid.".into()),
|
||||
},
|
||||
Err(_) => match EventId::from_hex(id) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return Err("Event ID is not valid.".into()),
|
||||
},
|
||||
};
|
||||
let settings = state.settings.lock().await;
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new().id(event_id);
|
||||
|
||||
if !settings.use_relay_hint {
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
let raw = event.as_json();
|
||||
let parsed = if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(RichEvent { raw, parsed })
|
||||
} else {
|
||||
Err("Cannot found this event with current relay list".into())
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
} else {
|
||||
// Add relay hint to relay pool
|
||||
if let Err(e) = client.add_relay(&relay_hint).await {
|
||||
return Err(e.to_string());
|
||||
}
|
||||
|
||||
if let Err(e) = client.connect_relay(&relay_hint).await {
|
||||
return Err(e.to_string());
|
||||
}
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![Filter::new().id(event_id)],
|
||||
@@ -113,49 +122,14 @@ pub async fn get_event_from(
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
} else {
|
||||
// Add relay hint to relay pool
|
||||
if let Err(err) = client.add_relay(relay_hint).await {
|
||||
return Err(err.to_string());
|
||||
}
|
||||
|
||||
if client.connect_relay(relay_hint).await.is_ok() {
|
||||
match client
|
||||
.get_events_from(vec![relay_hint], vec![Filter::new().id(event_id)], None)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
let raw = event.as_json();
|
||||
let parsed = if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(RichEvent { raw, parsed })
|
||||
} else {
|
||||
Err("Cannot found this event with current relay list".into())
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
} else {
|
||||
Err("Relay connection failed.".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_replies(id: &str, state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
|
||||
pub async fn get_replies(id: String, state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
|
||||
let event_id = match EventId::from_hex(id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return Err(err.to_string()),
|
||||
};
|
||||
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new().kinds(vec![Kind::TextNote]).event(event_id);
|
||||
|
||||
match client
|
||||
@@ -166,7 +140,7 @@ pub async fn get_replies(id: &str, state: State<'_, Nostr>) -> Result<Vec<RichEv
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let futures = events.into_iter().map(|ev| async move {
|
||||
let futures = events.iter().map(|ev| async move {
|
||||
let raw = ev.as_json();
|
||||
let parsed = if ev.kind == Kind::TextNote {
|
||||
Some(parse_event(&ev.content).await)
|
||||
@@ -186,48 +160,40 @@ pub async fn get_replies(id: &str, state: State<'_, Nostr>) -> Result<Vec<RichEv
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn listen_event_reply(id: &str, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
pub async fn subscribe_to(id: String, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
|
||||
let mut label = "event-".to_owned();
|
||||
label.push_str(id);
|
||||
let subscription_id = SubscriptionId::new(&id);
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
|
||||
let sub_id = SubscriptionId::new(label);
|
||||
let event_id = match EventId::from_hex(id) {
|
||||
Ok(id) => id,
|
||||
Err(err) => return Err(err.to_string()),
|
||||
};
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote])
|
||||
.event(event_id)
|
||||
.since(Timestamp::now());
|
||||
|
||||
// Subscribe
|
||||
let _ = client.subscribe_with_id(sub_id, vec![filter], None).await;
|
||||
|
||||
Ok(())
|
||||
match client
|
||||
.subscribe_with_id(subscription_id, vec![filter], None)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_events_by(
|
||||
public_key: &str,
|
||||
as_of: Option<&str>,
|
||||
public_key: String,
|
||||
limit: i32,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
let author = PublicKey::parse(&public_key).map_err(|err| err.to_string())?;
|
||||
|
||||
match PublicKey::from_str(public_key) {
|
||||
Ok(author) => {
|
||||
let until = match as_of {
|
||||
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])
|
||||
.kinds(vec![Kind::TextNote])
|
||||
.author(author)
|
||||
.limit(FETCH_LIMIT)
|
||||
.until(until);
|
||||
.limit(limit as usize);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
@@ -237,7 +203,8 @@ pub async fn get_events_by(
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let futures = events.into_iter().map(|ev| async move {
|
||||
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)
|
||||
@@ -254,40 +221,36 @@ pub async fn get_events_by(
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_local_events(
|
||||
pub async fn get_events_from_contacts(
|
||||
until: Option<&str>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
let contact_list = state
|
||||
.contact_list
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.clone();
|
||||
let contact_list = state.contact_list.lock().await;
|
||||
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
|
||||
|
||||
if authors.is_empty() {
|
||||
return Err("Contact List is empty.".into());
|
||||
}
|
||||
|
||||
let as_of = match until {
|
||||
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
|
||||
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(64)
|
||||
.limit(FETCH_LIMIT)
|
||||
.until(as_of)
|
||||
.authors(authors);
|
||||
|
||||
match client.database().query(vec![filter], Order::Desc).await {
|
||||
Ok(events) => {
|
||||
let dedup = dedup_event(&events);
|
||||
let futures = dedup.into_iter().map(|ev| async move {
|
||||
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)
|
||||
@@ -305,31 +268,6 @@ pub async fn get_local_events(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn listen_local_event(label: &str, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
|
||||
let contact_list = state
|
||||
.contact_list
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.clone();
|
||||
|
||||
let authors: Vec<PublicKey> = contact_list.into_iter().map(|f| f.public_key).collect();
|
||||
let sub_id = SubscriptionId::new(label);
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.authors(authors)
|
||||
.since(Timestamp::now());
|
||||
|
||||
// Subscribe
|
||||
let _ = client.subscribe_with_id(sub_id, vec![filter], None).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_group_events(
|
||||
@@ -345,7 +283,7 @@ pub async fn get_group_events(
|
||||
};
|
||||
|
||||
let authors: Vec<PublicKey> = public_keys
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|p| {
|
||||
if p.starts_with("npub1") {
|
||||
PublicKey::from_bech32(p).map_err(|err| err.to_string())
|
||||
@@ -369,9 +307,8 @@ pub async fn get_group_events(
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let dedup = dedup_event(&events);
|
||||
|
||||
let futures = dedup.into_iter().map(|ev| async move {
|
||||
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)
|
||||
@@ -415,8 +352,8 @@ pub async fn get_global_events(
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let dedup = dedup_event(&events);
|
||||
let futures = dedup.into_iter().map(|ev| async move {
|
||||
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)
|
||||
@@ -460,8 +397,8 @@ pub async fn get_hashtag_events(
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let dedup = dedup_event(&events);
|
||||
let futures = dedup.into_iter().map(|ev| async move {
|
||||
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)
|
||||
@@ -540,17 +477,14 @@ pub async fn reply(
|
||||
// Create tags from content
|
||||
let mut tags = create_event_tags(&content);
|
||||
|
||||
let reply_id = match EventId::from_hex(to) {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err("Event is not valid.".into()),
|
||||
};
|
||||
let reply_id = EventId::parse(&to).map_err(|err| err.to_string())?;
|
||||
|
||||
match database
|
||||
.query(vec![Filter::new().id(reply_id)], Order::Desc)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.into_iter().next() {
|
||||
if let Some(event) = events.first() {
|
||||
let relay_hint = if let Some(relays) = database
|
||||
.event_seen_on_relays(event.id)
|
||||
.await
|
||||
@@ -585,7 +519,7 @@ pub async fn reply(
|
||||
.query(vec![Filter::new().id(root_id)], Order::Desc)
|
||||
.await
|
||||
{
|
||||
if let Some(event) = events.into_iter().next() {
|
||||
if let Some(event) = events.first() {
|
||||
let relay_hint = if let Some(relays) = database
|
||||
.event_seen_on_relays(event.id)
|
||||
.await
|
||||
@@ -627,13 +561,21 @@ pub async fn repost(raw: &str, state: State<'_, Nostr>) -> Result<String, String
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn event_to_bech32(id: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
pub async fn delete(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
|
||||
let event_id = match EventId::from_hex(id) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return Err("ID is not valid.".into()),
|
||||
};
|
||||
match client.delete_event(event_id).await {
|
||||
Ok(event_id) => Ok(event_id.to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn event_to_bech32(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
|
||||
let seens = client
|
||||
.database()
|
||||
@@ -662,11 +604,7 @@ pub async fn event_to_bech32(id: &str, state: State<'_, Nostr>) -> Result<String
|
||||
#[specta::specta]
|
||||
pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
|
||||
let public_key = match PublicKey::from_str(user) {
|
||||
Ok(pk) => pk,
|
||||
Err(_) => return Err("Public Key is not valid.".into()),
|
||||
};
|
||||
let public_key = PublicKey::parse(user).map_err(|err| err.to_string())?;
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
@@ -704,12 +642,48 @@ pub async fn user_to_bech32(user: &str, state: State<'_, Nostr>) -> Result<Strin
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn unlisten(id: &str, state: State<'_, Nostr>) -> Result<(), ()> {
|
||||
pub async fn search(
|
||||
query: String,
|
||||
until: Option<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
let sub_id = SubscriptionId::new(id);
|
||||
|
||||
// Remove subscription
|
||||
client.unsubscribe(sub_id).await;
|
||||
let timestamp = match until {
|
||||
Some(str) => Timestamp::from_str(&str).map_err(|err| err.to_string())?,
|
||||
None => Timestamp::now(),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Metadata])
|
||||
.search(query)
|
||||
.until(timestamp)
|
||||
.limit(FETCH_LIMIT);
|
||||
|
||||
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(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
use keyring::Entry;
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use tauri::State;
|
||||
use tauri_specta::Event;
|
||||
|
||||
use crate::{Nostr, Settings};
|
||||
use crate::{NewSettings, Nostr, Settings};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||
pub struct Profile {
|
||||
name: String,
|
||||
display_name: String,
|
||||
about: Option<String>,
|
||||
picture: String,
|
||||
banner: Option<String>,
|
||||
nip05: Option<String>,
|
||||
lud16: Option<String>,
|
||||
website: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let public_key: PublicKey = match id {
|
||||
Some(user_id) => match PublicKey::from_str(&user_id) {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err("Public Key is not valid".into()),
|
||||
},
|
||||
Some(user_id) => PublicKey::parse(&user_id).map_err(|e| e.to_string())?,
|
||||
None => client.signer().await.unwrap().public_key().await.unwrap(),
|
||||
};
|
||||
|
||||
@@ -22,14 +34,14 @@ pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<
|
||||
.kind(Kind::Metadata)
|
||||
.limit(1);
|
||||
|
||||
let query = client
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(3))),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(events) = query {
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
if let Ok(metadata) = Metadata::from_json(&event.content) {
|
||||
Ok(metadata.as_json())
|
||||
@@ -39,26 +51,29 @@ pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<
|
||||
} else {
|
||||
Ok(Metadata::new().as_json())
|
||||
}
|
||||
} else {
|
||||
Err("Get metadata failed".into())
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_contact_list(
|
||||
public_keys: Vec<&str>,
|
||||
public_keys: Vec<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let contact_list: Vec<Contact> = public_keys
|
||||
.into_iter()
|
||||
.filter_map(|p| match PublicKey::from_hex(p) {
|
||||
.filter_map(|p| match PublicKey::parse(p) {
|
||||
Ok(pk) => Some(Contact::new(pk, None, Some(""))),
|
||||
Err(_) => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Update local state
|
||||
state.contact_list.lock().await.clone_from(&contact_list);
|
||||
|
||||
match client.set_contact_list(contact_list).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(err) => Err(err.to_string()),
|
||||
@@ -89,77 +104,69 @@ pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, St
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn create_profile(
|
||||
name: &str,
|
||||
display_name: &str,
|
||||
about: &str,
|
||||
picture: &str,
|
||||
banner: &str,
|
||||
nip05: &str,
|
||||
lud16: &str,
|
||||
website: &str,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<String, String> {
|
||||
pub async fn set_profile(profile: Profile, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let mut metadata = Metadata::new()
|
||||
.name(name)
|
||||
.display_name(display_name)
|
||||
.about(about)
|
||||
.nip05(nip05)
|
||||
.lud16(lud16);
|
||||
.name(profile.name)
|
||||
.display_name(profile.display_name)
|
||||
.about(profile.about.unwrap_or_default())
|
||||
.nip05(profile.nip05.unwrap_or_default())
|
||||
.lud16(profile.lud16.unwrap_or_default());
|
||||
|
||||
if let Ok(url) = Url::parse(picture) {
|
||||
if let Ok(url) = Url::parse(&profile.picture) {
|
||||
metadata = metadata.picture(url)
|
||||
}
|
||||
|
||||
if let Ok(url) = Url::parse(banner) {
|
||||
if let Some(b) = profile.banner {
|
||||
if let Ok(url) = Url::parse(&b) {
|
||||
metadata = metadata.banner(url)
|
||||
}
|
||||
|
||||
if let Ok(url) = Url::parse(website) {
|
||||
metadata = metadata.website(url)
|
||||
}
|
||||
|
||||
if let Ok(event_id) = client.set_metadata(&metadata).await {
|
||||
Ok(event_id.to_string())
|
||||
} else {
|
||||
Err("Create profile failed".into())
|
||||
if let Some(w) = profile.website {
|
||||
if let Ok(url) = Url::parse(&w) {
|
||||
metadata = metadata.website(url)
|
||||
}
|
||||
}
|
||||
|
||||
match client.set_metadata(&metadata).await {
|
||||
Ok(id) => Ok(id.to_string()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn is_contact_list_empty(state: State<'_, Nostr>) -> Result<bool, ()> {
|
||||
let contact_list = state.contact_list.lock().unwrap();
|
||||
Ok(contact_list.is_empty())
|
||||
Ok(state.contact_list.lock().await.is_empty())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn check_contact(hex: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let contact_list = state.contact_list.lock().unwrap();
|
||||
let contact_list = state.contact_list.lock().await;
|
||||
|
||||
match PublicKey::from_str(&hex) {
|
||||
match PublicKey::parse(&hex) {
|
||||
Ok(public_key) => match contact_list.iter().position(|x| x.public_key == public_key) {
|
||||
Some(_) => Ok(true),
|
||||
None => Ok(false),
|
||||
},
|
||||
Err(err) => Err(err.to_string()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn toggle_contact(
|
||||
hex: &str,
|
||||
alias: Option<&str>,
|
||||
id: String,
|
||||
alias: Option<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
|
||||
match client.get_contact_list(None).await {
|
||||
match client.get_contact_list(Some(Duration::from_secs(5))).await {
|
||||
Ok(mut contact_list) => {
|
||||
let public_key = PublicKey::from_str(hex).unwrap();
|
||||
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
match contact_list.iter().position(|x| x.public_key == public_key) {
|
||||
Some(index) => {
|
||||
@@ -175,7 +182,7 @@ pub async fn toggle_contact(
|
||||
}
|
||||
|
||||
// Update local state
|
||||
state.contact_list.lock().unwrap().clone_from(&contact_list);
|
||||
state.contact_list.lock().await.clone_from(&contact_list);
|
||||
|
||||
// Publish
|
||||
match client.set_contact_list(contact_list).await {
|
||||
@@ -189,9 +196,9 @@ pub async fn toggle_contact(
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_nstore(
|
||||
key: &str,
|
||||
content: &str,
|
||||
pub async fn set_lume_store(
|
||||
key: String,
|
||||
content: String,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
@@ -202,7 +209,6 @@ pub async fn set_nstore(
|
||||
.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]);
|
||||
|
||||
@@ -214,7 +220,7 @@ pub async fn set_nstore(
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
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())?;
|
||||
@@ -226,16 +232,12 @@ pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result<String, St
|
||||
.limit(1);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.get_events_of(vec![filter], EventSource::Database)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
let content = event.content();
|
||||
match signer.nip44_decrypt(public_key, content).await {
|
||||
match signer.nip44_decrypt(public_key, event.content()).await {
|
||||
Ok(decrypted) => Ok(decrypted),
|
||||
Err(_) => Err(event.content.to_string()),
|
||||
}
|
||||
@@ -316,7 +318,7 @@ pub async fn zap_profile(
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let public_key: PublicKey = PublicKey::from_str(id).map_err(|e| e.to_string())?;
|
||||
let public_key: PublicKey = PublicKey::parse(id).map_err(|e| e.to_string())?;
|
||||
|
||||
let details = ZapDetails::new(ZapType::Private).message(message);
|
||||
let num = amount.parse::<u64>().map_err(|e| e.to_string())?;
|
||||
@@ -361,7 +363,7 @@ pub async fn zap_event(
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
pub async fn copy_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
|
||||
match PublicKey::from_bech32(npub) {
|
||||
@@ -408,7 +410,7 @@ pub async fn get_following(
|
||||
public_key: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::from_str(public_key).map_err(|e| e.to_string())?;
|
||||
let public_key = PublicKey::parse(public_key).map_err(|e| e.to_string())?;
|
||||
|
||||
let filter = Filter::new().kind(Kind::ContactList).author(public_key);
|
||||
let events = match client
|
||||
@@ -444,7 +446,7 @@ pub async fn get_followers(
|
||||
public_key: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::from_str(public_key).map_err(|e| e.to_string())?;
|
||||
let public_key = PublicKey::parse(public_key).map_err(|e| e.to_string())?;
|
||||
|
||||
let filter = Filter::new().kind(Kind::ContactList).custom_tag(
|
||||
SingleLetterTag::lowercase(Alphabet::P),
|
||||
@@ -505,28 +507,51 @@ pub async fn get_notifications(state: State<'_, Nostr>) -> Result<Vec<String>, S
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_settings(state: State<'_, Nostr>) -> Result<Settings, ()> {
|
||||
let settings = state.settings.lock().unwrap().clone();
|
||||
Ok(settings)
|
||||
Ok(state.settings.lock().await.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_new_settings(settings: &str, state: State<'_, Nostr>) -> Result<(), ()> {
|
||||
let parsed: Settings =
|
||||
serde_json::from_str(settings).expect("Could not parse settings payload");
|
||||
*state.settings.lock().unwrap() = parsed;
|
||||
pub async fn set_settings(
|
||||
settings: &str,
|
||||
state: State<'_, Nostr>,
|
||||
handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let ident = "lume_v4:settings";
|
||||
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, 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 {
|
||||
Ok(_) => {
|
||||
let parsed: Settings = serde_json::from_str(settings).map_err(|e| e.to_string())?;
|
||||
|
||||
// Update state
|
||||
state.settings.lock().await.clone_from(&parsed);
|
||||
|
||||
// Emit new changes to frontend
|
||||
NewSettings(parsed).emit(&handle).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, String> {
|
||||
match PublicKey::from_str(key) {
|
||||
Ok(public_key) => {
|
||||
let status = nip05::verify(&public_key, nip05, None).await;
|
||||
Ok(status.is_ok())
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn verify_nip05(id: String, nip05: &str) -> Result<bool, String> {
|
||||
match PublicKey::from_str(&id) {
|
||||
Ok(public_key) => match nip05::verify(&public_key, nip05, None).await {
|
||||
Ok(status) => Ok(status),
|
||||
Err(e) => Err(e.to_string()),
|
||||
},
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,4 @@ pub mod account;
|
||||
pub mod event;
|
||||
pub mod metadata;
|
||||
pub mod relay;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod tray;
|
||||
pub mod window;
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use tauri::window::{Effect, EffectsBuilder};
|
||||
use tauri::{
|
||||
tray::{MouseButtonState, TrayIconEvent},
|
||||
WebviewWindowBuilder,
|
||||
};
|
||||
use tauri::{AppHandle, Manager, WebviewUrl};
|
||||
use tauri_nspanel::ManagerExt;
|
||||
|
||||
use crate::macos::{
|
||||
position_menubar_panel, set_corner_radius, setup_menubar_panel_listeners,
|
||||
swizzle_to_menubar_panel,
|
||||
};
|
||||
|
||||
pub fn create_tray_panel(account: &str, app: &AppHandle) {
|
||||
let tray = app.tray_by_id("main").unwrap();
|
||||
|
||||
tray.on_tray_icon_event(|tray, event| {
|
||||
if let TrayIconEvent::Click { button_state, .. } = event {
|
||||
if button_state == MouseButtonState::Up {
|
||||
let app = tray.app_handle();
|
||||
let panel = app.get_webview_panel("panel").unwrap();
|
||||
|
||||
match panel.is_visible() {
|
||||
true => panel.order_out(None),
|
||||
false => {
|
||||
position_menubar_panel(app, 0.0);
|
||||
panel.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(window) = app.get_webview_window("panel") {
|
||||
let _ = window.destroy();
|
||||
};
|
||||
|
||||
let url = format!("/{}/panel", account);
|
||||
|
||||
let window = WebviewWindowBuilder::new(app, "panel", WebviewUrl::App(PathBuf::from(url)))
|
||||
.title("Panel")
|
||||
.inner_size(350.0, 500.0)
|
||||
.fullscreen(false)
|
||||
.resizable(false)
|
||||
.visible(false)
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let _ = window.set_effects(
|
||||
EffectsBuilder::new()
|
||||
.effect(Effect::Popover)
|
||||
.state(tauri::window::EffectState::FollowsWindowActiveState)
|
||||
.build(),
|
||||
);
|
||||
|
||||
set_corner_radius(&window, 13.0);
|
||||
|
||||
// Convert window to panel
|
||||
swizzle_to_menubar_panel(app);
|
||||
setup_menubar_panel_listeners(app);
|
||||
}
|
||||
@@ -8,9 +8,8 @@ use tauri::utils::config::WindowEffectsConfig;
|
||||
use tauri::window::Effect;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
use tauri::WebviewWindowBuilder;
|
||||
use tauri::{LogicalPosition, LogicalSize, Manager, WebviewUrl};
|
||||
#[cfg(target_os = "windows")]
|
||||
use tauri::{WebviewBuilder, WebviewWindowBuilder};
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
|
||||
#[derive(Serialize, Deserialize, Type)]
|
||||
@@ -45,7 +44,7 @@ pub fn create_column(column: Column, app_handle: tauri::AppHandle) -> Result<Str
|
||||
let path = PathBuf::from(column.url);
|
||||
let webview_url = WebviewUrl::App(path);
|
||||
|
||||
let builder = tauri::webview::WebviewBuilder::new(column.label, webview_url)
|
||||
let builder = WebviewBuilder::new(column.label, webview_url)
|
||||
.incognito(true)
|
||||
.transparent(true);
|
||||
|
||||
@@ -68,14 +67,8 @@ pub fn create_column(column: Column, app_handle: tauri::AppHandle) -> Result<Str
|
||||
#[specta::specta]
|
||||
pub fn close_column(label: String, app_handle: tauri::AppHandle) -> Result<bool, String> {
|
||||
match app_handle.get_webview(&label) {
|
||||
Some(webview) => {
|
||||
if webview.close().is_ok() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
None => Err("Column not found.".into()),
|
||||
Some(webview) => Ok(webview.close().is_ok()),
|
||||
None => Err("Not found.".into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,16 +79,10 @@ pub fn reposition_column(
|
||||
x: f32,
|
||||
y: f32,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<bool, String> {
|
||||
match app_handle.get_webview(&label) {
|
||||
Some(webview) => {
|
||||
if webview.set_position(LogicalPosition::new(x, y)).is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Reposition column failed".into())
|
||||
}
|
||||
}
|
||||
None => Err("Webview not found".into()),
|
||||
Some(webview) => Ok(webview.set_position(LogicalPosition::new(x, y)).is_ok()),
|
||||
None => Err("Not found".into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,36 +93,25 @@ pub fn resize_column(
|
||||
width: f32,
|
||||
height: f32,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<bool, String> {
|
||||
match app_handle.get_webview(&label) {
|
||||
Some(webview) => {
|
||||
if webview.set_size(LogicalSize::new(width, height)).is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Resize column failed".into())
|
||||
}
|
||||
}
|
||||
None => Err("Webview not found".into()),
|
||||
Some(webview) => Ok(webview.set_size(LogicalSize::new(width, height)).is_ok()),
|
||||
None => Err("Not found".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<bool, String> {
|
||||
match app_handle.get_webview(&label) {
|
||||
Some(webview) => {
|
||||
if webview.eval("window.location.reload()").is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Reload column failed".into())
|
||||
}
|
||||
}
|
||||
None => Err("Webview not found".into()),
|
||||
Some(webview) => Ok(webview.eval("window.location.reload()").is_ok()),
|
||||
None => Err("Not found".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app_handle.get_window(&window.label) {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
@@ -145,7 +121,6 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
let _ = window.set_focus();
|
||||
};
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
let window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
&window.label,
|
||||
@@ -168,7 +143,25 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Restore native border
|
||||
window.add_border(None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app_handle.get_window(&window.label) {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
let _ = window.set_focus();
|
||||
} else {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
};
|
||||
} else {
|
||||
let window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
&window.label,
|
||||
@@ -180,6 +173,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.minimizable(window.minimizable)
|
||||
.maximizable(window.maximizable)
|
||||
.transparent(true)
|
||||
.decorations(false)
|
||||
.effects(WindowEffectsConfig {
|
||||
state: None,
|
||||
effects: vec![Effect::Mica],
|
||||
@@ -190,12 +184,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.unwrap();
|
||||
|
||||
// Set decoration
|
||||
#[cfg(target_os = "windows")]
|
||||
window.create_overlay_titlebar().unwrap();
|
||||
|
||||
// Restore native border
|
||||
#[cfg(target_os = "macos")]
|
||||
window.add_border(None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -203,7 +192,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn open_main_window(app: tauri::AppHandle) {
|
||||
pub fn reopen_lume(app: tauri::AppHandle) {
|
||||
if let Some(window) = app.get_window("main") {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
let _ = window.set_focus();
|
||||
@@ -225,11 +214,25 @@ pub fn open_main_window(app: tauri::AppHandle) {
|
||||
// Restore native border
|
||||
#[cfg(target_os = "macos")]
|
||||
window.add_border(None);
|
||||
|
||||
// Set a custom inset to the traffic lights
|
||||
#[cfg(target_os = "macos")]
|
||||
window.set_traffic_lights_inset(7.0, 13.0).unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let win = window.clone();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::ThemeChanged(_) = event {
|
||||
win.set_traffic_lights_inset(7.0, 13.0).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn force_quit() {
|
||||
pub fn quit() {
|
||||
std::process::exit(0)
|
||||
}
|
||||
|
||||
@@ -46,106 +46,27 @@ const NOSTR_MENTIONS: [&str; 10] = [
|
||||
const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
||||
const VIDEOS: [&str; 5] = ["mp4", "mov", "avi", "webm", "mkv"];
|
||||
|
||||
pub async fn init_nip65(client: &Client) {
|
||||
let signer = match client.signer().await {
|
||||
Ok(signer) => signer,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get signer: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let public_key = match signer.public_key().await {
|
||||
Ok(public_key) => public_key,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get public key: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::RelayList)
|
||||
.limit(1);
|
||||
|
||||
if let Ok(events) = client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some(event) = events.first() {
|
||||
let relay_list = nip65::extract_relay_list(event);
|
||||
for (url, metadata) in relay_list {
|
||||
let opts = match metadata {
|
||||
Some(RelayMetadata::Read) => RelayOptions::new().read(true).write(false),
|
||||
Some(_) => RelayOptions::new().write(true).read(false),
|
||||
None => RelayOptions::default(),
|
||||
};
|
||||
if let Err(e) = client.add_relay_with_opts(&url.to_string(), opts).await {
|
||||
eprintln!("Failed to add relay {}: {:?}", url, e);
|
||||
}
|
||||
if let Err(e) = client.connect_relay(url.to_string()).await {
|
||||
eprintln!("Failed to connect to relay {}: {:?}", url, e);
|
||||
} else {
|
||||
println!("Connecting to relay: {} - {:?}", url, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Failed to get events for RelayList.");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_user_settings(client: &Client) -> Result<Settings, String> {
|
||||
let ident = "lume:settings";
|
||||
let signer = client
|
||||
.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()
|
||||
.author(public_key)
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ident)
|
||||
.limit(1);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
let content = event.content();
|
||||
match signer.nip44_decrypt(public_key, content).await {
|
||||
Ok(decrypted) => match serde_json::from_str(&decrypted) {
|
||||
Ok(parsed) => Ok(parsed),
|
||||
Err(_) => Err("Could not parse settings payload".into()),
|
||||
},
|
||||
Err(e) => Err(format!("Failed to decrypt settings content: {:?}", e)),
|
||||
}
|
||||
} else {
|
||||
Err("Settings not found.".into())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(format!(
|
||||
"Failed to get events for ApplicationSpecificData: {:?}",
|
||||
e
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_event(events: &[Event]) -> Option<&Event> {
|
||||
events.iter().next()
|
||||
}
|
||||
|
||||
pub fn filter_converstation(events: Vec<Event>) -> Vec<Event> {
|
||||
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
|
||||
@@ -331,6 +252,102 @@ pub fn create_event_tags(content: &str) -> Vec<Tag> {
|
||||
tags
|
||||
}
|
||||
|
||||
pub async fn init_nip65(client: &Client) {
|
||||
let signer = match client.signer().await {
|
||||
Ok(signer) => signer,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get signer: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let public_key = match signer.public_key().await {
|
||||
Ok(public_key) => public_key,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get public key: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::RelayList)
|
||||
.limit(1);
|
||||
|
||||
if let Ok(events) = client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some(event) = events.first() {
|
||||
let relay_list = nip65::extract_relay_list(event);
|
||||
for (url, metadata) in relay_list {
|
||||
let opts = match metadata {
|
||||
Some(RelayMetadata::Read) => RelayOptions::new().read(true).write(false),
|
||||
Some(_) => RelayOptions::new().write(true).read(false),
|
||||
None => RelayOptions::default(),
|
||||
};
|
||||
if let Err(e) = client.add_relay_with_opts(&url.to_string(), opts).await {
|
||||
eprintln!("Failed to add relay {}: {:?}", url, e);
|
||||
}
|
||||
if let Err(e) = client.connect_relay(url.to_string()).await {
|
||||
eprintln!("Failed to connect to relay {}: {:?}", url, e);
|
||||
} else {
|
||||
println!("Connecting to relay: {} - {:?}", url, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Failed to get events for RelayList.");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_user_settings(client: &Client) -> Result<Settings, String> {
|
||||
let ident = "lume_v4:settings";
|
||||
let signer = client
|
||||
.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()
|
||||
.author(public_key)
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ident)
|
||||
.limit(1);
|
||||
|
||||
match client
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
EventSource::both(Some(Duration::from_secs(5))),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
let content = event.content();
|
||||
match signer.nip44_decrypt(public_key, content).await {
|
||||
Ok(decrypted) => match serde_json::from_str(&decrypted) {
|
||||
Ok(parsed) => Ok(parsed),
|
||||
Err(_) => Err("Could not parse settings payload".into()),
|
||||
},
|
||||
Err(e) => Err(format!("Failed to decrypt settings content: {:?}", e)),
|
||||
}
|
||||
} else {
|
||||
Err("Settings not found.".into())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(format!(
|
||||
"Failed to get events for ApplicationSpecificData: {:?}",
|
||||
e
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
use std::ffi::CString;
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager, WebviewWindow};
|
||||
use tauri_nspanel::{
|
||||
block::ConcreteBlock,
|
||||
cocoa::{
|
||||
appkit::{NSMainMenuWindowLevel, NSView, NSWindow, NSWindowCollectionBehavior},
|
||||
base::{id, nil},
|
||||
foundation::{NSPoint, NSRect},
|
||||
},
|
||||
objc::{class, msg_send, runtime::NO, sel, sel_impl},
|
||||
panel_delegate, ManagerExt, WebviewWindowExt,
|
||||
};
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7;
|
||||
|
||||
pub fn swizzle_to_menubar_panel(app_handle: &tauri::AppHandle) {
|
||||
let panel_delegate = panel_delegate!(SpotlightPanelDelegate {
|
||||
window_did_resign_key
|
||||
});
|
||||
|
||||
let window = app_handle.get_webview_window("panel").unwrap();
|
||||
|
||||
let panel = window.to_panel().unwrap();
|
||||
|
||||
let handle = app_handle.clone();
|
||||
|
||||
panel_delegate.set_listener(Box::new(move |delegate_name: String| {
|
||||
if delegate_name.as_str() == "window_did_resign_key" {
|
||||
let _ = handle.emit("menubar_panel_did_resign_key", ());
|
||||
}
|
||||
}));
|
||||
|
||||
panel.set_level(NSMainMenuWindowLevel + 1);
|
||||
|
||||
panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel);
|
||||
|
||||
panel.set_collection_behaviour(
|
||||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces
|
||||
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary
|
||||
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,
|
||||
);
|
||||
|
||||
panel.set_delegate(panel_delegate);
|
||||
}
|
||||
|
||||
pub fn setup_menubar_panel_listeners(app_handle: &AppHandle) {
|
||||
fn hide_menubar_panel(app_handle: &tauri::AppHandle) {
|
||||
if check_menubar_frontmost() {
|
||||
return;
|
||||
}
|
||||
|
||||
let panel = app_handle.get_webview_panel("panel").unwrap();
|
||||
|
||||
panel.order_out(None);
|
||||
}
|
||||
|
||||
let handle = app_handle.clone();
|
||||
|
||||
app_handle.listen_any("menubar_panel_did_resign_key", move |_| {
|
||||
hide_menubar_panel(&handle);
|
||||
});
|
||||
|
||||
let handle = app_handle.clone();
|
||||
|
||||
let callback = Box::new(move || {
|
||||
hide_menubar_panel(&handle);
|
||||
});
|
||||
|
||||
register_workspace_listener(
|
||||
"NSWorkspaceDidActivateApplicationNotification".into(),
|
||||
callback.clone(),
|
||||
);
|
||||
|
||||
register_workspace_listener(
|
||||
"NSWorkspaceActiveSpaceDidChangeNotification".into(),
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_corner_radius(window: &WebviewWindow, radius: f64) {
|
||||
let win: id = window.ns_window().unwrap() as _;
|
||||
|
||||
unsafe {
|
||||
let view: id = win.contentView();
|
||||
|
||||
view.wantsLayer();
|
||||
|
||||
let layer: id = view.layer();
|
||||
|
||||
let _: () = msg_send![layer, setCornerRadius: radius];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position_menubar_panel(app_handle: &tauri::AppHandle, padding_top: f64) {
|
||||
let window = app_handle.get_webview_window("panel").unwrap();
|
||||
|
||||
let monitor = monitor::get_monitor_with_cursor().unwrap();
|
||||
|
||||
let scale_factor = monitor.scale_factor();
|
||||
|
||||
let visible_area = monitor.visible_area();
|
||||
|
||||
let monitor_pos = visible_area.position().to_logical::<f64>(scale_factor);
|
||||
|
||||
let monitor_size = visible_area.size().to_logical::<f64>(scale_factor);
|
||||
|
||||
let mouse_location: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] };
|
||||
|
||||
let handle: id = window.ns_window().unwrap() as _;
|
||||
|
||||
let mut win_frame: NSRect = unsafe { msg_send![handle, frame] };
|
||||
|
||||
win_frame.origin.y = (monitor_pos.y + monitor_size.height) - win_frame.size.height;
|
||||
|
||||
win_frame.origin.y -= padding_top;
|
||||
|
||||
win_frame.origin.x = {
|
||||
let top_right = mouse_location.x + (win_frame.size.width / 2.0);
|
||||
|
||||
let is_offscreen = top_right > monitor_pos.x + monitor_size.width;
|
||||
|
||||
if !is_offscreen {
|
||||
mouse_location.x - (win_frame.size.width / 2.0)
|
||||
} else {
|
||||
let diff = top_right - (monitor_pos.x + monitor_size.width);
|
||||
|
||||
mouse_location.x - (win_frame.size.width / 2.0) - diff
|
||||
}
|
||||
};
|
||||
|
||||
let _: () = unsafe { msg_send![handle, setFrame: win_frame display: NO] };
|
||||
}
|
||||
|
||||
fn register_workspace_listener(name: String, callback: Box<dyn Fn()>) {
|
||||
let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] };
|
||||
let notification_center: id = unsafe { msg_send![workspace, notificationCenter] };
|
||||
|
||||
let block = ConcreteBlock::new(move |_notif: id| {
|
||||
callback();
|
||||
});
|
||||
|
||||
let block = block.copy();
|
||||
|
||||
let name: id =
|
||||
unsafe { msg_send![class!(NSString), stringWithCString: CString::new(name).unwrap()] };
|
||||
|
||||
unsafe {
|
||||
let _: () = msg_send![
|
||||
notification_center,
|
||||
addObserverForName: name object: nil queue: nil usingBlock: block
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
fn app_pid() -> i32 {
|
||||
let process_info: id = unsafe { msg_send![class!(NSProcessInfo), processInfo] };
|
||||
let pid: i32 = unsafe { msg_send![process_info, processIdentifier] };
|
||||
|
||||
pid
|
||||
}
|
||||
|
||||
fn get_frontmost_app_pid() -> i32 {
|
||||
let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] };
|
||||
let frontmost_application: id = unsafe { msg_send![workspace, frontmostApplication] };
|
||||
let pid: i32 = unsafe { msg_send![frontmost_application, processIdentifier] };
|
||||
|
||||
pid
|
||||
}
|
||||
|
||||
pub fn check_menubar_frontmost() -> bool {
|
||||
get_frontmost_app_pid() == app_pid()
|
||||
}
|
||||
@@ -3,12 +3,10 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
extern crate cocoa;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use border::WebviewWindowExt as BorderWebviewWindowExt;
|
||||
use commands::{account::*, event::*, metadata::*, relay::*, window::*};
|
||||
use common::parse_event;
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
@@ -17,21 +15,18 @@ use std::{
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
str::FromStr,
|
||||
sync::Mutex,
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::{path::BaseDirectory, Manager};
|
||||
use tauri::{path::BaseDirectory, Emitter, EventTarget, Manager};
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
use tauri_specta::{collect_commands, Builder};
|
||||
use tauri_plugin_notification::{NotificationExt, PermissionState};
|
||||
use tauri_specta::{collect_commands, collect_events, Builder, Event as TauriEvent};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub mod commands;
|
||||
pub mod common;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Nostr {
|
||||
#[serde(skip_serializing)]
|
||||
client: Client,
|
||||
contact_list: Mutex<Vec<Contact>>,
|
||||
settings: Mutex<Settings>,
|
||||
@@ -47,28 +42,45 @@ pub struct Settings {
|
||||
display_zap_button: bool,
|
||||
display_repost_button: bool,
|
||||
display_media: bool,
|
||||
vibrancy: bool,
|
||||
transparent: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
proxy: None,
|
||||
image_resize_service: Some("https://wsrv.nl/".into()),
|
||||
image_resize_service: Some("https://wsrv.nl".to_string()),
|
||||
use_relay_hint: true,
|
||||
content_warning: true,
|
||||
display_avatar: true,
|
||||
display_zap_button: true,
|
||||
display_repost_button: true,
|
||||
display_media: true,
|
||||
vibrancy: true,
|
||||
transparent: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const FETCH_LIMIT: usize = 20;
|
||||
#[derive(Serialize, Deserialize, Type)]
|
||||
enum SubKind {
|
||||
Subscribe,
|
||||
Unsubscribe,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, TauriEvent)]
|
||||
struct Subscription {
|
||||
label: String,
|
||||
kind: SubKind,
|
||||
event_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Clone, TauriEvent)]
|
||||
struct NewSettings(Settings);
|
||||
|
||||
pub const FETCH_LIMIT: usize = 44;
|
||||
pub const NEWSFEED_NEG_LIMIT: usize = 256;
|
||||
pub const NOTIFICATION_NEG_LIMIT: usize = 64;
|
||||
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
|
||||
|
||||
fn main() {
|
||||
let builder = Builder::<tauri::Wry>::new()
|
||||
@@ -83,54 +95,57 @@ fn main() {
|
||||
create_account,
|
||||
import_account,
|
||||
connect_account,
|
||||
get_private_key,
|
||||
delete_account,
|
||||
reset_password,
|
||||
login,
|
||||
get_profile,
|
||||
set_profile,
|
||||
get_contact_list,
|
||||
set_contact_list,
|
||||
create_profile,
|
||||
is_contact_list_empty,
|
||||
check_contact,
|
||||
toggle_contact,
|
||||
get_nstore,
|
||||
set_nstore,
|
||||
get_lume_store,
|
||||
set_lume_store,
|
||||
set_wallet,
|
||||
load_wallet,
|
||||
remove_wallet,
|
||||
zap_profile,
|
||||
zap_event,
|
||||
friend_to_friend,
|
||||
copy_friend,
|
||||
get_notifications,
|
||||
get_settings,
|
||||
set_new_settings,
|
||||
set_settings,
|
||||
verify_nip05,
|
||||
get_event_meta,
|
||||
get_event,
|
||||
get_event_from,
|
||||
get_replies,
|
||||
listen_event_reply,
|
||||
subscribe_to,
|
||||
get_events_by,
|
||||
get_local_events,
|
||||
listen_local_event,
|
||||
get_events_from_contacts,
|
||||
get_group_events,
|
||||
get_global_events,
|
||||
get_hashtag_events,
|
||||
search,
|
||||
publish,
|
||||
reply,
|
||||
repost,
|
||||
event_to_bech32,
|
||||
user_to_bech32,
|
||||
unlisten,
|
||||
create_column,
|
||||
close_column,
|
||||
reposition_column,
|
||||
resize_column,
|
||||
reload_column,
|
||||
open_window,
|
||||
open_main_window,
|
||||
force_quit
|
||||
]);
|
||||
reopen_lume,
|
||||
quit
|
||||
])
|
||||
.events(collect_events![Subscription, NewSettings]);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
builder
|
||||
.export(Typescript::default(), "../src/commands.gen.ts")
|
||||
.expect("Failed to export typescript bindings");
|
||||
@@ -148,10 +163,9 @@ fn main() {
|
||||
.setup(move |app| {
|
||||
builder.mount_events(app);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
app.handle().plugin(tauri_nspanel::init()).unwrap();
|
||||
|
||||
let handle = app.handle();
|
||||
let handle_clone = handle.clone();
|
||||
let handle_clone_child = handle_clone.clone();
|
||||
let main_window = app.get_webview_window("main").unwrap();
|
||||
|
||||
// Set custom decoration for Windows
|
||||
@@ -164,7 +178,7 @@ fn main() {
|
||||
|
||||
// Set a custom inset to the traffic lights
|
||||
#[cfg(target_os = "macos")]
|
||||
main_window.set_traffic_lights_inset(8.0, 16.0).unwrap();
|
||||
main_window.set_traffic_lights_inset(7.0, 10.0).unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let win = main_window.clone();
|
||||
@@ -172,25 +186,29 @@ fn main() {
|
||||
#[cfg(target_os = "macos")]
|
||||
main_window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::ThemeChanged(_) = event {
|
||||
win.set_traffic_lights_inset(8.0, 16.0).unwrap();
|
||||
win.set_traffic_lights_inset(7.0, 10.0).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
// Create data folder if not exist
|
||||
let home_dir = app.path().home_dir().unwrap();
|
||||
let _ = fs::create_dir_all(home_dir.join("Lume/"));
|
||||
|
||||
let client = tauri::async_runtime::block_on(async move {
|
||||
// Create data folder if not exist
|
||||
let dir = handle
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.expect("App config directory not found.");
|
||||
let _ = fs::create_dir_all(dir.clone());
|
||||
|
||||
// Setup database
|
||||
let database = SQLiteDatabase::open(home_dir.join("Lume/lume.db"))
|
||||
let database = SQLiteDatabase::open(dir.join("nostr.db"))
|
||||
.await
|
||||
.expect("Error.");
|
||||
.expect("Database error.");
|
||||
|
||||
// Config
|
||||
let opts = Options::new()
|
||||
.max_avg_latency(Duration::from_millis(500))
|
||||
.automatic_authentication(true)
|
||||
.connection_timeout(Some(Duration::from_secs(5)))
|
||||
.timeout(Duration::from_secs(30));
|
||||
.timeout(Duration::from_secs(20));
|
||||
|
||||
// Setup nostr client
|
||||
let client = ClientBuilder::default()
|
||||
@@ -211,10 +229,6 @@ fn main() {
|
||||
if let Some((relay, option)) = line.split_once(',') {
|
||||
match RelayMetadata::from_str(option) {
|
||||
Ok(meta) => {
|
||||
println!(
|
||||
"connecting to bootstrap relay...: {} - {}",
|
||||
relay, meta
|
||||
);
|
||||
let opts = if meta == RelayMetadata::Read {
|
||||
RelayOptions::new().read(true).write(false)
|
||||
} else {
|
||||
@@ -223,7 +237,6 @@ fn main() {
|
||||
let _ = client.add_relay_with_opts(relay, opts).await;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("connecting to bootstrap relay...: {}", relay);
|
||||
let _ = client.add_relay(relay).await;
|
||||
}
|
||||
}
|
||||
@@ -244,11 +257,116 @@ fn main() {
|
||||
settings: Mutex::new(Settings::default()),
|
||||
});
|
||||
|
||||
Subscription::listen_any(app, move |event| {
|
||||
let handle = handle_clone_child.to_owned();
|
||||
let payload = event.payload;
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
|
||||
match payload.kind {
|
||||
SubKind::Subscribe => {
|
||||
let subscription_id = SubscriptionId::new(payload.label);
|
||||
|
||||
let filter = if let Some(id) = payload.event_id {
|
||||
let event_id = EventId::from_str(&id).unwrap();
|
||||
|
||||
Filter::new().event(event_id).since(Timestamp::now())
|
||||
} else {
|
||||
let contact_list = state.contact_list.lock().await;
|
||||
let authors: Vec<PublicKey> =
|
||||
contact_list.iter().map(|f| f.public_key).collect();
|
||||
|
||||
Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.authors(authors)
|
||||
.since(Timestamp::now())
|
||||
};
|
||||
|
||||
if let Err(e) = client
|
||||
.subscribe_with_id(subscription_id, vec![filter], None)
|
||||
.await
|
||||
{
|
||||
println!("Subscription error: {}", e)
|
||||
}
|
||||
}
|
||||
SubKind::Unsubscribe => {
|
||||
let subscription_id = SubscriptionId::new(payload.label);
|
||||
client.unsubscribe(subscription_id).await
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle_clone.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
|
||||
let allow_notification = match handle_clone.notification().request_permission() {
|
||||
Ok(_) => {
|
||||
if let Ok(perm) = handle_clone.notification().permission_state() {
|
||||
PermissionState::Granted == perm
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
};
|
||||
|
||||
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
||||
|
||||
client
|
||||
.handle_notifications(|notification| async {
|
||||
if let RelayPoolNotification::Message { message, .. } = notification {
|
||||
if let RelayMessage::Event {
|
||||
subscription_id,
|
||||
event,
|
||||
} = message
|
||||
{
|
||||
// Handle events from notification subscription
|
||||
if subscription_id == notification_id {
|
||||
// Send native notification
|
||||
if allow_notification {
|
||||
let author = client
|
||||
.metadata(event.pubkey)
|
||||
.await
|
||||
.unwrap_or_else(|_| Metadata::new());
|
||||
|
||||
send_notification(&event, author, &handle_clone);
|
||||
}
|
||||
}
|
||||
|
||||
let label = subscription_id.to_string();
|
||||
let raw = event.as_json();
|
||||
let parsed = if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
handle_clone
|
||||
.emit_to(
|
||||
EventTarget::labeled(label),
|
||||
"event",
|
||||
RichEvent { raw, parsed },
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
println!("new message: {}", message.as_json())
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
})
|
||||
.await
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.plugin(tauri_plugin_prevent_default::init())
|
||||
.plugin(prevent_default())
|
||||
.plugin(tauri_plugin_theme::init(ctx.config_mut()))
|
||||
.plugin(tauri_plugin_decorum::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
@@ -262,3 +380,56 @@ fn main() {
|
||||
.run(ctx)
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
|
||||
tauri_plugin_prevent_default::Builder::new()
|
||||
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
||||
tauri_plugin_prevent_default::Builder::new().build()
|
||||
}
|
||||
|
||||
fn send_notification(event: &Event, author: Metadata, handle: &tauri::AppHandle) {
|
||||
match event.kind() {
|
||||
Kind::TextNote => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Mentioned you in a thread.")
|
||||
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
|
||||
.show()
|
||||
{
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
Kind::Repost => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Reposted your note.")
|
||||
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
|
||||
.show()
|
||||
{
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
Kind::ZapReceipt => {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("Zapped you.")
|
||||
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
|
||||
.show()
|
||||
{
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"licenseFile": "../LICENSE",
|
||||
"homepage": "https://lume.nu",
|
||||
"longDescription": "nostr client for desktop",
|
||||
"shortDescription": "nostr client",
|
||||
@@ -61,21 +60,6 @@
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "10.15"
|
||||
},
|
||||
"fileAssociations": [
|
||||
{
|
||||
"name": "bech32",
|
||||
"description": "Nostr BECH32",
|
||||
"ext": [
|
||||
"npub",
|
||||
"nsec",
|
||||
"nprofile",
|
||||
"nevent",
|
||||
"naddr",
|
||||
"nrelay"
|
||||
],
|
||||
"role": "Viewer"
|
||||
}
|
||||
],
|
||||
"createUpdaterArtifacts": true
|
||||
},
|
||||
"plugins": {
|
||||
|
||||
@@ -50,11 +50,11 @@ input::-ms-clear {
|
||||
}
|
||||
|
||||
div[data-tauri-decorum-tb] {
|
||||
@apply h-11 !important;
|
||||
@apply h-10 !important;
|
||||
}
|
||||
|
||||
button.decorum-tb-btn {
|
||||
@apply h-11 !important;
|
||||
@apply h-10 !important;
|
||||
}
|
||||
|
||||
.spinner-leaf {
|
||||
|
||||
23
src/app.tsx
@@ -1,13 +1,31 @@
|
||||
import {
|
||||
type PersistedQuery,
|
||||
experimental_createPersister,
|
||||
} from "@tanstack/query-persist-client-core";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { StrictMode } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { newQueryStorage } from "./commons";
|
||||
import { routeTree } from "./routes.gen"; // auto generated file
|
||||
import type { LumeEvent } from "./system";
|
||||
import "./app.css";
|
||||
import { Store } from "@tauri-apps/plugin-store";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const platform = type();
|
||||
const tauriStore = new Store(".lume.dat");
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
gcTime: 1000 * 30,
|
||||
persister: experimental_createPersister<PersistedQuery>({
|
||||
storage: newQueryStorage(tauriStore),
|
||||
maxAge: 1000 * 60 * 60 * 24, // 24 hours,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
@@ -24,6 +42,9 @@ declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
interface HistoryState {
|
||||
events?: LumeEvent[];
|
||||
}
|
||||
}
|
||||
|
||||
function App() {
|
||||
|
||||
@@ -72,6 +72,14 @@ async connectAccount(uri: string) : Promise<Result<string, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getPrivateKey(id: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_private_key", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async deleteAccount(id: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("delete_account", { id }) };
|
||||
@@ -80,6 +88,14 @@ async deleteAccount(id: string) : Promise<Result<null, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async resetPassword(key: string, password: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("reset_password", { key, password }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async login(account: string, password: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("login", { account, password }) };
|
||||
@@ -96,6 +112,14 @@ async getProfile(id: string | null) : Promise<Result<string, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setProfile(profile: Profile) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_profile", { profile }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getContactList() : Promise<Result<string[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_contact_list") };
|
||||
@@ -112,14 +136,6 @@ async setContactList(publicKeys: string[]) : Promise<Result<boolean, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async createProfile(name: string, displayName: string, about: string, picture: string, banner: string, nip05: string, lud16: string, website: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("create_profile", { name, displayName, about, picture, banner, nip05, lud16, website }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async isContactListEmpty() : Promise<Result<boolean, null>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("is_contact_list_empty") };
|
||||
@@ -136,25 +152,25 @@ async checkContact(hex: string) : Promise<Result<boolean, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async toggleContact(hex: string, alias: string | null) : Promise<Result<string, string>> {
|
||||
async toggleContact(id: string, alias: string | null) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("toggle_contact", { hex, alias }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("toggle_contact", { id, alias }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getNstore(key: string) : Promise<Result<string, string>> {
|
||||
async getLumeStore(key: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_nstore", { key }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_lume_store", { key }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setNstore(key: string, content: string) : Promise<Result<string, string>> {
|
||||
async setLumeStore(key: string, content: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_nstore", { key, content }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_lume_store", { key, content }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
@@ -200,9 +216,9 @@ async zapEvent(id: string, amount: string, message: string) : Promise<Result<boo
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async friendToFriend(npub: string) : Promise<Result<boolean, string>> {
|
||||
async copyFriend(npub: string) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("friend_to_friend", { npub }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("copy_friend", { npub }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
@@ -224,17 +240,17 @@ async getSettings() : Promise<Result<Settings, null>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setNewSettings(settings: string) : Promise<Result<null, null>> {
|
||||
async setSettings(settings: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_new_settings", { settings }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_settings", { settings }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async verifyNip05(key: string, nip05: string) : Promise<Result<boolean, string>> {
|
||||
async verifyNip05(id: string, nip05: string) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("verify_nip05", { key, nip05 }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("verify_nip05", { id, nip05 }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
@@ -272,33 +288,25 @@ async getReplies(id: string) : Promise<Result<RichEvent[], string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async listenEventReply(id: string) : Promise<Result<null, string>> {
|
||||
async subscribeTo(id: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("listen_event_reply", { id }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("subscribe_to", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getEventsBy(publicKey: string, asOf: string | null) : Promise<Result<RichEvent[], string>> {
|
||||
async getEventsBy(publicKey: string, limit: number) : Promise<Result<RichEvent[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_events_by", { publicKey, asOf }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_events_by", { publicKey, limit }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getLocalEvents(until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||
async getEventsFromContacts(until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_local_events", { until }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async listenLocalEvent(label: string) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("listen_local_event", { label }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_events_from_contacts", { until }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
@@ -328,6 +336,14 @@ async getHashtagEvents(hashtags: string[], until: string | null) : Promise<Resul
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async search(query: string, until: string | null) : Promise<Result<RichEvent[], string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("search", { query, until }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async publish(content: string, warning: string | null, difficulty: number | null) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("publish", { content, warning, difficulty }) };
|
||||
@@ -368,14 +384,6 @@ async userToBech32(user: string) : Promise<Result<string, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async unlisten(id: string) : Promise<Result<null, null>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("unlisten", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async createColumn(column: Column) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("create_column", { column }) };
|
||||
@@ -392,7 +400,7 @@ async closeColumn(label: string) : Promise<Result<boolean, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async repositionColumn(label: string, x: number, y: number) : Promise<Result<null, string>> {
|
||||
async repositionColumn(label: string, x: number, y: number) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("reposition_column", { label, x, y }) };
|
||||
} catch (e) {
|
||||
@@ -400,7 +408,7 @@ async repositionColumn(label: string, x: number, y: number) : Promise<Result<nul
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async resizeColumn(label: string, width: number, height: number) : Promise<Result<null, string>> {
|
||||
async resizeColumn(label: string, width: number, height: number) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("resize_column", { label, width, height }) };
|
||||
} catch (e) {
|
||||
@@ -408,7 +416,7 @@ async resizeColumn(label: string, width: number, height: number) : Promise<Resul
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async reloadColumn(label: string) : Promise<Result<null, string>> {
|
||||
async reloadColumn(label: string) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("reload_column", { label }) };
|
||||
} catch (e) {
|
||||
@@ -424,17 +432,24 @@ async openWindow(window: Window) : Promise<Result<null, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async openMainWindow() : Promise<void> {
|
||||
await TAURI_INVOKE("open_main_window");
|
||||
async reopenLume() : Promise<void> {
|
||||
await TAURI_INVOKE("reopen_lume");
|
||||
},
|
||||
async forceQuit() : Promise<void> {
|
||||
await TAURI_INVOKE("force_quit");
|
||||
async quit() : Promise<void> {
|
||||
await TAURI_INVOKE("quit");
|
||||
}
|
||||
}
|
||||
|
||||
/** user-defined events **/
|
||||
|
||||
|
||||
export const events = __makeEvents__<{
|
||||
newSettings: NewSettings,
|
||||
subscription: Subscription
|
||||
}>({
|
||||
newSettings: "new-settings",
|
||||
subscription: "subscription"
|
||||
})
|
||||
|
||||
/** user-defined constants **/
|
||||
|
||||
@@ -444,9 +459,13 @@ async forceQuit() : Promise<void> {
|
||||
|
||||
export type Column = { label: string; url: string; x: number; y: number; width: number; height: number }
|
||||
export type Meta = { content: string; images: string[]; videos: string[]; events: string[]; mentions: string[]; hashtags: string[] }
|
||||
export type NewSettings = Settings
|
||||
export type Profile = { name: string; display_name: string; about: string | null; picture: string; banner: string | null; nip05: string | null; lud16: string | null; website: string | null }
|
||||
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
|
||||
export type RichEvent = { raw: string; parsed: Meta | null }
|
||||
export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean; vibrancy: boolean }
|
||||
export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean; transparent: boolean }
|
||||
export type SubKind = "Subscribe" | "Unsubscribe"
|
||||
export type Subscription = { label: string; kind: SubKind; event_id: string | null }
|
||||
export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean }
|
||||
|
||||
/** tauri-specta globals **/
|
||||
|
||||
108
src/commons.ts
@@ -1,6 +1,12 @@
|
||||
import type { Contact } from "@/types";
|
||||
import type {
|
||||
AsyncStorage,
|
||||
MaybePromise,
|
||||
PersistedQuery,
|
||||
} from "@tanstack/query-persist-client-core";
|
||||
import { Store } from "@tanstack/store";
|
||||
import { ask, message } from "@tauri-apps/plugin-dialog";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import type { Store as TauriStore } from "@tauri-apps/plugin-store";
|
||||
import { check } from "@tauri-apps/plugin-updater";
|
||||
import { BitcoinUnit } from "bitcoin-units";
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
@@ -8,12 +14,12 @@ import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import updateLocale from "dayjs/plugin/updateLocale";
|
||||
import { decode } from "light-bolt11-decoder";
|
||||
import type { ReactNode } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { type BaseEditor, Transforms } from "slate";
|
||||
import { ReactEditor } from "slate-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { AUDIOS, IMAGES, VIDEOS } from "./constants";
|
||||
import type { RichEvent, Settings } from "./commands.gen";
|
||||
import { LumeEvent } from "./system";
|
||||
import type { NostrEvent } from "./types";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(updateLocale);
|
||||
@@ -35,12 +41,6 @@ export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export const Portal = ({ children }: { children?: ReactNode }) => {
|
||||
return typeof document === "object"
|
||||
? ReactDOM.createPortal(children, document.body)
|
||||
: null;
|
||||
};
|
||||
|
||||
export const isImagePath = (path: string) => {
|
||||
for (const suffix of ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"]) {
|
||||
if (path.endsWith(suffix)) return true;
|
||||
@@ -127,24 +127,17 @@ export function formatCreatedAt(time: number, message = false) {
|
||||
return formated;
|
||||
}
|
||||
|
||||
export function displayNsec(key: string, len: number) {
|
||||
if (key.length <= len) return key;
|
||||
export function replyTime(time: number) {
|
||||
const inputTime = dayjs.unix(time);
|
||||
const formated = inputTime.format("MM-DD-YY HH:mm");
|
||||
|
||||
const separator = " ... ";
|
||||
|
||||
const sepLen = separator.length;
|
||||
const charsToShow = len - sepLen;
|
||||
const frontChars = Math.ceil(charsToShow / 2);
|
||||
const backChars = Math.floor(charsToShow / 2);
|
||||
|
||||
return (
|
||||
key.substr(0, frontChars) + separator + key.substr(key.length - backChars)
|
||||
);
|
||||
return formated;
|
||||
}
|
||||
|
||||
export function displayNpub(pubkey: string, len: number) {
|
||||
if (pubkey.length <= len) return pubkey;
|
||||
|
||||
const str = pubkey.replace("nostr:", "");
|
||||
const separator = " ... ";
|
||||
|
||||
const sepLen = separator.length;
|
||||
@@ -153,9 +146,9 @@ export function displayNpub(pubkey: string, len: number) {
|
||||
const backChars = Math.floor(charsToShow / 2);
|
||||
|
||||
return (
|
||||
pubkey.substr(0, frontChars) +
|
||||
str.substring(0, frontChars) +
|
||||
separator +
|
||||
pubkey.substr(pubkey.length - backChars)
|
||||
str.substring(str.length - backChars)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,32 +160,6 @@ export function displayLongHandle(str: string) {
|
||||
return `${handle.substring(0, 16)}...@${service}`;
|
||||
}
|
||||
|
||||
// convert number to K, M, B, T, etc.
|
||||
export const compactNumber = Intl.NumberFormat("en", { notation: "compact" });
|
||||
|
||||
// country name
|
||||
export const regionNames = new Intl.DisplayNames(["en"], { type: "language" });
|
||||
|
||||
// verify link can be preview
|
||||
export function canPreview(text: string) {
|
||||
const url = new URL(text);
|
||||
const ext = url.pathname.split(".").pop();
|
||||
const hostname = url.hostname;
|
||||
|
||||
if (VIDEOS.includes(ext)) return false;
|
||||
if (IMAGES.includes(ext)) return false;
|
||||
if (AUDIOS.includes(ext)) return false;
|
||||
|
||||
if (hostname === "youtube.com") return false;
|
||||
if (hostname === "youtu.be") return false;
|
||||
if (hostname === "x.com") return false;
|
||||
if (hostname === "twitter.com") return false;
|
||||
if (hostname === "facebook.com") return false;
|
||||
if (hostname === "vimeo.com") return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// source: https://github.com/synonymdev/bitkit/blob/master/src/utils/displayValues/index.ts
|
||||
export function getBitcoinDisplayValues(satoshis: number) {
|
||||
let bitcoinFormatted = new BitcoinUnit(satoshis, "satoshis")
|
||||
@@ -273,3 +240,44 @@ export async function checkForAppUpdates(silent: boolean) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function toLumeEvents(richEvents: RichEvent[]) {
|
||||
const events = richEvents.map((item) => {
|
||||
const nostrEvent: NostrEvent = JSON.parse(item.raw);
|
||||
|
||||
if (item.parsed) {
|
||||
nostrEvent.meta = item.parsed;
|
||||
} else {
|
||||
nostrEvent.meta = null;
|
||||
}
|
||||
|
||||
const lumeEvent = new LumeEvent(nostrEvent);
|
||||
|
||||
return lumeEvent;
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
export function newQueryStorage(
|
||||
store: TauriStore,
|
||||
): AsyncStorage<PersistedQuery> {
|
||||
return {
|
||||
getItem: async (key) => await store.get(key),
|
||||
setItem: async (key, value) => await store.set(key, value),
|
||||
removeItem: async (key) =>
|
||||
(await store.delete(key)) as unknown as MaybePromise<void>,
|
||||
};
|
||||
}
|
||||
|
||||
export const appSettings = new Store<Settings>({
|
||||
proxy: null,
|
||||
image_resize_service: "https://wsrv.nl",
|
||||
use_relay_hint: true,
|
||||
content_warning: true,
|
||||
display_avatar: true,
|
||||
display_zap_button: true,
|
||||
display_repost_button: true,
|
||||
display_media: true,
|
||||
transparent: true,
|
||||
});
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { cn } from "@/commons";
|
||||
import { Spinner } from "@/components";
|
||||
import { NostrQuery } from "@/system";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import {
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
export function AvatarUploader({
|
||||
setPicture,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
setPicture: Dispatch<SetStateAction<string>>;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const image = await NostrQuery.upload();
|
||||
setPicture(image);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
await message(String(e), { title: "Lume", kind: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadAvatar()}
|
||||
className={cn("size-4", className)}
|
||||
>
|
||||
{loading ? <Spinner className="size-4" /> : children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { cn } from "@/commons";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function Box({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full">
|
||||
<div className="h-full w-full flex-1 px-2 pb-2">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full w-full overflow-y-auto rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] sm:px-0 dark:bg-white/5 dark:shadow-none",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
import { CheckIcon, HorizontalDotsIcon } from "@/components";
|
||||
import { commands } from "@/commands.gen";
|
||||
import type { LumeColumn } from "@/types";
|
||||
import { Check, DotsThree } from "@phosphor-icons/react";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Spinner } from "./spinner";
|
||||
|
||||
type WindowEvent = {
|
||||
scroll: boolean;
|
||||
resize: boolean;
|
||||
};
|
||||
|
||||
export const Column = memo(function Column({
|
||||
column,
|
||||
account,
|
||||
}: {
|
||||
column: LumeColumn;
|
||||
account: string;
|
||||
}) {
|
||||
export const Column = memo(function Column({ column }: { column: LumeColumn }) {
|
||||
const params = useParams({ strict: false });
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const webviewLabel = `column-${account}_${column.label}`;
|
||||
const webviewLabel = `column-${params.account}_${column.label}`;
|
||||
|
||||
const [isCreated, setIsCreated] = useState(false);
|
||||
|
||||
const repositionWebview = useCallback(async () => {
|
||||
if (!container.current) return;
|
||||
|
||||
const newRect = container.current.getBoundingClientRect();
|
||||
await invoke("reposition_column", {
|
||||
label: webviewLabel,
|
||||
@@ -33,6 +33,8 @@ export const Column = memo(function Column({
|
||||
}, []);
|
||||
|
||||
const resizeWebview = useCallback(async () => {
|
||||
if (!container.current) return;
|
||||
|
||||
const newRect = container.current.getBoundingClientRect();
|
||||
await invoke("resize_column", {
|
||||
label: webviewLabel,
|
||||
@@ -55,10 +57,10 @@ export const Column = memo(function Column({
|
||||
}, [isCreated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!container?.current) return;
|
||||
if (!container.current) return;
|
||||
|
||||
const rect = container.current.getBoundingClientRect();
|
||||
const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`;
|
||||
const url = `${column.url}?account=${params.account}&label=${column.label}&name=${column.name}`;
|
||||
|
||||
const prop = {
|
||||
label: webviewLabel,
|
||||
@@ -81,27 +83,25 @@ export const Column = memo(function Column({
|
||||
console.log("closed: ", webviewLabel);
|
||||
});
|
||||
};
|
||||
}, [account]);
|
||||
}, [params.account]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-[440px] shrink-0 p-2">
|
||||
<div className="flex flex-col w-full h-full rounded-xl bg-black/5 dark:bg-white/10">
|
||||
<Header
|
||||
label={column.label}
|
||||
webview={webviewLabel}
|
||||
name={column.name}
|
||||
/>
|
||||
<div ref={container} className="flex-1 w-full h-full" />
|
||||
<div className="flex flex-col w-full h-full rounded-xl bg-black/5 dark:bg-white/15">
|
||||
<Header label={column.label} name={column.name} />
|
||||
<div ref={container} className="flex-1 w-full h-full">
|
||||
{!isCreated ? (
|
||||
<div className="size-full flex items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function Header({
|
||||
label,
|
||||
webview,
|
||||
name,
|
||||
}: { label: string; webview: string; name: string }) {
|
||||
function Header({ label, name }: { label: string; name: string }) {
|
||||
const [title, setTitle] = useState(name);
|
||||
const [isChanged, setIsChanged] = useState(false);
|
||||
|
||||
@@ -120,11 +120,12 @@ function Header({
|
||||
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const window = getCurrentWindow();
|
||||
const menuItems = await Promise.all([
|
||||
MenuItem.new({
|
||||
text: "Reload",
|
||||
action: async () => {
|
||||
await invoke("reload_column", { label: webview });
|
||||
await commands.reloadColumn(label);
|
||||
},
|
||||
}),
|
||||
MenuItem.new({
|
||||
@@ -135,7 +136,7 @@ function Header({
|
||||
MenuItem.new({
|
||||
text: "Move left",
|
||||
action: async () => {
|
||||
await getCurrentWindow().emit("columns", {
|
||||
await window.emit("columns", {
|
||||
type: "move",
|
||||
label,
|
||||
direction: "left",
|
||||
@@ -145,7 +146,7 @@ function Header({
|
||||
MenuItem.new({
|
||||
text: "Move right",
|
||||
action: async () => {
|
||||
await getCurrentWindow().emit("columns", {
|
||||
await window.emit("columns", {
|
||||
type: "move",
|
||||
label,
|
||||
direction: "right",
|
||||
@@ -156,7 +157,7 @@ function Header({
|
||||
MenuItem.new({
|
||||
text: "Close",
|
||||
action: async () => {
|
||||
await getCurrentWindow().emit("columns", {
|
||||
await window.emit("columns", {
|
||||
type: "remove",
|
||||
label,
|
||||
});
|
||||
@@ -194,7 +195,7 @@ function Header({
|
||||
onClick={() => saveNewTitle()}
|
||||
className="text-teal-500 hover:text-teal-600"
|
||||
>
|
||||
<CheckIcon className="size-4" />
|
||||
<Check className="size-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -204,7 +205,7 @@ function Header({
|
||||
onClick={(e) => showContextMenu(e)}
|
||||
className="inline-flex items-center justify-center rounded-lg size-7 hover:bg-black/10 dark:hover:bg-white/10"
|
||||
>
|
||||
<HorizontalDotsIcon className="size-5" />
|
||||
<DotsThree className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { cn } from "@/commons";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function Container({
|
||||
children,
|
||||
withDrag = false,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
withDrag?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-transparent flex h-screen w-screen flex-col",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{withDrag ? (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="bg-transparent flex h-11 w-full shrink-0"
|
||||
/>
|
||||
) : null}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cn } from "@/commons";
|
||||
import { ThreadIcon } from "@/components";
|
||||
import { Note } from "@/components/note";
|
||||
import type { LumeEvent } from "@/system";
|
||||
import { ChatsTeardrop } from "@phosphor-icons/react";
|
||||
import { memo, useMemo } from "react";
|
||||
|
||||
export const Conversation = memo(function Conversation({
|
||||
@@ -25,7 +25,7 @@ export const Conversation = memo(function Conversation({
|
||||
{thread?.root?.id ? <Note.Child event={thread?.root} isRoot /> : null}
|
||||
<div className="flex items-center gap-2 px-3">
|
||||
<div className="inline-flex items-center gap-1.5 shrink-0 text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||
<ThreadIcon className="size-4" />
|
||||
<ChatsTeardrop className="size-4" />
|
||||
Thread
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-neutral-100 dark:bg-white/5" />
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export function AddMediaIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||
return (
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M15.25 8.75v-4a2 2 0 0 0-2-2h-8.5a2 2 0 0 0-2 2v8.5a2 2 0 0 0 2 2h4M3.1 11.9l1.794-1.176a2 2 0 0 1 2.206.01l1.279.852M6 6.25h.5m8 8.75h.5M6.75 6.25a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0Zm7 6.95v3.6l2.8-1.8-2.8-1.8Zm5.5 8.05h-8.5a2 2 0 0 1-2-2v-8.5a2 2 0 0 1 2-2h8.5a2 2 0 0 1 2 2v8.5a2 2 0 0 1-2 2Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function AddWidgetIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M12.25 21.25h-6.5a1 1 0 01-1-1V3.75a1 1 0 011-1h12.5a1 1 0 011 1v8.5m-1 3v3m0 0v3m0-3h-3m3 0h3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function AdvancedSettingsIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M13.75 7h-10m10 0a3.25 3.25 0 116.5 0 3.25 3.25 0 11-6.5 0zm6.5 10h-8m0 0a3.25 3.25 0 11-6.5 0m6.5 0a3.25 3.25 0 10-6.5 0m0 0h-2"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function AlbyIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="400"
|
||||
height="578"
|
||||
fill="none"
|
||||
viewBox="0 0 400 578"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#000"
|
||||
d="M201.283 577.511c54.122 0 97.998-8.1 97.998-18.092 0-9.992-43.876-18.092-97.998-18.092-54.123 0-97.998 8.1-97.998 18.092 0 9.992 43.875 18.092 97.998 18.092z"
|
||||
opacity="0.1"
|
||||
></path>
|
||||
<path
|
||||
fill="#fff"
|
||||
stroke="#000"
|
||||
strokeWidth="15.077"
|
||||
d="M295.75 471.344c50.627 0 73.67-112.102 73.67-154.608 0-33.13-22.86-53.208-52.913-53.208-29.866 0-54.113 12.843-54.414 28.747-.001 41.971-7.388 179.069 33.657 179.069zM110.837 471.344c-50.627 0-73.67-112.102-73.67-154.608 0-33.13 22.86-53.208 52.913-53.208 29.866 0 54.113 12.843 54.414 28.747.001 41.971 7.388 179.069-33.657 179.069z"
|
||||
></path>
|
||||
<path
|
||||
fill="#FFDF6F"
|
||||
stroke="#000"
|
||||
strokeWidth="15"
|
||||
d="M68.83 303.262v-.002c-.054-.519.052-.82.16-1.016.127-.232.368-.508.773-.738.84-.477 2.014-.563 3.108.076 37.603 22.042 80.976 34.678 128.13 34.678 47.163 0 91.339-12.881 129.184-35.307 1.087-.645 2.26-.565 3.102-.091.407.229.65.504.779.737.109.197.216.499.163 1.019-5.854 58.014-37.322 105.977-79.618 128.054-13.969 7.293-23.576 19.962-32.013 31.089l-.452.597-.002.002c-6.857 9.046-13.063 17.147-20.648 23.116-7.584-5.969-13.791-14.07-20.648-23.116l-.001-.002-.452-.597c-8.437-11.127-18.043-23.796-32.013-31.089-42.135-21.992-73.523-69.677-79.551-127.41z"
|
||||
></path>
|
||||
<path
|
||||
fill="#000"
|
||||
stroke="#000"
|
||||
strokeWidth="15.077"
|
||||
d="M201.786 346.338c73.274 0 132.674-19.8 132.674-44.225s-59.4-44.225-132.674-44.225-132.674 19.8-132.674 44.225 59.4 44.225 132.674 44.225z"
|
||||
></path>
|
||||
<path
|
||||
stroke="#000"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="15.077"
|
||||
d="M95.245 376.491s65.44 22.112 107.546 22.112c42.105 0 107.546-22.112 107.546-22.112"
|
||||
></path>
|
||||
<path
|
||||
fill="#000"
|
||||
d="M77 143c-16.569 0-30-13.431-30-30 0-16.569 13.431-30 30-30 16.569 0 30 13.431 30 30 0 16.569-13.431 30-30 30z"
|
||||
></path>
|
||||
<path stroke="#000" strokeWidth="15" d="M72 108.5l56 56"></path>
|
||||
<path
|
||||
fill="#000"
|
||||
d="M322 143c16.569 0 30-13.431 30-30 0-16.569-13.431-30-30-30-16.569 0-30 13.431-30 30 0 16.569 13.431 30 30 30z"
|
||||
></path>
|
||||
<path stroke="#000" strokeWidth="15" d="M327.5 108.5l-56 56"></path>
|
||||
<path
|
||||
fill="#FFDF6F"
|
||||
fillRule="evenodd"
|
||||
d="M85.516 292.019c-16.17-7.698-25.58-24.983-22.427-42.612C76.618 173.747 133 117 200.5 117c67.663 0 124.155 57.023 137.509 132.958 3.106 17.66-6.381 34.937-22.605 42.572C280.687 308.868 241.91 318 201 318c-41.335 0-80.493-9.323-115.484-25.981z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
<path
|
||||
fill="#000"
|
||||
d="M70.472 250.728C83.544 177.62 137.582 124.5 200.5 124.5v-15c-72.082 0-130.809 60.375-144.794 138.587l14.766 2.641zM200.5 124.5c63.069 0 117.218 53.379 130.122 126.757l14.774-2.598C331.592 170.166 272.758 109.5 200.5 109.5v15zm111.71 161.244C278.472 301.621 240.783 310.5 201 310.5v15c42.037 0 81.902-9.386 117.597-26.183l-6.387-13.573zM201 310.5c-40.196 0-78.255-9.064-112.26-25.253l-6.448 13.544C118.269 315.918 158.526 325.5 201 325.5v-15zm129.622-59.243c2.49 14.159-5.091 28.219-18.412 34.487l6.387 13.573c19.128-9.002 30.52-29.497 26.799-50.658l-14.774 2.598zm-274.916-3.17c-3.778 21.124 7.524 41.629 26.586 50.704l6.447-13.544c-13.276-6.32-20.795-20.387-18.267-34.519l-14.766-2.641z"
|
||||
></path>
|
||||
<path
|
||||
fill="#000"
|
||||
fillRule="evenodd"
|
||||
d="M114.365 273.209c-13.015-5.301-20.736-19.149-16.226-32.459C112.047 199.704 152.618 170 200.5 170c47.882 0 88.453 29.704 102.361 70.75 4.51 13.31-3.211 27.158-16.226 32.459C260.053 284.035 230.973 290 200.5 290c-30.473 0-59.553-5.965-86.135-16.791z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M235 254c13.807 0 25-8.954 25-20s-11.193-20-25-20-25 8.954-25 20 11.193 20 25 20zM163.432 254.012c13.807 0 25-8.954 25-20s-11.193-20-25-20-25 8.954-25 20 11.193 20 25 20z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
export function AnnouncementIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M16.36 3.014A27.429 27.429 0 0 1 8.143 8.04l-4.67 1.825a5.126 5.126 0 0 0 1.7 6.34l1.631-.25m9.556-12.94c-.875.234-.824 3.262.114 6.764.938 3.501 2.408 6.15 3.283 5.915M16.36 3.014c.875-.234 2.345 2.414 3.284 5.915.938 3.502.989 6.53.113 6.765m0 0a27.428 27.428 0 0 0-8.595-.382m0 0L13.295 22H8.92l-2.116-6.044m4.358-.644c-.345.04-.69.085-1.034.138l-3.324.506" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export function AntenasIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 14a5 5 0 118 0m1 4.483a9 9 0 10-10 0M12 22l1.367-4.103a1.441 1.441 0 10-2.735 0L12 22zm0-10a1 1 0 110-2 1 1 0 010 2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function ArrowDownIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6.5 14.17a30.23 30.23 0 005.406 5.62c.174.14.384.21.594.21m6-5.83a30.232 30.232 0 01-5.406 5.62.949.949 0 01-.594.21m0 0V4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export function ArrowLeftIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||
return (
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M10 5.75 3.75 12 10 18.25M4.5 12h15.75"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export function ArrowRightIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||
return (
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M14 5.75 20.25 12 14 18.25M19.5 12H3.75"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function ArrowRightCircleIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M7.75 12h8M13 8.75l2.896 2.896a.5.5 0 010 .708L13 15.25M21.25 12a9.25 9.25 0 11-18.5 0 9.25 9.25 0 0118.5 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function ArrowUpIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 9.83a30.23 30.23 0 015.406-5.62A.949.949 0 0112 4m6 5.83a30.233 30.233 0 00-5.406-5.62A.949.949 0 0012 4m0 0v16"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function ArrowUpSquareIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8.5 11.949a20.335 20.335 0 013.604-3.807A.626.626 0 0112.5 8m4 3.949a20.334 20.334 0 00-3.604-3.807A.626.626 0 0012.5 8m0 0v8m0 5c-2.796 0-4.193 0-5.296-.457a6 6 0 01-3.247-3.247C3.5 16.194 3.5 14.796 3.5 12c0-2.796 0-4.193.457-5.296a6 6 0 013.247-3.247C8.307 3 9.704 3 12.5 3c2.796 0 4.194 0 5.296.457a6 6 0 013.247 3.247c.457 1.103.457 2.5.457 5.296 0 2.796 0 4.194-.457 5.296a6 6 0 01-3.247 3.247C16.694 21 15.296 21 12.5 21z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function ArticleIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M20.248 15.25H17.25a2 2 0 0 0-2 2v2.998m4.998-4.998c.002-.026.002-.052.002-.078V5.75a2 2 0 0 0-2-2H5.75a2 2 0 0 0-2 2v12.5a2 2 0 0 0 2 2h9.422c.026 0 .052 0 .078-.002m4.998-4.998a2 2 0 0 1-.584 1.336l-3.078 3.078a2 2 0 0 1-1.336.584M8.75 8.75h6.5m-6.5 4h2.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function BellIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
d="M16 18.25c-.673 1.766-2.21 3-4 3s-3.327-1.234-4-3m-2.152 0h12.304a2 2 0 0 0 1.974-2.319l-1.17-7.258a7.045 7.045 0 0 0-13.911 0l-1.171 7.258a2 2 0 0 0 1.974 2.319Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function BellFilledIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M12 2a7.853 7.853 0 0 0-7.784 6.815l-.905 6.789A3 3 0 0 0 6.284 19h1.07c.904 1.748 2.607 3 4.646 3 2.039 0 3.742-1.252 4.646-3h1.07a3 3 0 0 0 2.973-3.396l-.905-6.789A7.853 7.853 0 0 0 12 2Zm2.222 17H9.778c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function BoldIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13 12H6m7 0a4 4 0 000-8H8a2 2 0 00-2 2v6m7 0h1a4 4 0 010 8H8a2 2 0 01-2-2v-6"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
export function CancelIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||
return (
|
||||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="1.5"
|
||||
d="m4.75 4.75 14.5 14.5m0-14.5-14.5 14.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||