Compare commits

..
9 Commits
Author SHA1 Message Date
Ren AmamiyaandGitHub 025562f9f0 Merge pull request #61 from luminous-devs/main
v1.1.1
2023-07-27 09:26:26 +07:00
Ren AmamiyaandGitHub 595bcc9b3c Merge pull request #59 from luminous-devs/main
v1.1.0
2023-07-26 09:27:39 +07:00
Ren AmamiyaandGitHub 15991d07ab Merge pull request #53 from luminous-devs/main
update set password flow
2023-07-10 17:20:26 +07:00
Ren AmamiyaandGitHub 99fc1f0b10 Merge pull request #52 from luminous-devs/main
update gh action and fix migrate page
2023-07-10 15:34:03 +07:00
Ren AmamiyaandGitHub 1041e1ccd4 Merge pull request #50 from luminous-devs/main
v1.0.1
2023-07-10 14:40:17 +07:00
Ren AmamiyaandGitHub 2eeb2c896d Merge pull request #45 from luminous-devs/main
rebuild with code signing
2023-07-07 12:24:08 +07:00
Ren AmamiyaandGitHub caf8fb584a Merge pull request #33 from luminous-devs/main
v1.0.0
2023-07-06 08:18:01 +07:00
Ren AmamiyaandGitHub 58205713ab Merge pull request #32 from luminous-devs/main
test github action again
2023-07-05 17:40:35 +07:00
Ren AmamiyaandGitHub 33802d32f3 Merge pull request #31 from luminous-devs/main
test github action
2023-07-05 09:50:57 +07:00
400 changed files with 17233 additions and 18366 deletions
-1
View File
@@ -1 +0,0 @@
use flake
-3
View File
@@ -1,3 +0,0 @@
/**/node_modules/*
node_modules/
dist/
+10 -9
View File
@@ -1,5 +1,8 @@
name: 'Publish' name: 'publish'
on: workflow_dispatch on:
push:
branches:
- release
env: env:
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
@@ -14,7 +17,7 @@ jobs:
settings: settings:
- platform: 'macos-latest' - platform: 'macos-latest'
args: '--target universal-apple-darwin' args: '--target universal-apple-darwin'
- platform: 'ubuntu-22.04' - platform: 'ubuntu-20.04'
args: '' args: ''
- platform: 'windows-latest' - platform: 'windows-latest'
args: '--target x86_64-pc-windows-msvc' args: '--target x86_64-pc-windows-msvc'
@@ -29,14 +32,14 @@ jobs:
with: with:
targets: aarch64-apple-darwin targets: aarch64-apple-darwin
- name: install dependencies (ubuntu only) - name: install dependencies (ubuntu only)
if: matrix.settings.platform == 'ubuntu-22.04' if: matrix.settings.platform == 'ubuntu-20.04'
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential libssl-dev javascriptcoregtk-4.1 libayatana-appindicator3-dev libsoup-3.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev webkit2gtk-4.1 librsvg2-dev patchelf sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
with: with:
version: 8.x.x version: 7.x.x
run_install: false run_install: false
- name: Setup node and cache for package data - name: Setup node and cache for package data
uses: actions/setup-node@v3 uses: actions/setup-node@v3
@@ -59,12 +62,10 @@ jobs:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with: with:
tagName: v__VERSION__ tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
releaseName: 'App v__VERSION__' releaseName: 'App v__VERSION__'
releaseBody: 'See the assets to download this version and install.' releaseBody: 'See the assets to download this version and install.'
releaseDraft: true releaseDraft: true
prerelease: false prerelease: false
args: ${{ matrix.settings.args }} args: ${{ matrix.settings.args }}
includeDebug: true
+1 -3
View File
@@ -14,11 +14,9 @@ out
*.local *.local
.next .next
.vscode .vscode
pnpm-lock.yaml
*.db *.db
*.db-journal *.db-journal
bun.lockb
.direnv
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
+6 -15
View File
@@ -8,12 +8,6 @@ Download Lume for your platform here: [https://github.com/luminous-devs/lume/rel
Supported platform: macOS, Windows and Linux Supported platform: macOS, Windows and Linux
### Prerequisites
- PNPM or Bun (experiment)
- Tauri: https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-macos
### Develop ### Develop
Clone project Clone project
@@ -28,23 +22,20 @@ Install packages
pnpm install pnpm install
``` ```
Run dev build Run dev
``` ```
pnpm tauri dev pnpm tauri dev
``` ```
Generate production build Build
``` ```
pnpm tauri build pnpm tauri build
``` ```
#### Nix (Advance) - Generate SQLite migration
Requirements: ```
pnpm add-migrate <migrate_name>
1. [Install Nix](https://zero-to-flakes.com/install) ```
1. [Setup `direnv`](https://zero-to-flakes.com/direnv)
`cd` into the root folder of the project to enter `nix develop` shell. Run `direnv allow` (only once). Then run `pnpm` or `bun` (experimental) commands as described above.
Generated
-130
View File
@@ -1,130 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1697723726,
"narHash": "sha256-SaTWPkI8a5xSHX/rrKzUe+/uVNy6zCGMXgoeMb7T9rg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7c9cc5a6e5d38010801741ac830a3f8fd667a7a0",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1697940838,
"narHash": "sha256-eyk92QqAoRNC0V99KOcKcBZjLPixxNBS0PRc4KlSQVs=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "a3e829c06eadf848f13d109c7648570ce37ebccd",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
-71
View File
@@ -1,71 +0,0 @@
# Nix.flake to build Lume based on Tauri's Guides:
# Prerequisites -> Installing -> Setting Up Linux -> NixOS
# https://tauri.app/v1/guides/getting-started/prerequisites/#1-system-dependencies
#
# To build Rust backend of Tauri `rust-overlay` is used
# https://github.com/oxalica/rust-overlay
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
libraries = with pkgs;[
webkitgtk
gtk3
cairo
gdk-pixbuf
glib
dbus
openssl_3
librsvg
];
packages = with pkgs; [
curl
wget
pkg-config
dbus
openssl_3
glib
gtk3
libsoup
webkitgtk
librsvg
];
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" ]; # needed by rust-analyzer
};
in
{
devShells.default = pkgs.mkShell {
buildInputs = [
rustToolchain
pkgs.nodejs
pkgs.nodePackages.pnpm
pkgs.bun # experimental in Lume
] ++ packages;
shellHook =
''
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libraries}:$LD_LIBRARY_PATH
export XDG_DATA_DIRS=${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}:$XDG_DATA_DIRS
'';
# Avoid white screen running with Nix
# https://github.com/tauri-apps/tauri/issues/4315#issuecomment-1207755694
WEBKIT_DISABLE_COMPOSITING_MODE = 1;
};
});
}
+3 -3
View File
@@ -1,11 +1,11 @@
<html lang="en"> <html lang="en" class="dark">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lume</title> <title>Lume</title>
</head> </head>
<body class="relative cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen text-neutral-950 dark:text-neutral-50"> <body class="cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen dark:bg-black dark:text-zinc-100">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>
+63 -89
View File
@@ -1,8 +1,7 @@
{ {
"name": "lume", "name": "lume",
"description": "the communication app",
"private": true, "private": true,
"version": "2.0.1", "version": "1.1.1",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
@@ -18,108 +17,83 @@
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write" "**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
}, },
"dependencies": { "dependencies": {
"@evilmartians/harmony": "^1.1.0", "@headlessui/react": "^1.7.15",
"@getalby/sdk": "^2.5.0", "@nostr-dev-kit/ndk": "^0.7.7",
"@nostr-dev-kit/ndk": "^2.0.3", "@nostr-fetch/adapter-ndk": "^0.11.0",
"@nostr-dev-kit/ndk-cache-dexie": "^2.0.3", "@radix-ui/react-dialog": "^1.0.4",
"@nostr-fetch/adapter-ndk": "^0.13.0", "@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-tooltip": "^1.0.6",
"@radix-ui/react-avatar": "^1.0.4", "@tanstack/react-query": "^4.32.0",
"@radix-ui/react-collapsible": "^1.0.3", "@tanstack/react-query-devtools": "^4.32.0",
"@radix-ui/react-dialog": "^1.0.5", "@tanstack/react-virtual": "3.0.0-beta.54",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@tauri-apps/api": "^1.4.0",
"@radix-ui/react-hover-card": "^1.0.7", "@tiptap/extension-image": "^2.0.4",
"@radix-ui/react-popover": "^1.0.7", "@tiptap/extension-mention": "^2.0.4",
"@radix-ui/react-toolbar": "^1.0.4", "@tiptap/extension-placeholder": "^2.0.4",
"@radix-ui/react-tooltip": "^1.0.7", "@tiptap/pm": "^2.0.4",
"@tanstack/query-sync-storage-persister": "^5.4.3", "@tiptap/react": "^2.0.4",
"@tanstack/react-query": "^5.0.5", "@tiptap/starter-kit": "^2.0.4",
"@tanstack/react-query-persist-client": "^5.4.3", "@tiptap/suggestion": "^2.0.4",
"@tauri-apps/api": "^2.0.0-alpha.9", "cheerio": "1.0.0-rc.12",
"@tauri-apps/cli": "^2.0.0-alpha.16", "dayjs": "^1.11.9",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-alpha.2", "destr": "^1.2.2",
"@tauri-apps/plugin-dialog": "^2.0.0-alpha.2", "framer-motion": "^10.13.1",
"@tauri-apps/plugin-fs": "^2.0.0-alpha.2", "get-urls": "^11.0.0",
"@tauri-apps/plugin-http": "^2.0.0-alpha.2",
"@tauri-apps/plugin-notification": "^2.0.0-alpha.2",
"@tauri-apps/plugin-os": "^2.0.0-alpha.3",
"@tauri-apps/plugin-process": "^2.0.0-alpha.2",
"@tauri-apps/plugin-shell": "^2.0.0-alpha.2",
"@tauri-apps/plugin-sql": "^2.0.0-alpha.2",
"@tauri-apps/plugin-updater": "^2.0.0-alpha.2",
"@tauri-apps/plugin-upload": "^2.0.0-alpha.2",
"@tiptap/extension-character-count": "^2.1.12",
"@tiptap/extension-document": "^2.1.12",
"@tiptap/extension-image": "^2.1.12",
"@tiptap/extension-mention": "^2.1.12",
"@tiptap/extension-paragraph": "^2.1.12",
"@tiptap/extension-placeholder": "^2.1.12",
"@tiptap/extension-text": "^2.1.12",
"@tiptap/pm": "^2.1.12",
"@tiptap/react": "^2.1.12",
"@tiptap/starter-kit": "^2.1.12",
"@tiptap/suggestion": "^2.1.12",
"dayjs": "^1.11.10",
"destr": "^2.0.2",
"framer-motion": "^10.16.4",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"immer": "^10.0.3", "immer": "^10.0.2",
"light-bolt11-decoder": "^3.0.0", "light-bolt11-decoder": "^3.0.0",
"lru-cache": "^10.0.1", "nostr-fetch": "^0.12.2",
"markdown-to-jsx": "^7.3.2", "nostr-tools": "^1.13.1",
"media-chrome": "^1.4.5",
"million": "^2.6.4",
"minidenticons": "^4.2.0",
"nostr-fetch": "^0.13.0",
"nostr-tools": "^1.17.0",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"re-resizable": "^6.9.11",
"react": "^18.2.0", "react": "^18.2.0",
"react-currency-input-field": "^3.6.11",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.47.0", "react-hook-form": "^7.45.2",
"react-hotkeys-hook": "^4.4.1", "react-hotkeys-hook": "^4.4.1",
"react-router-dom": "^6.17.0", "react-markdown": "^8.0.7",
"reactflow": "^11.9.4", "react-player": "^2.12.0",
"sonner": "^1.0.3", "react-router-dom": "^6.14.2",
"tailwind-scrollbar": "^3.0.5", "react-string-replace": "^1.1.1",
"tauri-controls": "^0.2.0", "react-virtuoso": "^4.4.2",
"remark-gfm": "^3.0.1",
"tailwind-merge": "^1.14.0",
"tauri-plugin-autostart-api": "github:tauri-apps/tauri-plugin-autostart#v1",
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql",
"tauri-plugin-stronghold-api": "github:tauri-apps/tauri-plugin-stronghold#v1",
"tauri-plugin-upload-api": "github:tauri-apps/tauri-plugin-upload#v1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.2", "zustand": "^4.3.9"
"virtua": "^0.15.6",
"zustand": "^4.4.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.9",
"@trivago/prettier-plugin-sort-imports": "^4.2.1", "@tauri-apps/cli": "^1.4.0",
"@types/html-to-text": "^9.0.3", "@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/node": "^20.8.9", "@types/html-to-text": "^9.0.1",
"@types/react": "^18.2.33", "@types/node": "^18.17.1",
"@types/react-dom": "^18.2.14", "@types/react": "^18.2.17",
"@types/youtube-player": "^5.5.9", "@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.9.0", "@types/youtube-player": "^5.5.7",
"@typescript-eslint/parser": "^6.9.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@vitejs/plugin-react-swc": "^3.4.0", "@typescript-eslint/parser": "^5.62.0",
"autoprefixer": "^10.4.16", "@vitejs/plugin-react-swc": "^3.3.2",
"clsx": "^2.0.0", "autoprefixer": "^10.4.14",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"csstype": "^3.1.2", "csstype": "^3.1.2",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"eslint": "^8.52.0", "eslint": "^8.45.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.0",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"lint-staged": "^15.0.2", "lint-staged": "^13.2.3",
"postcss": "^8.4.31", "postcss": "^8.4.27",
"prettier": "^3.0.3", "prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.5.6", "prettier-plugin-tailwindcss": "^0.3.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"tailwind-merge": "^1.14.0", "tailwindcss": "^3.3.3",
"tailwindcss": "^3.3.5", "typescript": "^4.9.5",
"typescript": "^5.2.2", "vite": "^4.4.7",
"vite": "^4.5.0", "vite-plugin-top-level-await": "^1.3.1",
"vite-tsconfig-paths": "^4.2.1" "vite-tsconfig-paths": "^4.2.0"
} }
} }
+3827 -2710
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 986 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

+1410 -1632
View File
File diff suppressed because it is too large Load Diff
+43 -28
View File
@@ -1,45 +1,60 @@
[package] [package]
name = "lume" name = "lume"
version = "2.0.1" version = "1.1.1"
description = "the communication app" description = "nostr client"
authors = ["Ren Amamiya"] authors = ["Ren Amamiya"]
license = "GPL-3.0" license = ""
repository = "https://github.com/luminous-devs/lume" repository = ""
edition = "2021" edition = "2021"
rust-version = "1.66" rust-version = "1.57"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-alpha", features = [] } tauri-build = { version = "1.2", features = [] }
[dependencies] [dependencies]
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2.0.0-alpha", features = [ tauri = { version = "1.2", features = [
"macos-private-api", "fs-remove-file",
"native-tls-vendored", "fs-write-file",
] } "window-create",
tauri-plugin-cli = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "path-all",
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "fs-read-dir",
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "fs-read-file",
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "clipboard-read-text",
tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "clipboard-write-text",
tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "dialog-open",
tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "http-all",
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "http-multipart",
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "notification-all",
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "os-all",
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "process-relaunch",
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "shell-open",
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "system-tray",
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } "updater",
tauri-plugin-sql = { git = "hhttps://github.com/tauri-apps/plugins-workspace", branch = "v2", features = [ "window-close",
"sqlite", "window-start-dragging",
] } ] }
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-stronghold = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
sqlx-cli = { version = "0.7.0", default-features = false, features = [ sqlx-cli = { version = "0.7.0", default-features = false, features = [
"sqlite", "sqlite",
] } ] }
webpage = { version = "1.6.0", features = ["serde"] } rust-argon2 = "1.0"
keyring = "2" rand = "0.8.5"
[dependencies.tauri-plugin-sql]
git = "https://github.com/tauri-apps/plugins-workspace"
branch = "v1"
features = ["sqlite"]
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
cocoa = "0.24.1"
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 886 B

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 894 B

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

@@ -1,29 +1,45 @@
-- Add migration script here
-- create accounts table -- create accounts table
-- is_active (multi-account feature), value:
-- 0: false
-- 1: true
CREATE TABLE CREATE TABLE
accounts ( accounts (
id TEXT NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
npub TEXT NOT NULL UNIQUE,
pubkey TEXT NOT NULL UNIQUE, pubkey TEXT NOT NULL UNIQUE,
follows TEXT, privkey TEXT NOT NULL,
circles TEXT, follows JSON,
is_active INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 0,
last_login_at NUMBER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
-- create notes table -- create notes table
CREATE TABLE CREATE TABLE
events ( notes (
id TEXT NOT NULL PRIMARY KEY, id INTEGER NOT NULL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE,
account_id INTEGER NOT NULL, account_id INTEGER NOT NULL,
event TEXT NOT NULL, pubkey TEXT NOT NULL,
author TEXT NOT NULL, kind INTEGER NOT NULL DEFAULT 1,
kind NUMBER NOT NULL DEFAULt 1, tags JSON,
root_id TEXT, content TEXT NOT NULL,
reply_id TEXT,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
parent_id TEXT,
FOREIGN KEY (account_id) REFERENCES accounts (id) FOREIGN KEY (account_id) REFERENCES accounts (id)
); );
-- create channels table
CREATE TABLE
channels (
id INTEGER NOT NULL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE,
name TEXT,
about TEXT,
picture TEXT,
created_at INTEGER NOT NULL
);
-- create settings table -- create settings table
CREATE TABLE CREATE TABLE
settings ( settings (
@@ -33,23 +49,11 @@ CREATE TABLE
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
-- create metadata table
CREATE TABLE CREATE TABLE
widgets ( metadata (
id INTEGER NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL, pubkey TEXT NOT NULL,
kind INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
FOREIGN KEY (account_id) REFERENCES accounts (id)
);
CREATE TABLE
relays (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
relay TEXT NOT NULL UNIQUE,
purpose TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id)
); );
@@ -0,0 +1,12 @@
-- Add migration script here
-- create chats table
CREATE TABLE
chats (
id INTEGER NOT NULL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE,
receiver_pubkey INTEGER NOT NULL,
sender_pubkey TEXT NOT NULL,
content TEXT NOT NULL,
tags JSON,
created_at INTEGER NOT NULL
);
@@ -0,0 +1,14 @@
-- Add migration script here
INSERT INTO
settings (key, value)
VALUES
("last_login", "0"),
(
"relays",
'["wss://relayable.org","wss://relay.damus.io","wss://relay.nostr.band/all","wss://relay.nostrgraph.net","wss://nostr.mutinywallet.com"]'
),
("auto_start", "0"),
("cache_time", "86400000"),
("compose_shortcut", "meta+n"),
("add_imageblock_shortcut", "meta+i"),
("add_feedblock_shortcut", "meta+f")
@@ -0,0 +1,3 @@
-- Add migration script here
-- add pubkey to channel
ALTER TABLE channels ADD pubkey TEXT NOT NULL DEFAULT '';
@@ -0,0 +1,38 @@
-- Add migration script here
INSERT
OR IGNORE INTO channels (
event_id,
pubkey,
name,
about,
picture,
created_at
)
VALUES
(
"e3cadf5beca1b2af1cddaa41a633679bedf263e3de1eb229c6686c50d85df753",
"126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f",
"lume-general",
"General channel for Lume",
"https://void.cat/d/UNyxBmAh1MUx5gQTX95jyf.webp",
1681898574
);
INSERT
OR IGNORE INTO channels (
event_id,
pubkey,
name,
about,
picture,
created_at
)
VALUES
(
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb",
"ed1d0e1f743a7d19aa2dfb0162df73bacdbc699f67cc55bb91a98c35f7deac69",
"Nostr",
"",
"https://cloudflare-ipfs.com/ipfs/QmTN4Eas9atUULVbEAbUU8cowhtvK7g3t7jfKztY7wc8eP?.png",
1661333723
);
@@ -0,0 +1,11 @@
-- Add migration script here
-- create blacklist table
CREATE TABLE
blacklist (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
content TEXT NOT NULL UNIQUE,
status INTEGER NOT NULL DEFAULT 0,
kind INTEGER NOT NULL,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);
@@ -0,0 +1,11 @@
-- Add migration script here
CREATE TABLE
blocks (
id INTEGER NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
kind INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id)
);
@@ -0,0 +1,15 @@
-- Add migration script here
CREATE TABLE
channel_messages (
id INTEGER NOT NULL PRIMARY KEY,
channel_id TEXT NOT NULL,
event_id TEXT NOT NULL UNIQUE,
pubkey TEXT NOT NULL,
kind INTEGER NOT NULL,
content TEXT NOT NULL,
tags JSON,
mute BOOLEAN DEFAULT 0,
hide BOOLEAN DEFAULT 0,
created_at INTEGER NOT NULL,
FOREIGN KEY (channel_id) REFERENCES channels (event_id)
);
@@ -0,0 +1,13 @@
-- Add migration script here
CREATE TABLE
replies (
id INTEGER NOT NULL PRIMARY KEY,
parent_id TEXT NOT NULL,
event_id TEXT NOT NULL UNIQUE,
pubkey TEXT NOT NULL,
kind INTEGER NOT NULL DEFAULT 1,
tags JSON,
content TEXT NOT NULL,
created_at INTEGER NOT NULL,
FOREIGN KEY (parent_id) REFERENCES notes (event_id)
);
@@ -0,0 +1,6 @@
-- Add migration script here
DROP TABLE IF EXISTS blacklist;
DROP TABLE IF EXISTS channel_messages;
DROP TABLE IF EXISTS channels;
@@ -0,0 +1,6 @@
-- Add migration script here
UPDATE settings
SET
value = '["wss://relayable.org","wss://relay.damus.io","wss://relay.nostr.band/all","wss://nostr.mutinywallet.com"]'
WHERE
key = 'relays';
@@ -1,27 +0,0 @@
-- Add migration script here
CREATE TABLE
ndk_users (
pubkey TEXT NOT NULL PRIMARY KEY,
profile TEXT,
createdAt NUMBER
);
CREATE TABLE
ndk_events (
id TEXT NOT NULL PRIMARY KEY,
pubkey TEXT,
content TEXT,
kind NUMBER,
createdAt NUMBER,
relay TEXT,
event TEXT
);
CREATE TABLE
ndk_eventtags (
id TEXT NOT NULL PRIMARY KEY,
eventId TEXT,
tag TEXT,
value TEXT,
tagValue TEXT
);
+112 -118
View File
@@ -3,12 +3,19 @@
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use keyring::Entry; #[cfg(target_os = "macos")]
use std::time::Duration; #[macro_use]
use tauri::Manager; extern crate objc;
// use rand::distributions::{Alphanumeric, DistString};
use tauri::{Manager, WindowEvent};
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_sql::{Migration, MigrationKind}; use tauri_plugin_sql::{Migration, MigrationKind};
use webpage::{Webpage, WebpageOptions};
#[cfg(target_os = "macos")]
use window_ext::WindowExt;
#[cfg(target_os = "macos")]
mod window_ext;
#[derive(Clone, serde::Serialize)] #[derive(Clone, serde::Serialize)]
struct Payload { struct Payload {
@@ -16,118 +23,35 @@ struct Payload {
cwd: String, cwd: String,
} }
#[derive(serde::Serialize)]
struct OpenGraphResponse {
title: String,
description: String,
url: String,
image: String,
}
async fn fetch_opengraph(url: String) -> OpenGraphResponse {
let options = WebpageOptions {
allow_insecure: false,
max_redirections: 3,
timeout: Duration::from_secs(15),
useragent: "lume - desktop app".to_string(),
..Default::default()
};
let result = match Webpage::from_url(&url, options) {
Ok(webpage) => webpage,
Err(_) => {
return OpenGraphResponse {
title: "".to_string(),
description: "".to_string(),
url: "".to_string(),
image: "".to_string(),
}
}
};
let html = result.html;
return OpenGraphResponse {
title: html
.opengraph
.properties
.get("title")
.cloned()
.unwrap_or_default(),
description: html
.opengraph
.properties
.get("description")
.cloned()
.unwrap_or_default(),
url: html
.opengraph
.properties
.get("url")
.cloned()
.unwrap_or_default(),
image: html
.opengraph
.images
.get(0)
.and_then(|i| Some(i.url.clone()))
.unwrap_or_default(),
};
}
#[tauri::command]
async fn opengraph(url: String) -> OpenGraphResponse {
let result = fetch_opengraph(url).await;
return result;
}
#[tauri::command]
fn secure_save(key: String, value: String) -> Result<(), ()> {
let entry = Entry::new("lume", &key).expect("Failed to create entry");
let _ = entry.set_password(&value);
Ok(())
}
#[tauri::command]
fn secure_load(key: String) -> Result<String, String> {
let entry = Entry::new("lume", &key).expect("Failed to create entry");
if let Ok(password) = entry.get_password() {
Ok(password)
} else {
Err("not found".to_string())
}
}
#[tauri::command]
fn secure_remove(key: String) -> Result<(), ()> {
let entry = Entry::new("lume", &key).expect("Failed to create entry");
let _ = entry.delete_password();
Ok(())
}
fn main() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.setup(|app| { .setup(|app| {
#[cfg(desktop)] #[cfg(target_os = "macos")]
app let main_window = app.get_window("main").unwrap();
.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?; #[cfg(target_os = "macos")]
main_window.position_traffic_lights(13.0, 17.0); // set inset for traffic lights (macos)
Ok(()) Ok(())
}) })
.plugin(tauri_plugin_clipboard_manager::init()) .on_window_event(|e| {
.plugin(tauri_plugin_dialog::init()) #[cfg(target_os = "macos")]
.plugin(tauri_plugin_fs::init()) let apply_offset = || {
.plugin(tauri_plugin_http::init()) let win = e.window();
.plugin(tauri_plugin_notification::init()) // keep inset for traffic lights when window resize (macos)
.plugin(tauri_plugin_os::init()) win.position_traffic_lights(13.0, 17.0);
.plugin(tauri_plugin_process::init()) };
.plugin(tauri_plugin_shell::init()) #[cfg(target_os = "macos")]
.plugin(tauri_plugin_upload::init()) match e.event() {
.plugin(tauri_plugin_updater::Builder::new().build()) WindowEvent::Resized(..) => apply_offset(),
WindowEvent::ThemeChanged(..) => apply_offset(),
_ => {}
}
})
.plugin( .plugin(
tauri_plugin_sql::Builder::default() tauri_plugin_sql::Builder::default()
.add_migrations( .add_migrations(
"sqlite:lume_v2.db", "sqlite:lume.db",
vec![ vec![
Migration { Migration {
version: 20230418013219, version: 20230418013219,
@@ -136,15 +60,92 @@ fn main() {
kind: MigrationKind::Up, kind: MigrationKind::Up,
}, },
Migration { Migration {
version: 20231028083224, version: 20230418080146,
description: "add ndk cache table", description: "create chats",
sql: include_str!("../migrations/20231028083224_add_ndk_cache_table.sql"), sql: include_str!("../migrations/20230418080146_create_chats.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230420040005,
description: "insert last login to settings",
sql: include_str!("../migrations/20230420040005_insert_last_login_to_settings.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230425023912,
description: "add pubkey to channel",
sql: include_str!("../migrations/20230425023912_add_pubkey_to_channel.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230425024708,
description: "add default channels",
sql: include_str!("../migrations/20230425024708_add_default_channels.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230425050745,
description: "create blacklist",
sql: include_str!("../migrations/20230425050745_add_blacklist_model.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230521092300,
description: "create block",
sql: include_str!("../migrations/20230521092300_add_block_model.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230617003135,
description: "add channel messages",
sql: include_str!("../migrations/20230617003135_add_channel_messages.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230619082415,
description: "add replies",
sql: include_str!("../migrations/20230619082415_add_replies.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230718072634,
description: "clean up",
sql: include_str!("../migrations/20230718072634_clean_up_old_tables.sql"),
kind: MigrationKind::Up,
},
Migration {
version: 20230725010250,
description: "update default relays",
sql: include_str!("../migrations/20230725010250_update_default_relays.sql"),
kind: MigrationKind::Up, kind: MigrationKind::Up,
}, },
], ],
) )
.build(), .build(),
) )
.plugin(
tauri_plugin_stronghold::Builder::new(|password| {
let config = argon2::Config {
lanes: 2,
mem_cost: 50_000,
time_cost: 30,
thread_mode: argon2::ThreadMode::from_threads(2),
variant: argon2::Variant::Argon2id,
..Default::default()
};
// let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12);
let key = argon2::hash_raw(
password.as_ref(),
b"LUME_NEED_RUST_DEVELOPER_HELP_MAKE_SALT_RANDOM",
&config,
)
.expect("failed to hash password");
key.to_vec()
})
.build(),
)
.plugin(tauri_plugin_autostart::init( .plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent, MacosLauncher::LaunchAgent,
Some(vec!["--flag1", "--flag2"]), Some(vec!["--flag1", "--flag2"]),
@@ -156,13 +157,6 @@ fn main() {
.unwrap(); .unwrap();
})) }))
.plugin(tauri_plugin_upload::init()) .plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_store::Builder::default().build())
.invoke_handler(tauri::generate_handler![
opengraph,
secure_save,
secure_load,
secure_remove
])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }
+60
View File
@@ -0,0 +1,60 @@
use tauri::{Runtime, Window};
pub trait WindowExt {
#[cfg(target_os = "macos")]
fn set_transparent_titlebar(&self, transparent: bool);
fn position_traffic_lights(&self, x: f64, y: f64);
}
impl<R: Runtime> WindowExt for Window<R> {
#[cfg(target_os = "macos")]
fn set_transparent_titlebar(&self, transparent: bool) {
use cocoa::appkit::{NSWindow, NSWindowTitleVisibility};
let window = self.ns_window().unwrap() as cocoa::base::id;
unsafe {
window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
if transparent {
window.setTitlebarAppearsTransparent_(cocoa::base::YES);
} else {
window.setTitlebarAppearsTransparent_(cocoa::base::NO);
}
}
}
#[cfg(target_os = "macos")]
fn position_traffic_lights(&self, x: f64, y: f64) {
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
use cocoa::foundation::NSRect;
let window = self.ns_window().unwrap() as cocoa::base::id;
unsafe {
let close = window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
let miniaturize = window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
let zoom = window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
let title_bar_container_view = close.superview().superview();
let close_rect: NSRect = msg_send![close, frame];
let button_height = close_rect.size.height;
let title_bar_frame_height = button_height + y;
let mut title_bar_rect = NSView::frame(title_bar_container_view);
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y = NSView::frame(window).size.height - title_bar_frame_height;
let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect];
let window_buttons = vec![close, miniaturize, zoom];
let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
for (i, button) in window_buttons.into_iter().enumerate() {
let mut rect: NSRect = NSView::frame(button);
rect.origin.x = x + (i as f64 * space_between);
button.setFrameOrigin(rect.origin);
}
}
}
}
+83 -22
View File
@@ -1,18 +1,35 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": { "build": {
"beforeBuildCommand": "pnpm run build", "beforeDevCommand": "pnpm dev",
"beforeDevCommand": "pnpm run dev", "beforeBuildCommand": "pnpm build",
"devPath": "http://localhost:3000", "devPath": "http://localhost:3000",
"distDir": "../dist", "distDir": "../dist",
"withGlobalTauri": true "withGlobalTauri": true
}, },
"package": { "package": {
"productName": "Lume", "productName": "Lume",
"version": "2.0.1" "version": "1.1.1"
},
"tauri": {
"allowlist": {
"all": false,
"app": {
"all": false
},
"os": {
"all": true
},
"http": {
"all": true,
"request": true,
"scope": ["http://**", "https://**"]
}, },
"plugins": {
"fs": { "fs": {
"all": false,
"readFile": true,
"readDir": true,
"writeFile": true,
"removeFile": true,
"scope": [ "scope": [
"$APPDATA/*", "$APPDATA/*",
"$DATA/*", "$DATA/*",
@@ -26,30 +43,44 @@
"$VIDEO/*" "$VIDEO/*"
] ]
}, },
"http": { "path": {
"scope": [ "all": true
"http://**/",
"https://**/"
]
}, },
"shell": { "shell": {
"all": false,
"open": true "open": true
}, },
"updater": { "clipboard": {
"endpoints": [ "all": false,
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}", "writeText": true,
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}" "readText": true
] },
"dialog": {
"all": false,
"open": true
},
"notification": {
"all": true
},
"window": {
"startDragging": true,
"close": true,
"create": true
},
"process": {
"all": false,
"exit": false,
"relaunch": true,
"relaunchDangerousAllowSymlinkMacos": false
} }
}, },
"tauri": {
"bundle": { "bundle": {
"active": true, "active": true,
"category": "SocialNetworking",
"copyright": "",
"appimage": { "appimage": {
"bundleMediaFramework": true "bundleMediaFramework": true
}, },
"category": "SocialNetworking",
"copyright": "",
"deb": { "deb": {
"depends": [] "depends": []
}, },
@@ -67,21 +98,51 @@
"entitlements": null, "entitlements": null,
"exceptionDomain": "", "exceptionDomain": "",
"frameworks": [], "frameworks": [],
"license": "../LICENSE",
"minimumSystemVersion": "10.15.0",
"providerShortName": null, "providerShortName": null,
"signingIdentity": null "signingIdentity": null
}, },
"resources": [], "resources": [],
"shortDescription": "", "shortDescription": "",
"targets": "all", "targets": "all",
"updater": {},
"windows": { "windows": {
"certificateThumbprint": null, "certificateThumbprint": null,
"digestAlgorithm": "sha256", "digestAlgorithm": "sha256",
"timestampUrl": "" "timestampUrl": ""
} }
}, },
"macOSPrivateApi": true "security": {
"csp": "upgrade-insecure-requests"
},
"updater": {
"active": true,
"dialog": true,
"endpoints": [
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU4RjAzODFBREQ4MkM3RTEKUldUaHg0TGRHamp3NkI5bnhoOEVjanlHWFNzQ2Q3NDhubFFLUmJpSHJ1L2FqNnB3alF1Y2R3U3gK",
"windows": {
"installMode": "passive"
}
},
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
},
"windows": [
{
"title": "Lume",
"theme": "Dark",
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"transparent": false,
"fullscreen": false,
"resizable": true,
"width": 1080,
"height": 800,
"minWidth": 1080,
"minHeight": 720
}
]
} }
} }
-21
View File
@@ -1,21 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"windows": [
{
"width": 1080,
"height": 800,
"minWidth": 560,
"minHeight": 800,
"resizable": true,
"title": "Lume",
"center": true,
"fullscreen": false,
"hiddenTitle": true,
"fileDropEnabled": true,
"decorations": false,
"transparent": false
}
]
}
}
-25
View File
@@ -1,25 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"windows": [
{
"width": 1080,
"height": 800,
"minWidth": 560,
"minHeight": 800,
"resizable": true,
"title": "Lume",
"titleBarStyle": "Overlay",
"center": true,
"fullscreen": false,
"hiddenTitle": true,
"fileDropEnabled": true,
"decorations": true,
"transparent": true,
"windowEffects": {
"effects": ["sidebar"]
}
}
]
}
}
-24
View File
@@ -1,24 +0,0 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"windows": [
{
"width": 1080,
"height": 800,
"minWidth": 560,
"minHeight": 800,
"resizable": true,
"title": "Lume",
"center": true,
"fullscreen": false,
"hiddenTitle": true,
"fileDropEnabled": true,
"decorations": false,
"transparent": true,
"windowEffects": {
"effects": ["micaLight", "micaDark"]
}
}
]
}
}
-66
View File
@@ -1,66 +0,0 @@
@import 'reactflow/dist/style.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.break-p {
word-break: break-word;
word-wrap: break-word;
overflow-wrap: break-word;
}
}
html {
font-size: 14px;
}
a {
@apply cursor-default no-underline !important;
}
button {
@apply cursor-default focus:outline-none;
}
input::-ms-reveal,
input::-ms-clear {
display: none;
}
::-webkit-input-placeholder {
line-height: normal;
}
.border {
background-clip: padding-box;
}
.player {
--brand-color: #f5f5f5;
--focus-color: #4e9cf6;
--audio-brand: var(--brand-color);
--audio-focus-ring-color: var(--focus-color);
--audio-border-radius: 2px;
--video-brand: var(--brand-color);
--video-focus-ring-color: var(--focus-color);
--video-border-radius: 8px;
width: 100%;
}
.player[data-view-type='video'] {
aspect-ratio: 16 /9;
}
.ProseMirror p.is-empty::before {
@apply text-neutral-600 dark:text-neutral-400;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.ProseMirror img.ProseMirror-selectednode {
@apply outline-blue-500;
}
+77 -220
View File
@@ -1,259 +1,116 @@
import { message } from '@tauri-apps/plugin-dialog'; import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';
import { ReactFlowProvider } from 'reactflow';
import { AuthCreateScreen } from '@app/auth/create';
import { CreateStep1Screen } from '@app/auth/create/step-1';
import { CreateStep2Screen } from '@app/auth/create/step-2';
import { CreateStep3Screen } from '@app/auth/create/step-3';
import { CreateStep4Screen } from '@app/auth/create/step-4';
import { CreateStep5Screen } from '@app/auth/create/step-5';
import { AuthImportScreen } from '@app/auth/import';
import { ImportStep1Screen } from '@app/auth/import/step-1';
import { ImportStep2Screen } from '@app/auth/import/step-2';
import { ImportStep3Screen } from '@app/auth/import/step-3';
import { MigrateScreen } from '@app/auth/migrate';
import { OnboardingScreen } from '@app/auth/onboarding'; import { OnboardingScreen } from '@app/auth/onboarding';
import { ChatsScreen } from '@app/chats'; import { ResetScreen } from '@app/auth/reset';
import { UnlockScreen } from '@app/auth/unlock';
import { WelcomeScreen } from '@app/auth/welcome';
import { ChannelScreen } from '@app/channel';
import { ChatScreen } from '@app/chats';
import { ErrorScreen } from '@app/error'; import { ErrorScreen } from '@app/error';
import { ExploreScreen } from '@app/explore'; import { NoteScreen } from '@app/note';
import { NewScreen } from '@app/new'; import { Root } from '@app/root';
import { AccountSettingsScreen } from '@app/settings/account';
import { GeneralSettingsScreen } from '@app/settings/general';
import { ShortcutsSettingsScreen } from '@app/settings/shortcuts';
import { SpaceScreen } from '@app/space';
import { TrendingScreen } from '@app/trending';
import { UserScreen } from '@app/users';
import { useStorage } from '@libs/storage/provider'; import { AppLayout } from '@shared/appLayout';
import { AuthLayout } from '@shared/authLayout';
import { Protected } from '@shared/protected';
import { SettingsLayout } from '@shared/settingsLayout';
import { LoaderIcon } from '@shared/icons'; import './index.css';
import { AppLayout } from '@shared/layouts/app';
import { AuthLayout } from '@shared/layouts/auth';
import { NoteLayout } from '@shared/layouts/note';
import { SettingsLayout } from '@shared/layouts/settings';
import './app.css';
export default function App() {
const { db } = useStorage();
const accountLoader = async () => {
try {
// redirect to welcome screen if none user exist
const totalAccount = await db.checkAccount();
if (totalAccount === 0) return redirect('/auth/welcome');
return null;
} catch (e) {
await message(e, { title: 'An unexpected error has occurred', type: 'error' });
}
};
const relayLoader = async ({ params }) => {
return defer({
relay: fetch(`https://${params.url}`, {
method: 'GET',
headers: {
Accept: 'application/nostr+json',
},
}).then((res) => res.json()),
});
};
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: '/', path: '/',
element: <AppLayout />,
errorElement: <ErrorScreen />,
loader: accountLoader,
children: [
{
path: '',
async lazy() {
const { SpaceScreen } = await import('@app/space');
return { Component: SpaceScreen };
},
},
{
path: 'users/:pubkey',
async lazy() {
const { UserScreen } = await import('@app/users');
return { Component: UserScreen };
},
},
{
path: 'nwc',
async lazy() {
const { NWCScreen } = await import('@app/nwc');
return { Component: NWCScreen };
},
},
{
path: 'relays',
async lazy() {
const { RelaysScreen } = await import('@app/relays');
return { Component: RelaysScreen };
},
},
{
path: 'relays/:url',
loader: relayLoader,
async lazy() {
const { RelayScreen } = await import('@app/relays/relay');
return { Component: RelayScreen };
},
},
{
path: 'explore',
element: ( element: (
<ReactFlowProvider> <Protected>
<ExploreScreen /> <Root />
</ReactFlowProvider> </Protected>
), ),
errorElement: <ErrorScreen />, errorElement: <ErrorScreen />,
}, },
{
path: 'chats',
element: <ChatsScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: 'chat/:pubkey',
async lazy() {
const { ChatScreen } = await import('@app/chats/chat');
return { Component: ChatScreen };
},
},
],
},
],
},
{
path: '/new',
element: <NewScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { NewPostScreen } = await import('@app/new/post');
return { Component: NewPostScreen };
},
},
{
path: 'article',
async lazy() {
const { NewArticleScreen } = await import('@app/new/article');
return { Component: NewArticleScreen };
},
},
{
path: 'file',
async lazy() {
const { NewFileScreen } = await import('@app/new/file');
return { Component: NewFileScreen };
},
},
],
},
{
path: '/notes',
element: <NoteLayout />,
errorElement: <ErrorScreen />,
children: [
{
path: 'text/:id',
async lazy() {
const { TextNoteScreen } = await import('@app/notes/text');
return { Component: TextNoteScreen };
},
},
{
path: 'article/:id',
async lazy() {
const { ArticleNoteScreen } = await import('@app/notes/article');
return { Component: ArticleNoteScreen };
},
},
],
},
{ {
path: '/auth', path: '/auth',
element: <AuthLayout />, element: <AuthLayout />,
errorElement: <ErrorScreen />,
children: [ children: [
{ path: 'welcome', element: <WelcomeScreen /> },
{ path: 'onboarding', element: <OnboardingScreen /> },
{ {
path: 'welcome', path: 'import',
async lazy() { element: <AuthImportScreen />,
const { WelcomeScreen } = await import('@app/auth/welcome'); children: [
return { Component: WelcomeScreen }; { path: '', element: <ImportStep1Screen /> },
}, { path: 'step-2', element: <ImportStep2Screen /> },
{ path: 'step-3', element: <ImportStep3Screen /> },
],
}, },
{ {
path: 'create', path: 'create',
async lazy() { element: <AuthCreateScreen />,
const { CreateAccountScreen } = await import('@app/auth/create');
return { Component: CreateAccountScreen };
},
},
{
path: 'import',
async lazy() {
const { ImportAccountScreen } = await import('@app/auth/import');
return { Component: ImportAccountScreen };
},
},
{
path: 'onboarding',
element: <OnboardingScreen />,
errorElement: <ErrorScreen />,
children: [ children: [
{ { path: '', element: <CreateStep1Screen /> },
path: '', { path: 'step-2', element: <CreateStep2Screen /> },
async lazy() { { path: 'step-3', element: <CreateStep3Screen /> },
const { OnboardingListScreen } = await import( { path: 'step-4', element: <CreateStep4Screen /> },
'@app/auth/onboarding/list' { path: 'step-5', element: <CreateStep5Screen /> },
);
return { Component: OnboardingListScreen };
},
},
{
path: 'enrich',
async lazy() {
const { OnboardEnrichScreen } = await import(
'@app/auth/onboarding/enrich'
);
return { Component: OnboardEnrichScreen };
},
},
{
path: 'hashtag',
async lazy() {
const { OnboardHashtagScreen } = await import(
'@app/auth/onboarding/hashtag'
);
return { Component: OnboardHashtagScreen };
},
},
], ],
}, },
{ path: 'unlock', element: <UnlockScreen /> },
{ path: 'migrate', element: <MigrateScreen /> },
{ path: 'reset', element: <ResetScreen /> },
],
},
{
path: '/app',
element: (
<Protected>
<AppLayout />
</Protected>
),
children: [
{ path: 'space', element: <SpaceScreen /> },
{ path: 'trending', element: <TrendingScreen /> },
{ path: 'note/:id', element: <NoteScreen /> },
{ path: 'users/:pubkey', element: <UserScreen /> },
{ path: 'chats/:pubkey', element: <ChatScreen /> },
{ path: 'channel/:id', element: <ChannelScreen /> },
], ],
}, },
{ {
path: '/settings', path: '/settings',
element: <SettingsLayout />, element: (
errorElement: <ErrorScreen />, <Protected>
<SettingsLayout />
</Protected>
),
children: [ children: [
{ { path: 'general', element: <GeneralSettingsScreen /> },
path: '', { path: 'shortcuts', element: <ShortcutsSettingsScreen /> },
async lazy() { { path: 'account', element: <AccountSettingsScreen /> },
const { GeneralSettingsScreen } = await import('@app/settings/general');
return { Component: GeneralSettingsScreen };
},
},
{
path: 'backup',
async lazy() {
const { AccountSettingsScreen } = await import('@app/settings/account');
return { Component: AccountSettingsScreen };
},
},
], ],
}, },
]); ]);
export default function App() {
return ( return (
<RouterProvider <RouterProvider
router={router} router={router}
fallbackElement={ fallbackElement={<p>Loading..</p>}
<div className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
</div>
}
future={{ v7_startTransition: true }} future={{ v7_startTransition: true }}
/> />
); );
@@ -1,50 +0,0 @@
import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function AllowNotification() {
const [notification, setNotification] = useOnboarding((state) => [
state.notification,
state.toggleNotification,
]);
const allow = async () => {
let permissionGranted = await isPermissionGranted();
if (!permissionGranted) {
const permission = await requestPermission();
permissionGranted = permission === 'granted';
}
if (permissionGranted) {
setNotification();
}
};
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between gap-2">
<div>
<h5 className="font-semibold">Allow notification</h5>
<p className="text-sm">
By allowing Lume to send notifications in your OS settings, you will receive
notification messages when someone interacts with you or your content.
</p>
</div>
{notification ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<button
type="button"
onClick={allow}
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Allow
</button>
)}
</div>
</div>
);
}
@@ -1,100 +0,0 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { LRUCache } from 'lru-cache';
import { useState } from 'react';
import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { CheckCircleIcon, LoaderIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function Circle() {
const { db } = useStorage();
const { ndk } = useNDK();
const [circle, setCircle] = useOnboarding((state) => [
state.circle,
state.toggleCircle,
]);
const [loading, setLoading] = useState(false);
const enableLinks = async () => {
setLoading(true);
const users = ndk.getUser({ hexpubkey: db.account.pubkey });
const follows = await users.follows();
if (follows.size === 0) {
setLoading(false);
return toast('You need to follow at least 1 account');
}
const lru = new LRUCache<string, string, void>({ max: 300 });
const followsAsArr = [];
// add user's follows to lru
follows.forEach((user) => {
lru.set(user.pubkey, user.pubkey);
followsAsArr.push(user.pubkey);
});
// get follows from follows
const events = await ndk.fetchEvents({
kinds: [NDKKind.Contacts],
authors: followsAsArr,
limit: 300,
});
events.forEach((event: NDKEvent) => {
event.tags.forEach((tag) => {
if (tag[0] === 'p') lru.set(tag[1], tag[1]);
});
});
// get lru values
const circleList = [...lru.values()] as string[];
// update db
await db.updateAccount('follows', JSON.stringify(followsAsArr));
await db.updateAccount('circles', JSON.stringify(circleList));
db.account.follows = followsAsArr;
db.account.circles = circleList;
// clear lru
lru.clear();
// done
await db.createSetting('circles', '1');
setCircle();
};
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between gap-2">
<div>
<h5 className="font-semibold">Enable Circle</h5>
<p className="text-sm">
Beside newsfeed from your follows, you will see more content from all people
that followed by your follows.
</p>
</div>
{circle ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<button
type="button"
onClick={enableLinks}
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : 'Enable'}
</button>
)}
</div>
</div>
);
}
@@ -1,47 +0,0 @@
import { useStorage } from '@libs/storage/provider';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function OutboxModel() {
const { db } = useStorage();
const [outbox, setOutbox] = useOnboarding((state) => [
state.outbox,
state.toggleOutbox,
]);
const enableOutbox = async () => {
await db.createSetting('outbox', '1');
setOutbox();
};
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between gap-2">
<div>
<h5 className="font-semibold">Enable Outbox (experiment)</h5>
<p className="text-sm">
When you request information about a user, Lume will automatically query the
user&apos;s outbox relays and subsequent queries will favour using those
relays for queries with that user&apos;s pubkey.
</p>
</div>
{outbox ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<button
type="button"
onClick={enableOutbox}
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Enable
</button>
)}
</div>
</div>
);
}
@@ -1,35 +0,0 @@
import { Link } from 'react-router-dom';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function FavoriteHashtag() {
const hashtag = useOnboarding((state) => state.hashtag);
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between">
<div>
<h5 className="font-semibold">Favorite hashtag</h5>
<p className="text-sm">
By adding favorite hashtag, Lume will display all contents related to this
hashtag as a column
</p>
</div>
{hashtag ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<Link
to="/auth/onboarding/hashtag"
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Add
</Link>
)}
</div>
</div>
);
}
@@ -1,56 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { LoaderIcon } from '@shared/icons';
import { User } from '@shared/user';
export function FollowList() {
const { db } = useStorage();
const { ndk } = useNDK();
const { status, data } = useQuery({
queryKey: ['follows'],
queryFn: async () => {
const user = ndk.getUser({ hexpubkey: db.account.pubkey });
const follows = await user.follows();
const followsAsArr = [];
follows.forEach((user) => {
followsAsArr.push(user.pubkey);
});
// update db
await db.updateAccount('follows', JSON.stringify(followsAsArr));
await db.updateAccount('circles', JSON.stringify(followsAsArr));
db.account.follows = followsAsArr;
db.account.circles = followsAsArr;
return followsAsArr;
},
refetchOnWindowFocus: false,
});
return (
<div className="relative rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<h5 className="font-semibold">Your follows</h5>
<div className="mt-2 flex w-full items-center justify-center">
{status === 'pending' ? (
<LoaderIcon className="h-4 w-4 animate-spin text-neutral-900 dark:text-neutral-100" />
) : (
<div className="isolate flex -space-x-2">
{data.slice(0, 16).map((item) => (
<User key={item} pubkey={item} variant="stacked" />
))}
{data.length > 16 ? (
<div className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-900 ring-1 ring-neutral-200 dark:bg-neutral-800 dark:text-neutral-100 dark:ring-neutral-800">
<span className="text-xs font-medium">+{data.length}</span>
</div>
) : null}
</div>
)}
</div>
</div>
);
}
@@ -1,35 +0,0 @@
import { Link } from 'react-router-dom';
import { CheckCircleIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
export function SuggestFollow() {
const enrich = useOnboarding((state) => state.enrich);
return (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex items-start justify-between">
<div>
<h5 className="font-semibold">Enrich your network</h5>
<p className="text-sm">
Follow more people to stay up to date with everything happening around the
world.
</p>
</div>
{enrich ? (
<div className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-teal-500 text-white">
<CheckCircleIcon className="h-4 w-4" />
</div>
) : (
<Link
to="/auth/onboarding/enrich"
className="mt-1 inline-flex h-9 w-24 shrink-0 items-center justify-center rounded-lg bg-neutral-200 font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
>
Check
</Link>
)}
</div>
</div>
);
}
+43
View File
@@ -0,0 +1,43 @@
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }) {
const { status, user } = useProfile(pubkey, fallback);
if (status === 'loading') {
return (
<div className="flex items-center gap-2">
<div className="relative h-10 w-10 shrink-0 animate-pulse rounded-md bg-zinc-800" />
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
<span className="h-4 w-1/2 animate-pulse rounded bg-zinc-800" />
<span className="h-3 w-1/3 animate-pulse rounded bg-zinc-800" />
</div>
</div>
);
}
return (
<div className="flex items-center gap-2">
<div className="relative h-10 w-10 shrink rounded-md">
<Image
src={user?.picture || user?.image}
fallback={DEFAULT_AVATAR}
alt={pubkey}
className="h-10 w-10 rounded-md object-cover"
/>
</div>
<div className="flex w-full flex-1 flex-col items-start text-start">
<span className="truncate font-medium leading-tight text-zinc-100">
{user?.name || user?.displayName || user?.display_name}
</span>
<span className="max-w-[15rem] truncate text-base leading-tight text-zinc-400">
{user?.nip05?.toLowerCase() || shortenKey(pubkey)}
</span>
</div>
</div>
);
}
-305
View File
@@ -1,305 +0,0 @@
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { downloadDir } from '@tauri-apps/api/path';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { message, save } from '@tauri-apps/plugin-dialog';
import { writeTextFile } from '@tauri-apps/plugin-fs';
import { motion } from 'framer-motion';
import { minidenticon } from 'minidenticons';
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { AvatarUploader } from '@shared/avatarUploader';
import { ArrowLeftIcon, LoaderIcon } from '@shared/icons';
import { User } from '@shared/user';
export function CreateAccountScreen() {
const [picture, setPicture] = useState('');
const [downloaded, setDownloaded] = useState(false);
const [loading, setLoading] = useState(false);
const [keys, setKeys] = useState<null | {
npub: string;
nsec: string;
pubkey: string;
privkey: string;
}>(null);
const {
register,
handleSubmit,
formState: { isDirty, isValid },
} = useForm();
const { db } = useStorage();
const { ndk } = useNDK();
const navigate = useNavigate();
const svgURI =
'data:image/svg+xml;utf8,' +
encodeURIComponent(minidenticon('lume new account', 90, 50));
const onSubmit = async (data: { name: string; about: string }) => {
try {
setLoading(true);
const profile = {
...data,
name: data.name,
display_name: data.name,
bio: data.about,
picture: picture,
avatar: picture,
};
const userPrivkey = generatePrivateKey();
const userPubkey = getPublicKey(userPrivkey);
const userNpub = nip19.npubEncode(userPubkey);
const userNsec = nip19.nsecEncode(userPrivkey);
const signer = new NDKPrivateKeySigner(userPrivkey);
ndk.signer = signer;
const event = new NDKEvent(ndk);
event.content = JSON.stringify(profile);
event.kind = NDKKind.Metadata;
event.created_at = Math.floor(Date.now() / 1000);
event.pubkey = userPubkey;
event.tags = [];
const publish = await event.publish();
if (publish) {
await db.createAccount(userNpub, userPubkey);
await db.secureSave(userPubkey, userPrivkey);
setKeys({
npub: userNpub,
nsec: userNsec,
pubkey: userPubkey,
privkey: userPrivkey,
});
setLoading(false);
} else {
toast('Create account failed');
setLoading(false);
}
} catch (e) {
return toast(e);
}
};
const copyNsec = async () => {
await writeText(keys.nsec);
};
const download = async () => {
try {
const downloadPath = await downloadDir();
const fileName = `nostr_keys_${new Date().toISOString()}.txt`;
const filePath = await save({
defaultPath: downloadPath + '/' + fileName,
});
if (filePath) {
await writeTextFile(
filePath,
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${keys.npub}\nPrivate key: ${keys.nsec}`
);
setDownloaded(true);
} // else { user cancel action }
} catch (e) {
await message(e, { title: 'Cannot download account keys', type: 'error' });
}
};
return (
<div className="relative flex h-full w-full items-center justify-center">
<div className="absolute left-[8px] top-2">
{!keys ? (
<button
onClick={() => navigate(-1)}
className="inline-flex items-center gap-2 text-sm font-medium"
>
<div className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
<ArrowLeftIcon className="h-5 w-5" />
</div>
Back
</button>
) : null}
</div>
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
Let&apos;s set up your account.
</h1>
<div className="flex flex-col gap-3">
{!keys ? (
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
<input type={'hidden'} {...register('picture')} value={picture} />
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<span className="font-semibold">Avatar</span>
<div className="relative flex h-36 w-full items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-800">
{picture.length > 0 ? (
<img
src={picture}
alt="user's avatar"
className="h-14 w-14 rounded-xl"
/>
) : (
<img
src={svgURI}
alt="user's avatar"
className="h-14 w-14 rounded-xl bg-black dark:bg-white"
/>
)}
<div className="absolute bottom-2 right-2">
<AvatarUploader setPicture={setPicture} />
</div>
</div>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="name" className="font-semibold">
Name *
</label>
<input
type={'text'}
{...register('name', {
required: true,
minLength: 1,
})}
spellCheck={false}
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="about" className="font-semibold">
Bio
</label>
<textarea
{...register('about')}
spellCheck={false}
className="relative h-20 w-full resize-none rounded-lg bg-neutral-200 px-3 py-2 !outline-none placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
/>
</div>
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="h-5 w-4 animate-spin" />
) : (
'Create and Continue'
)}
</button>
</div>
</form>
</div>
) : (
<>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{
opacity: 1,
y: 0,
}}
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
>
<User pubkey={keys.pubkey} variant="simple" />
</motion.div>
<motion.div
initial={{ opacity: 0, y: 80 }}
animate={{
opacity: 1,
y: 0,
}}
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
>
<div className="flex flex-col gap-1.5">
<h5 className="font-semibold">Backup account</h5>
<div>
<p className="mb-2 select-text text-sm text-neutral-800 dark:text-neutral-200">
Your private key is your password. If you lose this key, you will
lose access to your account! Copy it and keep it in a safe place.{' '}
<span className="text-red-600">
There is no way to reset your private key.
</span>
</p>
<p className="select-text text-sm text-neutral-800 dark:text-neutral-200">
Public key is used for sharing with other people so that they can
find you using the public key.
</p>
</div>
<div className="mt-3 flex flex-col gap-3">
<div className="flex flex-col gap-1">
<label htmlFor="nsec" className="text-sm font-semibold">
Private key
</label>
<div className="relative w-full">
<input
readOnly
value={
keys.nsec.substring(0, 10) + '**************************'
}
className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
/>
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
<button
type="button"
onClick={copyNsec}
className="rounded-md bg-neutral-300 px-2 py-1 text-sm font-medium hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600"
>
Copy
</button>
</div>
</div>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="nsec" className="text-sm font-semibold">
Public key
</label>
<input
readOnly
value={keys.npub}
className="h-11 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
/>
</div>
</div>
{!downloaded ? (
<button
type="button"
onClick={() => download()}
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
>
Download account keys
</button>
) : null}
</div>
</motion.div>
</>
)}
{downloaded ? (
<motion.button
initial={{ opacity: 0, y: 50 }}
animate={{
opacity: 1,
y: 0,
}}
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
type="button"
onClick={() => navigate('/auth/onboarding', { state: { newuser: true } })}
>
Finish
</motion.button>
) : null}
</div>
</div>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { Outlet } from 'react-router-dom';
export function AuthCreateScreen() {
return (
<div className="flex h-full w-full items-center justify-center">
<Outlet />
</div>
);
}
+151
View File
@@ -0,0 +1,151 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { BaseDirectory, writeTextFile } from '@tauri-apps/api/fs';
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { createAccount } from '@libs/storage';
import { Button } from '@shared/button';
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
import { useStronghold } from '@stores/stronghold';
export function CreateStep1Screen() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const setPrivkey = useStronghold((state) => state.setPrivkey);
const setPubkey = useOnboarding((state) => state.setPubkey);
const [privkeyInput, setPrivkeyInput] = useState('password');
const [loading, setLoading] = useState(false);
const [downloaded, setDownloaded] = useState(false);
const privkey = useMemo(() => generatePrivateKey(), []);
const pubkey = getPublicKey(privkey);
const npub = nip19.npubEncode(pubkey);
const nsec = nip19.nsecEncode(privkey);
// toggle private key
const showPrivateKey = () => {
if (privkeyInput === 'password') {
setPrivkeyInput('text');
} else {
setPrivkeyInput('password');
}
};
const download = async () => {
await writeTextFile(
'lume-keys.txt',
`Public key: ${pubkey}\nPrivate key: ${privkey}`,
{
dir: BaseDirectory.Download,
}
);
setDownloaded(true);
};
const account = useMutation({
mutationFn: (data: {
npub: string;
pubkey: string;
follows: null | string[][];
is_active: number;
}) => {
return createAccount(data.npub, data.pubkey, null, 1);
},
onSuccess: (data) => {
queryClient.setQueryData(['currentAccount'], data);
},
});
const submit = () => {
setLoading(true);
setPubkey(pubkey);
setPrivkey(privkey);
account.mutate({
npub,
pubkey,
follows: null,
is_active: 1,
});
// redirect to next step
setTimeout(() => navigate('/auth/create/step-2', { replace: true }), 1200);
};
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">Save your access key!</h1>
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<span className="text-base font-semibold text-zinc-400">Public Key</span>
<input
readOnly
value={npub}
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
/>
</div>
<div className="flex flex-col gap-1">
<span className="text-base font-semibold text-zinc-400">Private Key</span>
<div className="relative">
<input
readOnly
type={privkeyInput}
value={nsec}
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
/>
<button
type="button"
onClick={() => showPrivateKey()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
>
{privkeyInput === 'password' ? (
<EyeOffIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
) : (
<EyeOnIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
)}
</button>
</div>
<div className="mt-2 text-sm text-zinc-500">
<p>
Your private key is your password. If you lose this key, you will lose
access to your account! Copy it and keep it in a safe place. There is no way
to reset your private key.
</p>
</div>
</div>
<div className="flex flex-col gap-2">
<Button preset="large" onClick={() => submit()}>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : (
'I have saved my key, continue →'
)}
</Button>
{downloaded ? (
<span className="text-sm text-zinc-400">Saved in download folder</span>
) : (
<Button preset="large-alt" onClick={() => download()}>
Download
</Button>
)}
</div>
</div>
</div>
);
}
+138
View File
@@ -0,0 +1,138 @@
import { useState } from 'react';
import { Resolver, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
import { useOnboarding } from '@stores/onboarding';
import { useStronghold } from '@stores/stronghold';
import { useSecureStorage } from '@utils/hooks/useSecureStorage';
type FormValues = {
password: string;
};
const resolver: Resolver<FormValues> = async (values) => {
return {
values: values.password ? values : {},
errors: !values.password
? {
password: {
type: 'required',
message: 'This is required.',
},
}
: {},
};
};
export function CreateStep2Screen() {
const navigate = useNavigate();
const [passwordInput, setPasswordInput] = useState('password');
const [loading, setLoading] = useState(false);
const privkey = useStronghold((state) => state.privkey);
const pubkey = useOnboarding((state) => state.pubkey);
const { save } = useSecureStorage();
// toggle private key
const showPassword = () => {
if (passwordInput === 'password') {
setPasswordInput('text');
} else {
setPasswordInput('password');
}
};
const {
register,
setError,
handleSubmit,
formState: { errors, isDirty, isValid },
} = useForm<FormValues>({ resolver });
const onSubmit = async (data: { [x: string]: string }) => {
setLoading(true);
if (data.password.length > 3) {
// save privkey to secure storage
await save(pubkey, privkey, data.password);
// redirect to next step
navigate('/auth/create/step-3', { replace: true });
} else {
setLoading(false);
setError('password', {
type: 'custom',
message: 'Password is required and must be greater than 3',
});
}
};
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-8 text-center">
<h1 className="text-xl font-semibold text-zinc-100">
Set password to secure your key
</h1>
</div>
<div className="flex flex-col gap-4">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<div className="relative">
<input
{...register('password', { required: true })}
type={passwordInput}
className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
/>
<button
type="button"
onClick={() => showPassword()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
>
{passwordInput === 'password' ? (
<EyeOffIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
) : (
<EyeOnIcon
width={20}
height={20}
className="text-zinc-500 group-hover:text-zinc-100"
/>
)}
</button>
</div>
<div className="text-sm text-zinc-500">
<p>
Password is use to secure your key store in local machine, when you move
to other clients, you just need to copy your private key as nsec or
hexstring
</p>
</div>
<span className="text-sm text-red-400">
{errors.password && <p>{errors.password.message}</p>}
</span>
</div>
<div className="flex items-center justify-center">
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600 disabled:pointer-events-none disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
) : (
'Continue →'
)}
</button>
</div>
</form>
</div>
</div>
);
}

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