@ -1,11 +1,11 @@
|
||||
<html lang="en" class="dark">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lume</title>
|
||||
</head>
|
||||
<body class="relative cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen text-white">
|
||||
<body class="relative cursor-default select-none overflow-hidden font-sans antialiased h-screen w-screen text-neutral-950 dark:text-neutral-50">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
82
package.json
@ -18,10 +18,11 @@
|
||||
"**/*.{ts, tsx, css, md, html, json}": "prettier --cache --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@getalby/sdk": "^2.4.0",
|
||||
"@nostr-dev-kit/ndk": "^1.3.1",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "^1.3.0",
|
||||
"@evilmartians/harmony": "^1.1.0",
|
||||
"@formkit/auto-animate": "^0.8.0",
|
||||
"@getalby/sdk": "^2.5.0",
|
||||
"@nostr-dev-kit/ndk": "^2.0.3",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "^2.0.3",
|
||||
"@nostr-fetch/adapter-ndk": "^0.12.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
@ -31,61 +32,76 @@
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/react-query": "^4.35.7",
|
||||
"@tauri-apps/api": "^1.5.0",
|
||||
"@tiptap/extension-image": "^2.1.11",
|
||||
"@tiptap/extension-mention": "^2.1.11",
|
||||
"@tiptap/extension-placeholder": "^2.1.11",
|
||||
"@tiptap/pm": "^2.1.11",
|
||||
"@tiptap/react": "^2.1.11",
|
||||
"@tiptap/starter-kit": "^2.1.11",
|
||||
"@tiptap/suggestion": "^2.1.11",
|
||||
"@vidstack/react": "^1.1.7",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tauri-apps/api": "2.0.0-alpha.8",
|
||||
"@tauri-apps/cli": "2.0.0-alpha.15",
|
||||
"@tauri-apps/plugin-app": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-dialog": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-fs": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-http": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-notification": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-os": "2.0.0-alpha.2",
|
||||
"@tauri-apps/plugin-process": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-sql": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-updater": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-upload": "2.0.0-alpha.1",
|
||||
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
|
||||
"@tiptap/extension-image": "^2.1.12",
|
||||
"@tiptap/extension-mention": "^2.1.12",
|
||||
"@tiptap/extension-placeholder": "^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.1",
|
||||
"framer-motion": "^10.16.4",
|
||||
"html-to-text": "^9.0.5",
|
||||
"light-bolt11-decoder": "^3.0.0",
|
||||
"lru-cache": "^10.0.1",
|
||||
"media-chrome": "^1.4.4",
|
||||
"million": "^2.6.4",
|
||||
"minidenticons": "^4.2.0",
|
||||
"nostr-fetch": "^0.13.0",
|
||||
"nostr-tools": "^1.16.0",
|
||||
"nostr-tools": "^1.17.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"re-resizable": "^6.9.11",
|
||||
"react": "^18.2.0",
|
||||
"react-currency-input-field": "^3.6.11",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.47.0",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"reactflow": "^11.9.2",
|
||||
"react-router-dom": "^6.17.0",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"reactflow": "^11.9.4",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql#v1",
|
||||
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1",
|
||||
"tauri-plugin-stronghold-api": "github:tauri-apps/tauri-plugin-stronghold#v1",
|
||||
"tauri-plugin-upload-api": "github:tauri-apps/tauri-plugin-upload#v1",
|
||||
"sonner": "^1.0.3",
|
||||
"tailwind-scrollbar": "^3.0.5",
|
||||
"tauri-controls": "^0.2.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"virtua": "^0.9.1",
|
||||
"zustand": "^4.4.2"
|
||||
"virtua": "^0.13.0",
|
||||
"zustand": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@tauri-apps/cli": "^1.5.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||
"@types/html-to-text": "^9.0.2",
|
||||
"@types/node": "^20.8.2",
|
||||
"@types/react": "^18.2.24",
|
||||
"@types/react-dom": "^18.2.8",
|
||||
"@types/node": "^20.8.6",
|
||||
"@types/react": "^18.2.28",
|
||||
"@types/react-dom": "^18.2.13",
|
||||
"@types/youtube-player": "^5.5.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||
"@typescript-eslint/parser": "^6.8.0",
|
||||
"@vitejs/plugin-react-swc": "^3.4.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"clsx": "^2.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"csstype": "^3.1.2",
|
||||
"encoding": "^0.1.13",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
@ -94,12 +110,12 @@
|
||||
"lint-staged": "^14.0.1",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.5",
|
||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||
"prop-types": "^15.8.1",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.10",
|
||||
"vite": "^4.4.11",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
}
|
||||
}
|
||||
|
2148
pnpm-lock.yaml
generated
BIN
public/icon.png
Normal file
After ![]() (image error) Size: 29 KiB |
BIN
public/lume.png
Before ![]() (image error) Size: 55 KiB |
2000
src-tauri/Cargo.lock
generated
@ -8,56 +8,40 @@ repository = "https://github.com/luminous-devs/lume"
|
||||
edition = "2021"
|
||||
rust-version = "1.66"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
tauri-build = { version = "2.0.0-alpha", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.5", features = [
|
||||
tauri = { version = "2.0.0-alpha", features = [
|
||||
"macos-private-api",
|
||||
"window-close",
|
||||
"window-print",
|
||||
"window-create",
|
||||
"fs-read-dir",
|
||||
"fs-read-file",
|
||||
"window-start-dragging",
|
||||
"path-all",
|
||||
"http-all",
|
||||
"clipboard-write-text",
|
||||
"os-all",
|
||||
"notification-all",
|
||||
"clipboard-read-text",
|
||||
"window-set-resizable",
|
||||
"window-set-size",
|
||||
"shell-open",
|
||||
"fs-write-file",
|
||||
"app-all",
|
||||
"fs-remove-file",
|
||||
"window-center",
|
||||
"dialog-all",
|
||||
"http-multipart",
|
||||
"native-tls-vendored",
|
||||
] }
|
||||
tauri-plugin-sql = { git = "hhttps://github.com/tauri-apps/plugins-workspace", branch = "v1", features = [
|
||||
tauri-plugin-app = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-cli = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-window = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-sql = { git = "hhttps://github.com/tauri-apps/plugins-workspace", branch = "v2", features = [
|
||||
"sqlite",
|
||||
] }
|
||||
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-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
window-vibrancy = { git = "https://github.com/tauri-apps/window-vibrancy", branch = "dev" }
|
||||
sqlx-cli = { version = "0.7.0", default-features = false, features = [
|
||||
"sqlite",
|
||||
] }
|
||||
rust-argon2 = "1.0"
|
||||
webpage = { version = "1.6.0", features = ["serde"] }
|
||||
|
||||
[target.'cfg(any(target_os = "macos"))'.dependencies]
|
||||
cocoa = "0.25.0"
|
||||
objc = "0.2.7"
|
||||
keyring = "2"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
Before ![]() (image error) Size: 5.6 KiB After ![]() (image error) Size: 4.9 KiB ![]() ![]() |
Before ![]() (image error) Size: 12 KiB After ![]() (image error) Size: 11 KiB ![]() ![]() |
Before ![]() (image error) Size: 1.2 KiB After ![]() (image error) Size: 1.0 KiB ![]() ![]() |
Before ![]() (image error) Size: 4.4 KiB After ![]() (image error) Size: 3.9 KiB ![]() ![]() |
Before ![]() (image error) Size: 6.2 KiB After ![]() (image error) Size: 5.6 KiB ![]() ![]() |
Before ![]() (image error) Size: 6.6 KiB After ![]() (image error) Size: 5.9 KiB ![]() ![]() |
Before ![]() (image error) Size: 14 KiB After ![]() (image error) Size: 12 KiB ![]() ![]() |
Before ![]() (image error) Size: 1.1 KiB After ![]() (image error) Size: 1.0 KiB ![]() ![]() |
Before ![]() (image error) Size: 16 KiB After ![]() (image error) Size: 14 KiB ![]() ![]() |
Before ![]() (image error) Size: 1.7 KiB After ![]() (image error) Size: 1.4 KiB ![]() ![]() |
Before ![]() (image error) Size: 2.8 KiB After ![]() (image error) Size: 2.5 KiB ![]() ![]() |
Before ![]() (image error) Size: 3.6 KiB After ![]() (image error) Size: 3.2 KiB ![]() ![]() |
Before ![]() (image error) Size: 1.9 KiB After ![]() (image error) Size: 1.8 KiB ![]() ![]() |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 1.7 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After ![]() (image error) Size: 6.4 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
After ![]() (image error) Size: 1.7 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 1.7 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After ![]() (image error) Size: 4.0 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
After ![]() (image error) Size: 1.7 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 3.4 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After ![]() (image error) Size: 8.9 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
After ![]() (image error) Size: 3.4 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 5.6 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
After ![]() (image error) Size: 15 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After ![]() (image error) Size: 5.6 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
After ![]() (image error) Size: 7.8 KiB |
After ![]() (image error) Size: 22 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After ![]() (image error) Size: 7.8 KiB |
Before (image error) Size: 20 KiB After (image error) Size: 18 KiB |
Before ![]() (image error) Size: 29 KiB After ![]() (image error) Size: 29 KiB ![]() ![]() |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
After ![]() (image error) Size: 679 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
After ![]() (image error) Size: 1.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
After ![]() (image error) Size: 1.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
After ![]() (image error) Size: 2.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
After ![]() (image error) Size: 981 B |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
After ![]() (image error) Size: 1.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
After ![]() (image error) Size: 1.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
After ![]() (image error) Size: 3.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
After ![]() (image error) Size: 1.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
After ![]() (image error) Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
After ![]() (image error) Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
After ![]() (image error) Size: 4.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
After ![]() (image error) Size: 55 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
After ![]() (image error) Size: 4.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
After ![]() (image error) Size: 6.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
After ![]() (image error) Size: 2.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
After ![]() (image error) Size: 5.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
After ![]() (image error) Size: 5.9 KiB |
@ -1,45 +1,29 @@
|
||||
-- Add migration script here
|
||||
-- create accounts table
|
||||
-- is_active (multi-account feature), value:
|
||||
-- 0: false
|
||||
-- 1: true
|
||||
CREATE TABLE
|
||||
accounts (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
npub TEXT NOT NULL UNIQUE,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
pubkey TEXT NOT NULL UNIQUE,
|
||||
privkey TEXT NOT NULL,
|
||||
follows JSON,
|
||||
follows TEXT,
|
||||
circles TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 0,
|
||||
last_login_at NUMBER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- create notes table
|
||||
CREATE TABLE
|
||||
notes (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
event_id TEXT NOT NULL UNIQUE,
|
||||
events (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL,
|
||||
pubkey TEXT NOT NULL,
|
||||
kind INTEGER NOT NULL DEFAULT 1,
|
||||
tags JSON,
|
||||
content TEXT NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
kind NUMBER NOT NULL DEFAULt 1,
|
||||
root_id TEXT,
|
||||
reply_id TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
parent_id TEXT,
|
||||
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 TABLE
|
||||
settings (
|
||||
@ -49,11 +33,23 @@ CREATE TABLE
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- create metadata table
|
||||
CREATE TABLE
|
||||
metadata (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
pubkey TEXT NOT NULL,
|
||||
widgets (
|
||||
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
|
||||
);
|
||||
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)
|
||||
);
|
||||
|
@ -1,12 +0,0 @@
|
||||
-- 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
|
||||
);
|
@ -1,14 +0,0 @@
|
||||
-- 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")
|
@ -1,3 +0,0 @@
|
||||
-- Add migration script here
|
||||
-- add pubkey to channel
|
||||
ALTER TABLE channels ADD pubkey TEXT NOT NULL DEFAULT '';
|
@ -1,38 +0,0 @@
|
||||
-- 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
|
||||
);
|
@ -1,11 +0,0 @@
|
||||
-- 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)
|
||||
);
|
@ -1,11 +0,0 @@
|
||||
-- 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)
|
||||
);
|
@ -1,15 +0,0 @@
|
||||
-- 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)
|
||||
);
|
@ -1,13 +0,0 @@
|
||||
-- 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)
|
||||
);
|
@ -1,6 +0,0 @@
|
||||
-- Add migration script here
|
||||
DROP TABLE IF EXISTS blacklist;
|
||||
|
||||
DROP TABLE IF EXISTS channel_messages;
|
||||
|
||||
DROP TABLE IF EXISTS channels;
|
@ -1,6 +0,0 @@
|
||||
-- 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,2 +0,0 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE accounts ADD network JSON;
|
@ -1,10 +0,0 @@
|
||||
-- Add migration script here
|
||||
CREATE TABLE
|
||||
relays (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL,
|
||||
relay TEXT NOT NULL,
|
||||
purpose TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||
);
|
@ -1,3 +0,0 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE blocks
|
||||
RENAME TO widgets;
|
@ -1,13 +0,0 @@
|
||||
-- Add migration script here
|
||||
CREATE TABLE
|
||||
events (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
kind NUMBER NOT NULL DEFAULt 1,
|
||||
root_id TEXT,
|
||||
reply_id TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts (id)
|
||||
);
|
@ -1,8 +0,0 @@
|
||||
-- Add migration script here
|
||||
DROP TABLE IF EXISTS notes;
|
||||
|
||||
DROP TABLE IF EXISTS chats;
|
||||
|
||||
DROP TABLE IF EXISTS metadata;
|
||||
|
||||
DROP TABLE IF EXISTS replies;
|
@ -1,3 +0,0 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE accounts
|
||||
ADD COLUMN last_login_at NUMBER NOT NULL DEFAULT 0;
|
@ -1,2 +0,0 @@
|
||||
-- Add migration script here
|
||||
CREATE UNIQUE INDEX unique_relay ON relays (relay);
|
@ -3,25 +3,13 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
use keyring::Entry;
|
||||
use std::time::Duration;
|
||||
use tauri::{Manager, WindowEvent};
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_sql::{Migration, MigrationKind};
|
||||
use webpage::{Webpage, WebpageOptions};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use traffic_light::TrafficLight;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod traffic_light;
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct Payload {
|
||||
args: Vec<String>,
|
||||
@ -94,175 +82,56 @@ async fn opengraph(url: String) -> OpenGraphResponse {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn close_splashscreen(window: tauri::Window) {
|
||||
// Close splashscreen
|
||||
if let Some(splashscreen) = window.get_window("splashscreen") {
|
||||
splashscreen.close().unwrap();
|
||||
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())
|
||||
}
|
||||
// Show main window
|
||||
window.get_window("main").unwrap().show().unwrap();
|
||||
}
|
||||
|
||||
#[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() {
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
let window = app.get_window("main").unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, None)
|
||||
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
window.position_traffic_lights(16.0, 25.0);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|e| {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let WindowEvent::Resized(..) = e.event() {
|
||||
let window = e.window();
|
||||
window.position_traffic_lights(16.0, 25.0);
|
||||
}
|
||||
})
|
||||
.plugin(tauri_plugin_app::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_upload::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_window::init())
|
||||
.plugin(
|
||||
tauri_plugin_sql::Builder::default()
|
||||
.add_migrations(
|
||||
"sqlite:lume.db",
|
||||
vec![
|
||||
Migration {
|
||||
version: 20230418013219,
|
||||
description: "initial data",
|
||||
sql: include_str!("../migrations/20230418013219_initial_data.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 20230418080146,
|
||||
description: "create chats",
|
||||
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,
|
||||
},
|
||||
Migration {
|
||||
version: 20230804083544,
|
||||
description: "add network to accounts",
|
||||
sql: include_str!("../migrations/20230804083544_add_network_to_account.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 20230808085847,
|
||||
description: "add relays",
|
||||
sql: include_str!("../migrations/20230808085847_add_relays_table.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 20230811074423,
|
||||
description: "rename blocks to widgets",
|
||||
sql: include_str!("../migrations/20230811074423_rename_blocks_to_widgets.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 20230814083543,
|
||||
description: "add events",
|
||||
sql: include_str!("../migrations/20230814083543_add_events_table.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 20230816090508,
|
||||
description: "clean up tables",
|
||||
sql: include_str!("../migrations/20230816090508_clean_up_tables.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 20230817014932,
|
||||
description: "add last login to account",
|
||||
sql: include_str!("../migrations/20230817014932_add_last_login_time_to_account.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 20230918235335,
|
||||
description: "add unique to relay",
|
||||
sql: include_str!("../migrations/20230918235335_add_uniq_to_relay.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
],
|
||||
"sqlite:lume_v2.db",
|
||||
vec![Migration {
|
||||
version: 20230418013219,
|
||||
description: "initial data",
|
||||
sql: include_str!("../migrations/20230418013219_initial_data.sql"),
|
||||
kind: MigrationKind::Up,
|
||||
}],
|
||||
)
|
||||
.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 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(
|
||||
MacosLauncher::LaunchAgent,
|
||||
Some(vec!["--flag1", "--flag2"]),
|
||||
@ -275,7 +144,12 @@ fn main() {
|
||||
}))
|
||||
.plugin(tauri_plugin_upload::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.invoke_handler(tauri::generate_handler![close_splashscreen, opengraph])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
opengraph,
|
||||
secure_save,
|
||||
secure_load,
|
||||
secure_remove
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
@ -1,60 +0,0 @@
|
||||
use tauri::{Runtime, Window};
|
||||
|
||||
pub trait TrafficLight {
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_transparent_titlebar(&self, transparent: bool);
|
||||
fn position_traffic_lights(&self, x: f64, y: f64);
|
||||
}
|
||||
|
||||
impl<R: Runtime> TrafficLight 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,76 +11,38 @@
|
||||
"productName": "Lume",
|
||||
"version": "1.2.7"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"app": {
|
||||
"all": true,
|
||||
"show": true,
|
||||
"hide": true
|
||||
},
|
||||
"path": {
|
||||
"all": true
|
||||
},
|
||||
"dialog": {
|
||||
"all": true,
|
||||
"ask": true,
|
||||
"confirm": true,
|
||||
"message": true,
|
||||
"open": true,
|
||||
"save": true
|
||||
},
|
||||
"fs": {
|
||||
"all": false,
|
||||
"removeFile": true,
|
||||
"writeFile": true,
|
||||
"readDir": true,
|
||||
"readFile": true,
|
||||
"scope": [
|
||||
"$APPDATA/*",
|
||||
"$DATA/*",
|
||||
"$LOCALDATA/*",
|
||||
"$DESKTOP/*",
|
||||
"$DOCUMENT/*",
|
||||
"$DOWNLOAD/*",
|
||||
"$HOME/*",
|
||||
"$PICTURE/*",
|
||||
"$PUBLIC/*",
|
||||
"$VIDEO/*"
|
||||
]
|
||||
},
|
||||
"http": {
|
||||
"all": true,
|
||||
"scope": [
|
||||
"http://**",
|
||||
"https://**"
|
||||
]
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
},
|
||||
"os": {
|
||||
"all": true
|
||||
},
|
||||
"window": {
|
||||
"all": false,
|
||||
"center": true,
|
||||
"setResizable": true,
|
||||
"setSize": true,
|
||||
"startDragging": true,
|
||||
"create": true,
|
||||
"close": true,
|
||||
"print": true
|
||||
},
|
||||
"clipboard": {
|
||||
"all": false,
|
||||
"writeText": true,
|
||||
"readText": true
|
||||
},
|
||||
"notification": {
|
||||
"all": true
|
||||
}
|
||||
"plugins": {
|
||||
"fs": {
|
||||
"scope": [
|
||||
"$APPDATA/*",
|
||||
"$DATA/*",
|
||||
"$LOCALDATA/*",
|
||||
"$DESKTOP/*",
|
||||
"$DOCUMENT/*",
|
||||
"$DOWNLOAD/*",
|
||||
"$HOME/*",
|
||||
"$PICTURE/*",
|
||||
"$PUBLIC/*",
|
||||
"$VIDEO/*"
|
||||
]
|
||||
},
|
||||
"http": {
|
||||
"scope": [
|
||||
"http://**/",
|
||||
"https://**/"
|
||||
]
|
||||
},
|
||||
"shell": {
|
||||
"open": true
|
||||
},
|
||||
"updater": {
|
||||
"endpoints": [
|
||||
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
|
||||
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"appimage": {
|
||||
@ -105,35 +67,33 @@
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null,
|
||||
"license": "../LICENSE",
|
||||
"minimumSystemVersion": "10.15.0",
|
||||
"license": "../LICENSE"
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"updater": {},
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"endpoints": [
|
||||
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
|
||||
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
|
||||
]
|
||||
},
|
||||
"security": {
|
||||
"csp": "default-src 'self'; connect-src ipc: http://ipc.localhost",
|
||||
"dangerousRemoteDomainIpcAccess": [
|
||||
{
|
||||
"scheme": "https",
|
||||
"domain": "nwc.getalby.com",
|
||||
"windows": ["alby"],
|
||||
"enableTauriAPI": true
|
||||
"scheme": "https",
|
||||
"windows": [
|
||||
"alby"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"macOSPrivateApi": true
|
||||
}
|
||||
}
|
||||
}
|
@ -2,30 +2,19 @@
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"tauri": {
|
||||
"windows": [
|
||||
{
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"decorations": false,
|
||||
"title": "Lume",
|
||||
"center": true,
|
||||
"resizable": false,
|
||||
"label": "splashscreen",
|
||||
"url": "splashscreen"
|
||||
},
|
||||
{
|
||||
"width": 1080,
|
||||
"height": 800,
|
||||
"minWidth": 1080,
|
||||
"minHeight": 800,
|
||||
"resizable": true,
|
||||
"theme": "Dark",
|
||||
"title": "Lume",
|
||||
"transparent": false,
|
||||
"center": true,
|
||||
"fullscreen": false,
|
||||
"hiddenTitle": true,
|
||||
"visible": false,
|
||||
"fileDropEnabled": true
|
||||
"fileDropEnabled": true,
|
||||
"decorations": false,
|
||||
"transparent": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,35 +1,24 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"tauri": {
|
||||
"macOSPrivateApi": true,
|
||||
"windows": [
|
||||
{
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"decorations": false,
|
||||
"title": "Lume",
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true,
|
||||
"center": true,
|
||||
"resizable": false,
|
||||
"label": "splashscreen",
|
||||
"url": "splashscreen"
|
||||
},
|
||||
{
|
||||
"width": 1080,
|
||||
"height": 800,
|
||||
"minWidth": 1080,
|
||||
"minHeight": 800,
|
||||
"resizable": true,
|
||||
"theme": "Dark",
|
||||
"title": "Lume",
|
||||
"titleBarStyle": "Overlay",
|
||||
"transparent": true,
|
||||
"center": true,
|
||||
"fullscreen": false,
|
||||
"hiddenTitle": true,
|
||||
"visible": false,
|
||||
"fileDropEnabled": true
|
||||
"fileDropEnabled": true,
|
||||
"decorations": true,
|
||||
"transparent": true,
|
||||
"windowEffects": {
|
||||
"effects": ["sidebar"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -2,30 +2,22 @@
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||
"tauri": {
|
||||
"windows": [
|
||||
{
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"decorations": false,
|
||||
"title": "Lume",
|
||||
"center": true,
|
||||
"resizable": false,
|
||||
"label": "splashscreen",
|
||||
"url": "splashscreen"
|
||||
},
|
||||
{
|
||||
"width": 1080,
|
||||
"height": 800,
|
||||
"minWidth": 1080,
|
||||
"minHeight": 800,
|
||||
"resizable": true,
|
||||
"theme": "Dark",
|
||||
"title": "Lume",
|
||||
"transparent": false,
|
||||
"center": true,
|
||||
"fullscreen": false,
|
||||
"hiddenTitle": true,
|
||||
"visible": false,
|
||||
"fileDropEnabled": true
|
||||
"fileDropEnabled": true,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"windowEffects": {
|
||||
"effects": ["micaLight", "micaDark"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
58
src/app.css
Normal file
@ -0,0 +1,58 @@
|
||||
@import 'reactflow/dist/style.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
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;
|
||||
}
|
202
src/app.tsx
@ -1,49 +1,30 @@
|
||||
import { message } from '@tauri-apps/api/dialog';
|
||||
import { fetch } from '@tauri-apps/api/http';
|
||||
import { message } from '@tauri-apps/plugin-dialog';
|
||||
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
import { AuthCreateScreen } from '@app/auth/create';
|
||||
import { AuthImportScreen } from '@app/auth/import';
|
||||
import { OnboardingScreen } from '@app/auth/onboarding';
|
||||
import { ChatsScreen } from '@app/chats';
|
||||
import { ErrorScreen } from '@app/error';
|
||||
import { ExploreScreen } from '@app/explore';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { Frame } from '@shared/frame';
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
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 './index.css';
|
||||
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();
|
||||
|
||||
const stronghold = sessionStorage.getItem('stronghold');
|
||||
const privkey = JSON.parse(stronghold).state.privkey || null;
|
||||
|
||||
const onboarding = localStorage.getItem('onboarding');
|
||||
const step = JSON.parse(onboarding).state.step || null;
|
||||
|
||||
if (totalAccount === 0) {
|
||||
return redirect('/auth/welcome');
|
||||
} else {
|
||||
if (step) {
|
||||
return redirect(step);
|
||||
}
|
||||
|
||||
if (!privkey) {
|
||||
return redirect('/auth/unlock');
|
||||
}
|
||||
}
|
||||
if (totalAccount === 0) return redirect('/auth/welcome');
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
@ -58,7 +39,7 @@ export default function App() {
|
||||
headers: {
|
||||
Accept: 'application/nostr+json',
|
||||
},
|
||||
}).then((res) => res.data),
|
||||
}).then((res) => res.json()),
|
||||
});
|
||||
};
|
||||
|
||||
@ -83,13 +64,6 @@ export default function App() {
|
||||
return { Component: UserScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'chats/:pubkey',
|
||||
async lazy() {
|
||||
const { ChatScreen } = await import('@app/chats');
|
||||
return { Component: ChatScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'notifications',
|
||||
async lazy() {
|
||||
@ -104,15 +78,6 @@ export default function App() {
|
||||
return { Component: NWCScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'explore',
|
||||
element: (
|
||||
<ReactFlowProvider>
|
||||
<ExploreScreen />
|
||||
</ReactFlowProvider>
|
||||
),
|
||||
errorElement: <ErrorScreen />,
|
||||
},
|
||||
{
|
||||
path: 'relays',
|
||||
async lazy() {
|
||||
@ -128,6 +93,29 @@ export default function App() {
|
||||
return { Component: RelayScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'explore',
|
||||
element: (
|
||||
<ReactFlowProvider>
|
||||
<ExploreScreen />
|
||||
</ReactFlowProvider>
|
||||
),
|
||||
errorElement: <ErrorScreen />,
|
||||
},
|
||||
{
|
||||
path: 'chats',
|
||||
element: <ChatsScreen />,
|
||||
errorElement: <ErrorScreen />,
|
||||
children: [
|
||||
{
|
||||
path: 'chat/:pubkey',
|
||||
async lazy() {
|
||||
const { ChatScreen } = await import('@app/chats/chat');
|
||||
return { Component: ChatScreen };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -151,14 +139,6 @@ export default function App() {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/splashscreen',
|
||||
errorElement: <ErrorScreen />,
|
||||
async lazy() {
|
||||
const { SplashScreen } = await import('@app/splash');
|
||||
return { Component: SplashScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/auth',
|
||||
element: <AuthLayout />,
|
||||
@ -172,60 +152,18 @@ export default function App() {
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'import',
|
||||
element: <AuthImportScreen />,
|
||||
errorElement: <ErrorScreen />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
async lazy() {
|
||||
const { ImportStep1Screen } = await import('@app/auth/import/step-1');
|
||||
return { Component: ImportStep1Screen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'step-2',
|
||||
async lazy() {
|
||||
const { ImportStep2Screen } = await import('@app/auth/import/step-2');
|
||||
return { Component: ImportStep2Screen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'step-3',
|
||||
async lazy() {
|
||||
const { ImportStep3Screen } = await import('@app/auth/import/step-3');
|
||||
return { Component: ImportStep3Screen };
|
||||
},
|
||||
},
|
||||
],
|
||||
path: 'create',
|
||||
async lazy() {
|
||||
const { CreateAccountScreen } = await import('@app/auth/create');
|
||||
return { Component: CreateAccountScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
element: <AuthCreateScreen />,
|
||||
errorElement: <ErrorScreen />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
async lazy() {
|
||||
const { CreateStep1Screen } = await import('@app/auth/create/step-1');
|
||||
return { Component: CreateStep1Screen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'step-2',
|
||||
async lazy() {
|
||||
const { CreateStep2Screen } = await import('@app/auth/create/step-2');
|
||||
return { Component: CreateStep2Screen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'step-3',
|
||||
async lazy() {
|
||||
const { CreateStep3Screen } = await import('@app/auth/create/step-3');
|
||||
return { Component: CreateStep3Screen };
|
||||
},
|
||||
},
|
||||
],
|
||||
path: 'import',
|
||||
async lazy() {
|
||||
const { ImportAccountScreen } = await import('@app/auth/import');
|
||||
return { Component: ImportAccountScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'onboarding',
|
||||
@ -235,58 +173,32 @@ export default function App() {
|
||||
{
|
||||
path: '',
|
||||
async lazy() {
|
||||
const { OnboardStep1Screen } = await import(
|
||||
'@app/auth/onboarding/step-1'
|
||||
const { OnboardingListScreen } = await import(
|
||||
'@app/auth/onboarding/list'
|
||||
);
|
||||
return { Component: OnboardStep1Screen };
|
||||
return { Component: OnboardingListScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'step-2',
|
||||
path: 'enrich',
|
||||
async lazy() {
|
||||
const { OnboardStep2Screen } = await import(
|
||||
'@app/auth/onboarding/step-2'
|
||||
const { OnboardEnrichScreen } = await import(
|
||||
'@app/auth/onboarding/enrich'
|
||||
);
|
||||
return { Component: OnboardStep2Screen };
|
||||
return { Component: OnboardEnrichScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'hashtag',
|
||||
async lazy() {
|
||||
const { OnboardHashtagScreen } = await import(
|
||||
'@app/auth/onboarding/hashtag'
|
||||
);
|
||||
return { Component: OnboardHashtagScreen };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'complete',
|
||||
async lazy() {
|
||||
const { CompleteScreen } = await import('@app/auth/complete');
|
||||
return { Component: CompleteScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'unlock',
|
||||
async lazy() {
|
||||
const { UnlockScreen } = await import('@app/auth/unlock');
|
||||
return { Component: UnlockScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'lock',
|
||||
async lazy() {
|
||||
const { LockScreen } = await import('@app/auth/lock');
|
||||
return { Component: LockScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'migrate',
|
||||
async lazy() {
|
||||
const { MigrateScreen } = await import('@app/auth/migrate');
|
||||
return { Component: MigrateScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'reset',
|
||||
async lazy() {
|
||||
const { ResetScreen } = await import('@app/auth/reset');
|
||||
return { Component: ResetScreen };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -316,9 +228,9 @@ export default function App() {
|
||||
<RouterProvider
|
||||
router={router}
|
||||
fallbackElement={
|
||||
<Frame className="flex h-full w-full items-center justify-center">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
||||
</Frame>
|
||||
</div>
|
||||
}
|
||||
future={{ v7_startTransition: true }}
|
||||
/>
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export function CompleteScreen() {
|
||||
const navigate = useNavigate();
|
||||
const [count, setCount] = useState(5);
|
||||
|
||||
useEffect(() => {
|
||||
let counter: NodeJS.Timeout;
|
||||
|
||||
if (count > 0) {
|
||||
counter = setTimeout(() => setCount(count - 1), 1000);
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(counter);
|
||||
};
|
||||
}, [count]);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col items-center justify-center">
|
||||
<div className="mx-auto flex max-w-xl flex-col gap-1.5 text-center">
|
||||
<h1 className="text-2xl font-light leading-none text-white">
|
||||
<span className="font-semibold">You're ready</span>, redirecting in {count}
|
||||
</h1>
|
||||
<p className="text-white/70">
|
||||
Thank you for using Lume. Lume doesn't use telemetry. If you encounter any
|
||||
problems, please submit a report via the "Report Issue" button.
|
||||
<br />
|
||||
You can find it while using the application.
|
||||
</p>
|
||||
</div>
|
||||
<div className="absolute bottom-6 left-1/2 flex -translate-x-1/2 transform items-center justify-center">
|
||||
<img src="/lume.png" alt="lume" className="h-auto w-1/5" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
50
src/app/auth/components/features/allowNotification.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
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>
|
||||
);
|
||||
}
|
100
src/app/auth/components/features/enableCircle.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
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>
|
||||
);
|
||||
}
|
47
src/app/auth/components/features/enableOutbox.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
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's outbox relays and subsequent queries will favour using those
|
||||
relays for queries with that user'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>
|
||||
);
|
||||
}
|
35
src/app/auth/components/features/favoriteHashtag.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
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>
|
||||
);
|
||||
}
|
58
src/app/auth/components/features/followList.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
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(
|
||||
['follows'],
|
||||
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="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 === 'loading' ? (
|
||||
<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>
|
||||
);
|
||||
}
|
35
src/app/auth/components/features/suggestFollow.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
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>
|
||||
);
|
||||
}
|
295
src/app/auth/create.tsx
Normal file
@ -0,0 +1,295 @@
|
||||
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,
|
||||
};
|
||||
|
||||
const userPrivkey = generatePrivateKey();
|
||||
const userPubkey = getPublicKey(userPrivkey);
|
||||
const userNpub = nip19.npubEncode(userPubkey);
|
||||
const userNsec = nip19.nsecEncode(userPrivkey);
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
const signer = new NDKPrivateKeySigner(userPrivkey);
|
||||
|
||||
event.content = JSON.stringify(profile);
|
||||
event.kind = NDKKind.Metadata;
|
||||
event.created_at = Math.floor(Date.now() / 1000);
|
||||
event.pubkey = userPubkey;
|
||||
event.tags = [];
|
||||
|
||||
await event.sign(signer);
|
||||
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,
|
||||
`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's set up your Nostr 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">
|
||||
<img
|
||||
src={picture || svgURI}
|
||||
alt="user's avatar"
|
||||
className="h-14 w-14 rounded-lg bg-black object-cover 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-500">
|
||||
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>
|
||||
);
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
export function AuthCreateScreen() {
|
||||
const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
|
||||
useEffect(() => {
|
||||
if (step) {
|
||||
setPrivkey(tmpPrivkey);
|
||||
}
|
||||
}, [tmpPrivkey]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
import { writeText } from '@tauri-apps/api/clipboard';
|
||||
import { message, save } from '@tauri-apps/api/dialog';
|
||||
import { writeTextFile } from '@tauri-apps/api/fs';
|
||||
import { downloadDir } from '@tauri-apps/api/path';
|
||||
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { CopyIcon } from '@shared/icons';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
export function CreateStep1Screen() {
|
||||
const { db } = useStorage();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
const setTempPrivkey = useOnboarding((state) => state.setTempPrivkey);
|
||||
const setPubkey = useOnboarding((state) => state.setPubkey);
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [copied, setCopied] = 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);
|
||||
|
||||
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,
|
||||
`Generated by Lume (lume.nu)\nPublic key: ${npub}\nPrivate key: ${nsec}`
|
||||
);
|
||||
|
||||
setDownloaded(true);
|
||||
} // else { user cancel action }
|
||||
} catch (e) {
|
||||
await message(e, { title: 'Cannot download account keys', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const copyPrivkey = async () => {
|
||||
try {
|
||||
await writeText(nsec);
|
||||
setCopied(true);
|
||||
|
||||
setTimeout(() => setCopied(false), 3000);
|
||||
} catch (e) {
|
||||
await message(e, { title: 'Cannot copy private key', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
setLoading(true);
|
||||
|
||||
// update state
|
||||
setPrivkey(privkey);
|
||||
setTempPrivkey(privkey); // only use if user close app and reopen it
|
||||
setPubkey(pubkey);
|
||||
|
||||
// save to database
|
||||
await db.createAccount(npub, pubkey);
|
||||
|
||||
// redirect to next step
|
||||
navigate('/auth/create/step-2', { replace: true });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/create');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-4 border-b border-white/10 pb-4">
|
||||
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||
This is your new Nostr account
|
||||
</h1>
|
||||
<p className="mb-2 text-white/70">
|
||||
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>
|
||||
<p className="text-white/70">
|
||||
Public key is used for sharing with other people so that they can find you using
|
||||
the public key.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium text-white">Private Key</span>
|
||||
<div className="relative">
|
||||
<input
|
||||
readOnly
|
||||
value={nsec.substring(0, 5) + '**************************************'}
|
||||
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 py-1 pl-3.5 pr-11 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyPrivkey()}
|
||||
className="group absolute right-2 top-1/2 inline-flex h-7 -translate-y-1/2 transform items-center gap-1.5 rounded-md bg-white/20 px-2.5 text-sm hover:bg-white/30"
|
||||
>
|
||||
<CopyIcon className="h-4 w-4 text-white/70 group-hover:text-white" />
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium text-white">Public Key</span>
|
||||
<input
|
||||
readOnly
|
||||
value={npub}
|
||||
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => download()}
|
||||
className="inline-flex h-12 w-full items-center justify-center rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||
>
|
||||
{downloaded ? 'Downloaded' : 'Download account keys'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
className="inline-flex h-12 w-full items-center justify-center rounded-lg border-t border-white/10 bg-white/20 px-6 font-medium leading-none text-white hover:bg-white/30 focus:outline-none"
|
||||
>
|
||||
{loading ? 'Creating...' : 'Continue'}
|
||||
</button>
|
||||
<span className="text-center text-sm text-white/50">
|
||||
By clicking 'Continue', you are ensuring that your keys are saved in
|
||||
a safe place. You cannot recover these keys if they are lost.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
import { appConfigDir } from '@tauri-apps/api/path';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Stronghold } from 'tauri-plugin-stronghold-api';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
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 setStep = useOnboarding((state) => state.setStep);
|
||||
const pubkey = useOnboarding((state) => state.pubkey);
|
||||
const privkey = useStronghold((state) => state.privkey);
|
||||
|
||||
const [passwordInput, setPasswordInput] = useState('password');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { db } = useStorage();
|
||||
|
||||
// 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) {
|
||||
const dir = await appConfigDir();
|
||||
const stronghold = await Stronghold.load(`${dir}lume.stronghold`, data.password);
|
||||
|
||||
if (!db.secureDB) db.secureDB = stronghold;
|
||||
|
||||
// save privkey to secure storage
|
||||
await db.secureSave(pubkey, privkey);
|
||||
|
||||
// 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',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/create/step-2');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-4 border-b border-white/10 pb-4">
|
||||
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||
Set password to secure your key
|
||||
</h1>
|
||||
<p className="text-white/70">
|
||||
Password is not related to your Nostr account. It is only used to secure your
|
||||
keys stored on your local machine and to unlock the app (like unlocking your
|
||||
phone with a passcode). When you move to other Nostr clients, you just need to
|
||||
copy your private key.
|
||||
</p>
|
||||
</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}
|
||||
placeholder="Enter password"
|
||||
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3.5 py-1 text-center tracking-widest text-white !outline-none backdrop-blur-xl placeholder:tracking-normal placeholder:text-white/70"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showPassword()}
|
||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 backdrop-blur-xl hover:bg-white/20"
|
||||
>
|
||||
{passwordInput === 'password' ? (
|
||||
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||
) : (
|
||||
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||
)}
|
||||
</button>
|
||||
</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-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Securing your account...</span>
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Continue</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
import { NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { AvatarUploader } from '@shared/avatarUploader';
|
||||
import { BannerUploader } from '@shared/bannerUploader';
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { WidgetKinds } from '@stores/widgets';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function CreateStep3Screen() {
|
||||
const navigate = useNavigate();
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [picture, setPicture] = useState('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
|
||||
const [banner, setBanner] = useState('');
|
||||
|
||||
const { db } = useStorage();
|
||||
const { publish } = useNostr();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = async (data: { name: string; about: string; website: string }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const profile = {
|
||||
...data,
|
||||
name: data.name,
|
||||
display_name: data.name,
|
||||
bio: data.about,
|
||||
website: data.website,
|
||||
};
|
||||
|
||||
const event = await publish({
|
||||
content: JSON.stringify(profile),
|
||||
kind: NDKKind.Metadata,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
// create default widget
|
||||
await db.createWidget(WidgetKinds.other.learnNostr, 'Learn Nostr', '');
|
||||
|
||||
if (event) {
|
||||
navigate('/auth/onboarding', { replace: true });
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error: ', e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/create/step-3');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-4 border-b border-white/10 pb-4">
|
||||
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||
Personalize your Nostr profile
|
||||
</h1>
|
||||
<p className="text-white/70">
|
||||
Nostr profile is synchronous across all Nostr clients. If you create a profile
|
||||
on Lume, it will also work well with other Nostr clients. If you update your
|
||||
profile on another Nostr client, it will also sync to Lume.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full overflow-hidden rounded-xl bg-white/10 backdrop-blur-xl">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
||||
<input type={'hidden'} {...register('picture')} value={picture} />
|
||||
<input type={'hidden'} {...register('banner')} value={banner} />
|
||||
<div className="relative">
|
||||
<div className="relative h-36 w-full bg-white/10 backdrop-blur-xl">
|
||||
{banner ? (
|
||||
<Image
|
||||
src={banner}
|
||||
alt="user's banner"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full bg-white/20" />
|
||||
)}
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<BannerUploader setBanner={setBanner} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-5 px-4">
|
||||
<div className="relative z-10 -mt-8 h-16 w-16">
|
||||
<Image
|
||||
src={picture}
|
||||
alt="user's avatar"
|
||||
className="h-16 w-16 rounded-lg object-cover ring-2 ring-white/20"
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<AvatarUploader setPicture={setPicture} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="name" className="font-medium text-white">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('name', {
|
||||
required: true,
|
||||
minLength: 1,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-12 w-full rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="about" className="font-medium text-white">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register('about')}
|
||||
spellCheck={false}
|
||||
className="relative h-20 w-full resize-none rounded-lg bg-white/20 px-3 py-2 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="website" className="font-medium text-white">
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
{...register('website', {
|
||||
required: false,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-12 w-full rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Creating...</span>
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Continue</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
259
src/app/auth/import.tsx
Normal file
@ -0,0 +1,259 @@
|
||||
import { readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { motion } from 'framer-motion';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ArrowLeftIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function ImportAccountScreen() {
|
||||
const { db } = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [npub, setNpub] = useState<string>('');
|
||||
const [nsec, setNsec] = useState<string>('');
|
||||
const [pubkey, setPubkey] = useState<undefined | string>(undefined);
|
||||
const [created, setCreated] = useState(false);
|
||||
const [savedPrivkey, setSavedPrivkey] = useState(false);
|
||||
|
||||
const submitNpub = async () => {
|
||||
if (npub.length < 6) return toast('You must enter valid npub');
|
||||
if (!npub.startsWith('npub1')) return toast('npub must be starts with npub1');
|
||||
|
||||
try {
|
||||
const pubkey = nip19.decode(npub).data as string;
|
||||
setPubkey(pubkey);
|
||||
} catch (e) {
|
||||
return toast(`npub invalid: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
const changeAccount = async () => {
|
||||
setNpub('');
|
||||
setPubkey('');
|
||||
};
|
||||
|
||||
const createAccount = async () => {
|
||||
try {
|
||||
await db.createAccount(npub, pubkey);
|
||||
setCreated(true);
|
||||
} catch (e) {
|
||||
return toast(`Create account failed: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
const pasteNsec = async () => {
|
||||
const tempNsec = await readText();
|
||||
setNsec(tempNsec);
|
||||
};
|
||||
|
||||
const submitNsec = async () => {
|
||||
if (savedPrivkey) return;
|
||||
if (nsec.length > 50 && nsec.startsWith('nsec1')) {
|
||||
try {
|
||||
const privkey = nip19.decode(nsec).data as string;
|
||||
await db.secureSave(pubkey, privkey);
|
||||
setSavedPrivkey(true);
|
||||
} catch (e) {
|
||||
return toast(`nsec invalid: ${e}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<div className="absolute left-[8px] top-2">
|
||||
{!created ? (
|
||||
<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">
|
||||
Import your Nostr account.
|
||||
</h1>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div 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">
|
||||
<label htmlFor="npub" className="font-semibold">
|
||||
Enter your public key:
|
||||
</label>
|
||||
<div className="inline-flex w-full items-center gap-2">
|
||||
<input
|
||||
name="npub"
|
||||
type="text"
|
||||
value={npub}
|
||||
onChange={(e) => setNpub(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="npub1"
|
||||
className="h-11 flex-1 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
{!pubkey ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={submitNpub}
|
||||
className="h-11 w-24 shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{pubkey ? (
|
||||
<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"
|
||||
>
|
||||
<h5 className="mb-1.5 font-semibold">Account found</h5>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="inline-flex h-full flex-1 items-center rounded-lg bg-neutral-200 p-2">
|
||||
<User pubkey={pubkey} variant="simple" />
|
||||
</div>
|
||||
{!created ? (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={changeAccount}
|
||||
className="h-9 flex-1 shrink-0 rounded-lg bg-neutral-200 font-semibold text-neutral-800 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Change account
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={createAccount}
|
||||
className="h-9 flex-1 shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
{created ? (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
className="rounded-lg bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="nsec" className="font-semibold">
|
||||
Enter your private key (optional):
|
||||
</label>
|
||||
<div className="inline-flex w-full items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
name="nsec"
|
||||
type="text"
|
||||
value={nsec}
|
||||
onChange={(e) => setNsec(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="nsec1"
|
||||
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"
|
||||
/>
|
||||
{nsec.length < 5 ? (
|
||||
<div className="absolute right-0 top-0 inline-flex h-11 items-center justify-center px-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={pasteNsec}
|
||||
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"
|
||||
>
|
||||
Paste
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{nsec.length > 5 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={submitNsec}
|
||||
className={twMerge(
|
||||
'h-11 w-24 shrink-0 rounded-lg font-semibold text-white',
|
||||
!savedPrivkey
|
||||
? 'bg-blue-500 hover:bg-blue-600'
|
||||
: 'bg-teal-500 hover:bg-teal-600'
|
||||
)}
|
||||
>
|
||||
{savedPrivkey ? 'Saved' : 'Save'}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 select-text">
|
||||
<p className="text-sm">
|
||||
<b>Private Key</b> is used to sign your event. For example, if you
|
||||
want to make a new post or send a message to your contact, you need to
|
||||
use your private key to sign this event.
|
||||
</p>
|
||||
<h5 className="mt-2 font-semibold">
|
||||
1. In case you store private key in Lume
|
||||
</h5>
|
||||
<p className="text-sm">
|
||||
Lume will put your private key to{' '}
|
||||
<b>
|
||||
{db.platform === 'macos'
|
||||
? 'Apple Keychain (macOS)'
|
||||
: db.platform === 'windows'
|
||||
? 'Credential Manager (Windows)'
|
||||
: 'Secret Service (Linux)'}
|
||||
</b>
|
||||
, it will be secured by your OS
|
||||
</p>
|
||||
<h5 className="mt-2 font-semibold">
|
||||
2. In case you do not store private key in Lume
|
||||
</h5>
|
||||
<p className="text-sm">
|
||||
When you make an event that requires a sign by your private key, Lume
|
||||
will show a prompt for you to enter private key. It will be cleared
|
||||
after signing and not stored anywhere.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.button
|
||||
initial={{ opacity: 0, y: 80 }}
|
||||
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: false } })
|
||||
}
|
||||
>
|
||||
Finish
|
||||
</motion.button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
export function AuthImportScreen() {
|
||||
const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
|
||||
useEffect(() => {
|
||||
if (step) {
|
||||
setPrivkey(tmpPrivkey);
|
||||
}
|
||||
}, [tmpPrivkey]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
import { getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
type FormValues = {
|
||||
privkey: string;
|
||||
};
|
||||
|
||||
const resolver: Resolver<FormValues> = async (values) => {
|
||||
return {
|
||||
values: values.privkey ? values : {},
|
||||
errors: !values.privkey
|
||||
? {
|
||||
privkey: {
|
||||
type: 'required',
|
||||
message: 'This is required.',
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
};
|
||||
|
||||
export function ImportStep1Screen() {
|
||||
const navigate = useNavigate();
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
const setTempPubkey = useOnboarding((state) => state.setTempPrivkey);
|
||||
const setPubkey = useOnboarding((state) => state.setPubkey);
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [passwordInput, setPasswordInput] = useState('password');
|
||||
|
||||
const { db } = useStorage();
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty, isValid },
|
||||
} = useForm<FormValues>({ resolver });
|
||||
|
||||
const onSubmit = async (data: { [x: string]: string }) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
let privkey = data['privkey'];
|
||||
if (privkey.substring(0, 4) === 'nsec') {
|
||||
privkey = nip19.decode(privkey).data as string;
|
||||
}
|
||||
|
||||
if (typeof getPublicKey(privkey) === 'string') {
|
||||
const pubkey = getPublicKey(privkey);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
|
||||
setPrivkey(privkey);
|
||||
setTempPubkey(privkey); // only use if user close app and reopen it
|
||||
setPubkey(pubkey);
|
||||
|
||||
// add account to local database
|
||||
await db.createAccount(npub, pubkey);
|
||||
|
||||
// redirect to step 2 with delay 1.2s
|
||||
setTimeout(() => navigate('/auth/import/step-2', { replace: true }), 1200);
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setError('privkey', {
|
||||
type: 'custom',
|
||||
message: 'Private key is invalid, please check again',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// toggle private key
|
||||
const showPassword = () => {
|
||||
if (passwordInput === 'password') {
|
||||
setPasswordInput('text');
|
||||
} else {
|
||||
setPasswordInput('password');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/import');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-4 pb-4">
|
||||
<h1 className="text-center text-2xl font-semibold text-white">
|
||||
Import your Nostr key
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="privkey" className="font-medium text-white">
|
||||
Insert your nostr private key, in nsec or hex format
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register('privkey', { required: true, minLength: 32 })}
|
||||
type={passwordInput}
|
||||
placeholder="nsec1..."
|
||||
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3 py-1 text-white backdrop-blur-xl placeholder:text-white/70 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showPassword()}
|
||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 backdrop-blur-xl hover:bg-white/20"
|
||||
>
|
||||
{passwordInput === 'password' ? (
|
||||
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||
) : (
|
||||
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-red-500">
|
||||
{errors.privkey && <p>{errors.privkey.message}</p>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Importing...</span>
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Continue</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
import { appConfigDir } from '@tauri-apps/api/path';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Stronghold } from 'tauri-plugin-stronghold-api';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
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 ImportStep2Screen() {
|
||||
const navigate = useNavigate();
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
const pubkey = useOnboarding((state) => state.pubkey);
|
||||
const privkey = useStronghold((state) => state.privkey);
|
||||
|
||||
const [passwordInput, setPasswordInput] = useState('password');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { db } = useStorage();
|
||||
|
||||
// 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) {
|
||||
const dir = await appConfigDir();
|
||||
const stronghold = await Stronghold.load(`${dir}lume.stronghold`, data.password);
|
||||
|
||||
if (!db.secureDB) db.secureDB = stronghold;
|
||||
|
||||
// save privkey to secure storage
|
||||
await db.secureSave(pubkey, privkey);
|
||||
|
||||
// redirect to next step
|
||||
navigate('/auth/import/step-3', { replace: true });
|
||||
} else {
|
||||
setLoading(false);
|
||||
setError('password', {
|
||||
type: 'custom',
|
||||
message: 'Password is required and must be greater than 3, please check again',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/import/step-2');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-4 border-b border-white/10 pb-4">
|
||||
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
|
||||
Set password to secure your key
|
||||
</h1>
|
||||
<p className="text-white/70">
|
||||
Password is not related to your Nostr account. It is only used to secure your
|
||||
keys stored on your local machine and to unlock the app (like unlocking your
|
||||
phone with a passcode). When you move to other Nostr clients, you only need to
|
||||
copy your private key.
|
||||
</p>
|
||||
</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}
|
||||
placeholder="Enter password"
|
||||
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3.5 py-1 text-center tracking-widest text-white !outline-none backdrop-blur-xl placeholder:tracking-normal placeholder:text-white/70"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showPassword()}
|
||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 backdrop-blur-xl hover:bg-white/20"
|
||||
>
|
||||
{passwordInput === 'password' ? (
|
||||
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||
) : (
|
||||
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
|
||||
)}
|
||||
</button>
|
||||
</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-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Securing your account...</span>
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Continue</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { WidgetKinds } from '@stores/widgets';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function ImportStep3Screen() {
|
||||
const navigate = useNavigate();
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
|
||||
const { db } = useStorage();
|
||||
const { fetchUserData } = useNostr();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// show loading indicator
|
||||
setLoading(true);
|
||||
|
||||
// prefetch data
|
||||
const user = await fetchUserData();
|
||||
|
||||
// create default widget
|
||||
await db.createWidget(WidgetKinds.other.learnNostr, 'Learn Nostr', '');
|
||||
|
||||
// redirect to next step
|
||||
if (user.status === 'ok') {
|
||||
navigate('/auth/onboarding/step-2', { replace: true });
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error: ', e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/import/step-3');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-4 pb-4">
|
||||
<h1 className="text-center text-2xl font-semibold text-white">
|
||||
{loading ? 'Downloading...' : 'Your Nostr profile'}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="rounded-lg border-t border-white/10 bg-white/20 px-3 py-3">
|
||||
<User pubkey={db.account.pubkey} variant="simple" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none"
|
||||
onClick={() => submit()}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>It might take a bit, please patient...</span>
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Continue</span>
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<span className="text-center text-sm text-white/50">
|
||||
By clicking 'Continue', Lume will download your old relay list and
|
||||
metadata. It may take a bit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
export function LockScreen() {
|
||||
return (
|
||||
<div
|
||||
className="h-full w-full bg-cover bg-center"
|
||||
style={{ backgroundImage: 'url(/wallpapers/1.png)' }}
|
||||
>
|
||||
<p>TODO</p>
|
||||
</div>
|
||||
);
|
||||
}
|