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
This commit is contained in:
雨宮蓮
2024-08-27 19:37:30 +07:00
committed by GitHub
parent 26ae473521
commit 61ad96ca63
318 changed files with 5564 additions and 8458 deletions

View File

@@ -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
View File

@@ -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
View File

@@ -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"

View File

@@ -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"

View File

@@ -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": [

View File

@@ -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",

File diff suppressed because one or more lines are too long

View File

@@ -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"]}}

View File

@@ -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": [

View File

@@ -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": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

View 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": ""
}
]

View File

@@ -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"
}
]

View File

@@ -1,2 +1,4 @@
wss://relay.damus.io,
wss://relay.nostr.net,
wss://purplepag.es/,
wss://directory.yabu.me/,

View File

@@ -1,5 +0,0 @@
[
{ "label": "onboarding", "name": "Onboarding", "content": "/onboarding" },
{ "label": "lume_newsfeed", "name": "Newsfeed", "content": "/newsfeed" },
{ "label": "lume_topic", "name": "Topic", "content": "/topic" }
]

View File

@@ -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)

View File

@@ -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()),
}
}

View File

@@ -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()),
}
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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)
}

View File

@@ -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::*;

View File

@@ -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()
}

View File

@@ -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);
}
}
_ => {}
}
}

View File

@@ -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": {

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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 **/

View File

@@ -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,
});

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);

View File

@@ -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>
);
}

View File

@@ -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" />

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

Some files were not shown because too many files have changed in this diff Show More