Compare commits

...
26 Commits
Author SHA1 Message Date
Ren Amamiya 48066a4018 bump version 2023-09-06 16:53:05 +07:00
Ren Amamiya 5c8850ea8f redesign widget list 2023-09-06 14:30:57 +07:00
Ren Amamiya 09aea3cff5 update widgets 2023-09-06 08:58:02 +07:00
Ren Amamiya 45c5a890b9 update tauri config 2023-09-06 07:46:31 +07:00
Ren Amamiya 69a3e85cb3 small updates and bump version 2023-09-05 17:25:00 +07:00
Ren Amamiya 224439f62b support drag and drop upload in composer 2023-09-05 13:07:21 +07:00
Ren Amamiya 2389ad5fdc replace void.cat with nostr.build 2023-09-05 12:46:00 +07:00
Ren Amamiya 4019623d99 small updates 2023-09-05 08:50:13 +07:00
Ren Amamiya 57c17ffbf9 use nostr.com to display unfound event 2023-09-04 18:09:41 +07:00
Ren Amamiya 98d2ccfc86 small fixes 2023-09-04 17:18:28 +07:00
Ren Amamiya c74a81cfdb re-add link preview 2023-09-04 14:35:57 +07:00
Ren Amamiya 3ebcf4a981 new parser, faster than before 50% 2023-09-04 14:05:04 +07:00
Ren Amamiya 5d45027776 fix build issue 2023-09-04 07:44:57 +07:00
Ren Amamiya 21ea8309c7 fix ci (final) 2023-09-03 12:02:59 +07:00
Ren Amamiya 431331174a fix ci again 2023-09-03 11:30:45 +07:00
Ren Amamiya c26cfc038d fix ci 2023-09-03 11:08:13 +07:00
Ren Amamiya 39b7b34bb7 new mention popup in composer 2023-09-03 08:43:08 +07:00
Ren Amamiya a4cf65e7c2 update ui consistent for cross platform 2023-09-03 07:43:38 +07:00
Ren Amamiya 37668393f1 expt: disable note metadata 2023-09-02 17:28:05 +07:00
Ren AmamiyaandGitHub 4309f734b6 Merge pull request #80 from luminous-devs/revert-to-tauri-v1
Revert to Tauri v1.4.0
2023-09-02 12:49:51 +07:00
Ren Amamiya b4957bae1f native fetch and shadow 2023-09-02 12:49:04 +07:00
Ren Amamiya 7a3b19bf7b revert to tauri v1 2023-09-02 12:15:48 +07:00
Ren Amamiya 1931373515 update composer 2023-09-02 08:50:54 +07:00
Ren Amamiya 28939d1733 improve hashtag parser 2023-09-01 18:26:22 +07:00
Ren Amamiya e6d35bc635 fix mention in composer and improve error handling 2023-09-01 15:57:31 +07:00
Ren Amamiya cc315a190a fully support nip05 2023-09-01 08:58:33 +07:00
114 changed files with 2889 additions and 2317 deletions
+4 -3
View File
@@ -1,4 +1,4 @@
name: 'publish'
name: 'Publish'
on: workflow_dispatch
env:
@@ -24,7 +24,7 @@ jobs:
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 18
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
@@ -32,7 +32,7 @@ jobs:
if: matrix.settings.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libsoup-3.0 libgtk-3-dev webkit2gtk-4.1 javascriptcoregtk-4.1 libappindicator3-dev librsvg2-dev patchelf
sudo apt-get install -y build-essential libssl-dev libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
@@ -66,3 +66,4 @@ jobs:
releaseDraft: true
prerelease: false
args: ${{ matrix.settings.args }}
includeDebug: true
+25 -35
View File
@@ -2,7 +2,7 @@
"name": "lume",
"description": "the communication app",
"private": true,
"version": "1.2.1",
"version": "1.2.3",
"scripts": {
"dev": "vite",
"build": "vite build",
@@ -19,6 +19,7 @@
},
"dependencies": {
"@ctrl/magnet-link": "^3.1.2",
"@getalby/sdk": "^2.4.0",
"@nostr-dev-kit/ndk": "^0.8.23",
"@nostr-fetch/adapter-ndk": "^0.12.2",
"@radix-ui/react-alert-dialog": "^1.0.4",
@@ -29,30 +30,14 @@
"@radix-ui/react-tooltip": "^1.0.6",
"@tanstack/react-query": "^4.33.0",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "2.0.0-alpha.6",
"@tauri-apps/cli": "2.0.0-alpha.11",
"@tauri-apps/plugin-app": "github:tauri-apps/tauri-plugin-app#v2",
"@tauri-apps/plugin-autostart": "github:tauri-apps/tauri-plugin-autostart#v2",
"@tauri-apps/plugin-clipboard-manager": "github:tauri-apps/tauri-plugin-clipboard-manager#v2",
"@tauri-apps/plugin-dialog": "github:tauri-apps/tauri-plugin-dialog#v2",
"@tauri-apps/plugin-fs": "github:tauri-apps/tauri-plugin-fs#v2",
"@tauri-apps/plugin-http": "github:tauri-apps/tauri-plugin-http#v2",
"@tauri-apps/plugin-notification": "github:tauri-apps/tauri-plugin-notification#v2",
"@tauri-apps/plugin-os": "github:tauri-apps/tauri-plugin-os#v2",
"@tauri-apps/plugin-process": "github:tauri-apps/tauri-plugin-process#v2",
"@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2",
"@tauri-apps/plugin-sql": "github:tauri-apps/tauri-plugin-sql#v2",
"@tauri-apps/plugin-store": "github:tauri-apps/tauri-plugin-store#v2",
"@tauri-apps/plugin-stronghold": "github:tauri-apps/tauri-plugin-stronghold#v2",
"@tauri-apps/plugin-upload": "github:tauri-apps/tauri-plugin-upload#v2",
"@tauri-apps/plugin-window": "github:tauri-apps/tauri-plugin-window#v2",
"@tiptap/extension-image": "^2.1.7",
"@tiptap/extension-mention": "^2.1.7",
"@tiptap/extension-placeholder": "^2.1.7",
"@tiptap/pm": "^2.1.7",
"@tiptap/react": "^2.1.7",
"@tiptap/starter-kit": "^2.1.7",
"@tiptap/suggestion": "^2.1.7",
"@tauri-apps/api": "^1.4.0",
"@tiptap/extension-image": "^2.1.8",
"@tiptap/extension-mention": "^2.1.8",
"@tiptap/extension-placeholder": "^2.1.8",
"@tiptap/pm": "^2.1.8",
"@tiptap/react": "^2.1.8",
"@tiptap/starter-kit": "^2.1.8",
"@tiptap/suggestion": "^2.1.8",
"@void-cat/api": "^1.0.7",
"dayjs": "^1.11.9",
"destr": "^2.0.1",
@@ -61,32 +46,37 @@
"light-bolt11-decoder": "^3.0.0",
"lru-cache": "^10.0.1",
"minidenticons": "^4.2.0",
"nostr-fetch": "^0.12.2",
"nostr-fetch": "^0.13.0",
"nostr-tools": "^1.14.2",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.45.4",
"react-hook-form": "^7.46.1",
"react-hotkeys-hook": "^4.4.1",
"react-markdown": "^8.0.7",
"react-player": "^2.12.0",
"react-player": "^2.13.0",
"react-router-dom": "^6.15.0",
"react-textarea-autosize": "^8.5.3",
"react-virtuoso": "^4.5.0",
"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",
"tippy.js": "^6.3.7",
"zustand": "^4.4.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@tailwindcss/typography": "^0.5.10",
"@tauri-apps/cli": "^1.4.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/html-to-text": "^9.0.1",
"@types/node": "^20.5.6",
"@types/node": "^20.5.9",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/youtube-player": "^5.5.7",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"clsx": "^2.0.0",
@@ -100,9 +90,9 @@
"eslint-plugin-simple-import-sort": "^10.0.0",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"postcss": "^8.4.28",
"prettier": "^3.0.2",
"prettier-plugin-tailwindcss": "^0.5.3",
"postcss": "^8.4.29",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"prop-types": "^15.8.1",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3",
+548 -627
View File
File diff suppressed because it is too large Load Diff
+715 -899
View File
File diff suppressed because it is too large Load Diff
+34 -29
View File
@@ -1,51 +1,56 @@
[package]
name = "lume"
version = "1.2.1"
description = "nostr client"
version = "1.2.3"
description = "the communication app"
authors = ["Ren Amamiya"]
license = "GPL-3.0"
repository = "https://github.com/luminous-devs/lume"
edition = "2021"
rust-version = "1.71"
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 = "2.0.0-alpha.8", features = [] }
tauri-build = { version = "1.4", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { git = "https://git.nextgraph.org/NextGraph/tauri.git", branch = "alpha.11-nextgraph", features = [
"protocol-asset",
tauri = { version = "1.4", features = [
"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",
"macos-private-api",
"no-ipc-custom-protocol",
"http-multipart",
] }
tauri-plugin-single-instance = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-autostart = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-stronghold = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-upload = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-updater = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-dialog = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-http = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-fs = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-clipboard-manager = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-notification = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-app = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-process = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-os = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-window = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-store = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-shell = { git = "https://github.com/luminous-devs/plugins-workspace", branch = "v2" }
tauri-plugin-sql = { git = "hhttps://github.com/tauri-apps/plugins-workspace", branch = "v1", 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"
[dependencies.tauri-plugin-sql]
git = "https://github.com/luminous-devs/plugins-workspace"
branch = "v2"
features = ["sqlite"]
webpage = { version = "1.6.0", features = ["serde"] }
[features]
# by default Tauri runs in production mode
@@ -53,7 +58,7 @@ features = ["sqlite"]
default = ["custom-protocol"]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = []
custom-protocol = ["tauri/custom-protocol"]
# Optimized for bundle size. If you want faster builds comment out/delete this section.
[profile.release]
+80 -12
View File
@@ -6,6 +6,11 @@
use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_sql::{Migration, MigrationKind};
use webpage::{Webpage, WebpageOptions};
use std::time::Duration;
#[cfg(target_os = "macos")]
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
#[derive(Clone, serde::Serialize)]
struct Payload {
@@ -13,6 +18,71 @@ struct Payload {
cwd: String,
}
#[derive(serde::Serialize)]
struct OpenGraphResponse {
title: String,
description: String,
url: String,
image: String,
}
async fn fetch_opengraph(url: String) -> OpenGraphResponse {
let options = WebpageOptions {
allow_insecure: false,
max_redirections: 3,
timeout: Duration::from_secs(15),
useragent: "lume - desktop app".to_string(),
..Default::default()
};
let result = match Webpage::from_url(&url, options) {
Ok(webpage) => webpage,
Err(_) => {
return OpenGraphResponse {
title: "".to_string(),
description: "".to_string(),
url: "".to_string(),
image: "".to_string(),
}
}
};
let html = result.html;
return OpenGraphResponse {
title: html
.opengraph
.properties
.get("title")
.cloned()
.unwrap_or_default(),
description: html
.opengraph
.properties
.get("description")
.cloned()
.unwrap_or_default(),
url: html
.opengraph
.properties
.get("url")
.cloned()
.unwrap_or_default(),
image: html
.opengraph
.images
.get(0)
.and_then(|i| Some(i.url.clone()))
.unwrap_or_default(),
};
}
#[tauri::command]
async fn opengraph(url: String) -> OpenGraphResponse {
let result = fetch_opengraph(url).await;
return result;
}
#[tauri::command]
async fn close_splashscreen(window: tauri::Window) {
// Close splashscreen
@@ -25,6 +95,15 @@ async fn close_splashscreen(window: tauri::Window) {
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");
Ok(())
})
.plugin(
tauri_plugin_sql::Builder::default()
.add_migrations(
@@ -168,20 +247,9 @@ fn main() {
.emit_all("single-instance", Payload { args: argv, cwd })
.unwrap();
}))
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_app::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_window::init())
.plugin(tauri_plugin_store::Builder::default().build())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![close_splashscreen])
.invoke_handler(tauri::generate_handler![close_splashscreen, opengraph])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+75 -43
View File
@@ -1,4 +1,5 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": {
"beforeBuildCommand": "pnpm build",
"beforeDevCommand": "pnpm dev",
@@ -8,40 +9,75 @@
},
"package": {
"productName": "Lume",
"version": "1.2.1"
},
"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}}"
]
}
"version": "1.2.3"
},
"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
},
"clipboard": {
"all": false,
"writeText": true,
"readText": true
},
"notification": {
"all": true
}
},
"bundle": {
"active": true,
"appimage": {
@@ -79,20 +115,16 @@
"timestampUrl": ""
}
},
"updater": {
"endpoints": [
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
]
},
"security": {
"csp": {
"connect-src": "ipc: https://ipc.localhost",
"content-security-policy": "upgrade-insecure-requests"
},
"freezePrototype": false,
"assetProtocol": {
"enable": true,
"scope": {
"allow": ["$APPCONFIG/*.db", "$RESOURCE/**"],
"deny": ["$APPCONFIG/*.stronghold"]
}
}
},
"macOSPrivateApi": true
}
}
}
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"windows": [
{
+3 -5
View File
@@ -1,5 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"macOSPrivateApi": true,
"windows": [
{
"width": 400,
@@ -27,11 +29,7 @@
"fullscreen": false,
"hiddenTitle": true,
"visible": false,
"fileDropEnabled": true,
"windowEffects": {
"effects": ["hudWindow"],
"state": "followsWindowActiveState"
}
"fileDropEnabled": true
}
]
}
+3 -6
View File
@@ -1,4 +1,5 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"windows": [
{
@@ -19,16 +20,12 @@
"resizable": true,
"theme": "Dark",
"title": "Lume",
"transparent": true,
"transparent": false,
"center": true,
"fullscreen": false,
"hiddenTitle": true,
"visible": false,
"fileDropEnabled": true,
"windowEffects": {
"effects": ["micaDark", "micaLight", "acrylic"],
"state": "followsWindowActiveState"
}
"fileDropEnabled": true
}
]
}
+3 -2
View File
@@ -5,6 +5,7 @@ import { AuthImportScreen } from '@app/auth/import';
import { OnboardingScreen } from '@app/auth/onboarding';
import { ErrorScreen } from '@app/error';
import { Frame } from '@shared/frame';
import { LoaderIcon } from '@shared/icons';
import { AppLayout } from '@shared/layouts/app';
import { AuthLayout } from '@shared/layouts/auth';
@@ -261,9 +262,9 @@ export default function App() {
<RouterProvider
router={router}
fallbackElement={
<div className="flex h-full w-full items-center justify-center bg-black/90 backdrop-blur-xl">
<Frame className="flex h-full w-full items-center justify-center">
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
</div>
</Frame>
}
/>
);
+1 -1
View File
@@ -27,7 +27,7 @@ export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }
/>
<div className="flex w-full flex-1 flex-col items-start text-start">
<p className="max-w-[15rem] truncate font-medium leading-tight text-white">
{user?.name || user?.display_name || user?.nip05}
{user?.name || user?.display_name}
</p>
<span className="max-w-[15rem] truncate leading-tight text-white/50">
{displayNpub(pubkey, 16)}
+1 -1
View File
@@ -28,7 +28,7 @@ export function UserRelay({ pubkey }: { pubkey: string }) {
className="h-5 w-5 shrink-0 rounded object-cover"
/>
<span className="truncate text-sm font-medium leading-none text-white">
{user?.name || user?.display_name || user?.nip05 || displayNpub(pubkey, 16)}
{user?.name || user?.display_name || displayNpub(pubkey, 16)}
</span>
</div>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
import { BaseDirectory, writeTextFile } from '@tauri-apps/plugin-fs';
import { BaseDirectory, writeTextFile } from '@tauri-apps/api/fs';
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
+1 -1
View File
@@ -1,8 +1,8 @@
import { appConfigDir } from '@tauri-apps/api/path';
import { Stronghold } from '@tauri-apps/plugin-stronghold';
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';
+11 -6
View File
@@ -1,3 +1,4 @@
import { NDKKind } from '@nostr-dev-kit/ndk';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
@@ -40,7 +41,7 @@ export function CreateStep3Screen() {
const event = await publish({
content: JSON.stringify(profile),
kind: 0,
kind: NDKKind.Metadata,
tags: [],
});
@@ -69,11 +70,15 @@ export function CreateStep3Screen() {
<input type={'hidden'} {...register('banner')} value={banner} />
<div className="relative">
<div className="relative h-44 w-full bg-white/10 backdrop-blur-xl">
<Image
src={banner}
alt="user's banner"
className="h-full w-full object-cover"
/>
{banner ? (
<Image
src={banner}
alt="user's banner"
className="h-full w-full object-cover"
/>
) : (
<div className="h-full w-full bg-black/50" />
)}
<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>
+1 -1
View File
@@ -1,8 +1,8 @@
import { appConfigDir } from '@tauri-apps/api/path';
import { Stronghold } from '@tauri-apps/plugin-stronghold';
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';
+1 -1
View File
@@ -1,9 +1,9 @@
import { useQueryClient } from '@tanstack/react-query';
import { appConfigDir } from '@tauri-apps/api/path';
import { Stronghold } from '@tauri-apps/plugin-stronghold';
import { 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';
+5 -4
View File
@@ -118,10 +118,11 @@ export function OnboardStep3Screen() {
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
</div>
) : relaysAsArray.length === 0 ? (
<div className="flex h-full w-full items-center justify-center">
<div className="flex h-full w-full items-center justify-center px-6">
<p className="text-center text-white/50">
Can&apos;t found any relays, you can skip this step and use default relays
instead
Lume couldn&apos;t find any relays from your follows.
<br />
You can skip this step and use default relays instead.
</p>
</div>
) : (
@@ -145,7 +146,7 @@ export function OnboardStep3Screen() {
))
)}
{relays.size > 5 && (
<div className="sticky bottom-0 left-0 inline-flex w-full items-center justify-center bg-white/10 px-4 py-2 backdrop-blur-2xl backdrop-blur-xl">
<div className="sticky bottom-0 left-0 inline-flex w-full items-center justify-center bg-white/10 px-4 py-2 backdrop-blur-2xl">
<p className="text-sm text-orange-400">
Using too much relay can cause high resource usage
</p>
+11 -5
View File
@@ -1,9 +1,9 @@
import { appConfigDir } from '@tauri-apps/api/path';
import { Stronghold } from '@tauri-apps/plugin-stronghold';
import { getPublicKey, nip19 } from 'nostr-tools';
import { useState } from 'react';
import { Resolver, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { Stronghold } from 'tauri-plugin-stronghold-api';
import { useStorage } from '@libs/storage/provider';
@@ -124,7 +124,7 @@ export function ResetScreen() {
{...register('privkey', { required: true })}
type="text"
placeholder="nsec..."
className="relative h-12 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/10"
className="relative h-12 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
/>
</div>
</div>
@@ -137,7 +137,7 @@ export function ResetScreen() {
{...register('password', { required: true })}
type={passwordInput}
placeholder="min. 4 characters"
className="relative h-12 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/10"
className="relative h-12 w-full rounded-lg bg-white/10 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
/>
<button
type="button"
@@ -155,7 +155,7 @@ export function ResetScreen() {
{errors.password && <p>{errors.password.message}</p>}
</span>
</div>
<div className="flex items-center justify-center">
<div className="flex flex-col items-center justify-center">
<button
type="submit"
disabled={!isDirty || !isValid}
@@ -167,6 +167,12 @@ export function ResetScreen() {
'Continue →'
)}
</button>
<Link
to="/auth/unlock"
className="mt-1 inline-flex h-11 w-full items-center justify-center rounded-lg text-center text-white/50 hover:bg-white/10"
>
Back
</Link>
</div>
</form>
</div>
+1 -1
View File
@@ -1,8 +1,8 @@
import { appConfigDir } from '@tauri-apps/api/path';
import { Stronghold } from '@tauri-apps/plugin-stronghold';
import { useState } from 'react';
import { Resolver, useForm } from 'react-hook-form';
import { Link, useNavigate } from 'react-router-dom';
import { Stronghold } from 'tauri-plugin-stronghold-api';
import { User } from '@app/auth/components/user';
+10 -9
View File
@@ -1,7 +1,8 @@
import { LogicalSize, getCurrent } from '@tauri-apps/plugin-window';
import { LogicalSize, getCurrent } from '@tauri-apps/api/window';
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Frame } from '@shared/frame';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
export function WelcomeScreen() {
@@ -28,16 +29,16 @@ export function WelcomeScreen() {
}, []);
return (
<div className="flex h-screen w-full flex-col justify-between bg-white/10 backdrop-blur-xl">
<Frame className="flex h-screen w-full flex-col justify-between">
<div className="flex flex-col gap-10 pt-16">
<div className="sflex flex-col gap-2 text-center">
<h1 className="text-3xl font-medium text-white">Welcome to Lume</h1>
<h3 className="mx-auto w-2/3 text-white/50">
<div className="flex flex-col gap-1.5 text-center">
<h1 className="text-3xl font-semibold text-white">Welcome to Lume</h1>
<p className="mx-auto w-2/3 leading-tight text-white/50">
Let&apos;s get you up and connecting with all peoples around the world on
Nostr
</h3>
</p>
</div>
<div className="inline-flex w-full flex-col items-center gap-3 px-4 pb-10">
<div className="inline-flex w-full flex-col items-center gap-2 px-4 pb-10">
<Link
to="/auth/import"
className="inline-flex h-11 w-2/3 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"
@@ -48,7 +49,7 @@ export function WelcomeScreen() {
</Link>
<Link
to="/auth/create"
className="inline-flex h-11 w-2/3 items-center justify-center gap-2 rounded-lg bg-white/10 px-6 font-medium leading-none text-white/50 backdrop-blur-xl hover:bg-white/20 focus:outline-none"
className="inline-flex h-11 w-2/3 items-center justify-center gap-2 rounded-lg bg-white/10 px-6 font-medium leading-none text-white backdrop-blur-xl hover:bg-white/20 focus:outline-none"
>
Create new key
</Link>
@@ -57,6 +58,6 @@ export function WelcomeScreen() {
<div className="flex flex-1 items-end justify-center pb-10">
<img src="/lume.png" alt="lume" className="h-auto w-1/3" />
</div>
</div>
</Frame>
);
}
+1 -1
View File
@@ -38,7 +38,7 @@ export function ChatsListItem({ pubkey }: { pubkey: string }) {
/>
<div className="inline-flex w-full flex-1 items-center justify-between">
<h5 className="max-w-[10rem] truncate">
{user?.nip05 || user?.name || user?.display_name || displayNpub(pubkey, 16)}
{user?.name || user?.display_name || displayNpub(pubkey, 16)}
</h5>
</div>
</NavLink>
-4
View File
@@ -36,10 +36,6 @@ export function ChatsList() {
<div className="relative h-7 w-7 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
<div className="h-4 w-full animate-pulse rounded bg-white/10 backdrop-blur-xl" />
</div>
<div className="inline-flex h-10 items-center gap-2.5 border-l-2 border-transparent pl-4">
<div className="relative h-7 w-7 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
<div className="h-4 w-full animate-pulse rounded bg-white/10 backdrop-blur-xl" />
</div>
</div>
);
}
+1 -1
View File
@@ -35,7 +35,7 @@ export function NewMessageModal() {
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
+12 -3
View File
@@ -1,6 +1,7 @@
import { Link } from 'react-router-dom';
import { Image } from '@shared/image';
import { NIP05 } from '@shared/nip05';
import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey';
@@ -23,9 +24,17 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
<h3 className="text-lg font-semibold leading-none">
{user?.display_name || user?.name}
</h3>
<h5 className="leading-none text-white/50">
{user?.nip05 || displayNpub(pubkey, 16)}
</h5>
{user?.nip05 ? (
<NIP05
pubkey={pubkey}
nip05={user?.nip05}
className="leading-none text-white/50"
/>
) : (
<span className="leading-none text-white/50">
{displayNpub(pubkey, 16)}
</span>
)}
</div>
<div>
<p className="leading-tight">{user?.bio || user?.about}</p>
+1 -1
View File
@@ -35,7 +35,7 @@ export function UnknownsModal({ data }: { data: string[] }) {
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
+26 -13
View File
@@ -1,40 +1,53 @@
import { useEffect, useState } from 'react';
import { useLocation, useRouteError } from 'react-router-dom';
interface IRouteError {
import { Frame } from '@shared/frame';
interface RouteError {
statusText: string;
message: string;
}
interface IDebugInfo {
interface DebugInfo {
os: null | string;
version: null | string;
appDir: null | string;
}
export function ErrorScreen() {
const error = useRouteError() as IRouteError;
const error = useRouteError() as RouteError;
const location = useLocation();
const [debugInfo, setDebugInfo] = useState<IDebugInfo>({ os: null, version: null });
const [debugInfo, setDebugInfo] = useState<DebugInfo>({
os: null,
version: null,
appDir: null,
});
useEffect(() => {
async function getInformation() {
const { platform, version } = await import('@tauri-apps/plugin-os');
const { getVersion } = await import('@tauri-apps/plugin-app');
const { platform, version } = await import('@tauri-apps/api/os');
const { getVersion } = await import('@tauri-apps/api/app');
const { appConfigDir } = await import('@tauri-apps/api/path');
const platformName = await platform();
const osVersion = await version();
const appVersion = await getVersion();
const appDir = await appConfigDir();
setDebugInfo({ os: platformName + ' ' + osVersion, version: appVersion });
setDebugInfo({
os: platformName + ' ' + osVersion,
version: appVersion,
appDir: appDir,
});
}
getInformation();
}, []);
return (
<div className="flex h-full items-center justify-center bg-black/90 backdrop-blur-xl">
<div className="flex max-w-lg flex-col gap-4">
<Frame className="flex h-full items-center justify-center">
<div className="flex w-full flex-col gap-4 px-4 md:max-w-lg md:px-0">
<div className="flex flex-col">
<h1 className="mb-1 text-2xl font-semibold text-white">
Sorry, an unexpected error has occurred.
@@ -57,24 +70,24 @@ export function ErrorScreen() {
href="https://github.com/luminous-devs/lume/issues/new"
target="_blank"
rel="noreferrer"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-white/10 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/20"
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
>
Click here to report the issue on GitHub
</a>
<button
type="button"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-white/10 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/20"
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
>
Reload app
</button>
<button
type="button"
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-white/10 text-sm font-medium text-white backdrop-blur-xl hover:bg-white/20"
className="inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium text-white backdrop-blur-xl hover:bg-white/10"
>
Reset app
</button>
</div>
</div>
</div>
</Frame>
);
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { writeText } from '@tauri-apps/api/clipboard';
import { nip19 } from 'nostr-tools';
import { EventPointer } from 'nostr-tools/lib/nip19';
import { useRef, useState } from 'react';
+2 -2
View File
@@ -1,5 +1,5 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { writeText } from '@tauri-apps/api/clipboard';
import { nip19 } from 'nostr-tools';
import { EventPointer } from 'nostr-tools/lib/nip19';
import { useRef, useState } from 'react';
@@ -51,7 +51,7 @@ export function TextNoteScreen() {
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote event={event} />;
return <TextNote content={event.content} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:
@@ -18,7 +18,7 @@ export const SimpleNote = memo(function SimpleNote({ id }: { id: string }) {
const openThread = (event, thread: string) => {
const selection = window.getSelection();
if (selection.toString().length === 0) {
setWidget(db, { kind: WidgetKinds.thread, title: 'Thread', content: thread });
setWidget(db, { kind: WidgetKinds.local.thread, title: 'Thread', content: thread });
} else {
event.stopPropagation();
}
+1 -1
View File
@@ -25,7 +25,7 @@ export function NotiUser({ pubkey }: { pubkey: string }) {
className="h-11 w-11 shrink-0 rounded-lg object-cover"
/>
<span className="max-w-[10rem] flex-1 truncate font-medium leading-none text-white">
{user?.nip05 || user?.name || user?.display_name || displayNpub(pubkey, 16)}
{user?.name || user?.display_name || displayNpub(pubkey, 16)}
</span>
</div>
);
+1 -1
View File
@@ -1,4 +1,4 @@
import { getVersion } from '@tauri-apps/plugin-app';
import { getVersion } from '@tauri-apps/api/app';
import { useEffect, useState } from 'react';
export function VersionSetting() {
+76 -7
View File
@@ -2,9 +2,17 @@ import { useCallback } from 'react';
import { useStorage } from '@libs/storage/provider';
import {
ArticleIcon,
FileIcon,
FollowsIcon,
GroupFeedsIcon,
HashtagIcon,
TrendingIcon,
} from '@shared/icons';
import { TitleBar } from '@shared/titleBar';
import { DefaultWidgets, useWidgets } from '@stores/widgets';
import { DefaultWidgets, WidgetKinds, useWidgets } from '@stores/widgets';
import { Widget, WidgetGroup, WidgetGroupItem } from '@utils/types';
@@ -20,16 +28,61 @@ export function WidgetList({ params }: { params: Widget }) {
removeWidget(db, params.id);
};
const renderIcon = useCallback(
(kind: number) => {
switch (kind) {
case WidgetKinds.tmp.xfeed:
return <GroupFeedsIcon className="h-5 w-5 text-white" />;
case WidgetKinds.local.follows:
return <FollowsIcon className="h-5 w-5 text-white" />;
case WidgetKinds.local.files:
case WidgetKinds.global.files:
return <FileIcon className="h-5 w-5 text-white" />;
case WidgetKinds.local.articles:
case WidgetKinds.global.articles:
return <ArticleIcon className="h-5 w-5 text-white" />;
case WidgetKinds.tmp.xhashtag:
return <HashtagIcon className="h-5 w-4 text-white" />;
case WidgetKinds.nostrBand.trendingAccounts:
case WidgetKinds.nostrBand.trendingNotes:
return <TrendingIcon className="h-5 w-4 text-white" />;
default:
return '';
}
},
[DefaultWidgets]
);
const renderItem = useCallback(
(row: WidgetGroup) => {
return (
<div className="flex flex-col gap-3">
<h3 className="font-medium text-white/50">{row.title}</h3>
<div className="grid grid-cols-3 gap-6">
<div className="flex flex-col divide-y divide-white/5 overflow-hidden rounded-xl bg-white/10">
{row.data.map((item, index) => (
<button onClick={() => openWidget(item)} key={index}>
<div className="inline-flex aspect-square h-full w-full transform-gpu flex-col items-center justify-center gap-2.5 rounded-2xl bg-white/5 hover:bg-white/10">
<h5 className="text-sm font-medium">{item.title}</h5>
<button
onClick={() => openWidget(item)}
key={index}
className="flex items-center gap-2.5 px-4 hover:bg-white/10"
>
{item.icon ? (
<div className="h-10 w-10 shrink-0 rounded-md">
<img
src={item.icon}
alt={item.title}
className="h-10 w-10 object-cover"
/>
</div>
) : (
<div className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-white/10">
{renderIcon(item.kind)}
</div>
)}
<div className="inline-flex h-16 w-full flex-col items-start justify-center gap-1">
<h5 className="line-clamp-1 font-medium leading-none">{item.title}</h5>
<p className="line-clamp-1 text-xs leading-none text-white/50">
{item.description}
</p>
</div>
</button>
))}
@@ -41,11 +94,27 @@ export function WidgetList({ params }: { params: Widget }) {
);
return (
<div className="relative h-full shrink-0 grow-0 basis-[400px] overflow-hidden">
<div className="relative h-full shrink-0 grow-0 basis-[400px] overflow-hidden bg-white/10">
<TitleBar id={params.id} title="Add widget" />
<div className="flex flex-col gap-8 px-3">
<div className="flex flex-col gap-6 px-3">
{DefaultWidgets.map((row: WidgetGroup) => renderItem(row))}
</div>
<div className="mt-6 px-3">
<div className="border-t border-white/5 pt-6">
<button
type="button"
disabled
className="inline-flex h-14 w-full items-center justify-center gap-2.5 rounded-xl bg-white/5 text-sm font-medium text-white/50"
>
Build your own widget{' '}
<div className="-rotate-3 transform rounded-md border border-white/20 bg-white/10 px-1.5 py-1">
<span className="bg-gradient-to-t from-fuchsia-200 via-red-200 to-orange-300 bg-clip-text text-xs text-transparent">
Coming soon
</span>
</div>
</button>
</div>
</div>
</div>
);
}
+9 -9
View File
@@ -1,3 +1,4 @@
import { message } from '@tauri-apps/api/dialog';
import { invoke } from '@tauri-apps/api/tauri';
import { useEffect, useState } from 'react';
@@ -10,7 +11,7 @@ import { useNostr } from '@utils/hooks/useNostr';
export function SplashScreen() {
const { db } = useStorage();
const { ndk, relayUrls } = useNDK();
const { ndk } = useNDK();
const { fetchUserData, prefetchEvents } = useNostr();
const [isLoading, setIsLoading] = useState<boolean>(true);
@@ -40,16 +41,17 @@ export function SplashScreen() {
} catch (e) {
setIsLoading(false);
setErrorMessage(e);
console.log('prefetch failed, error: ', e);
await message(`Something wrong: ${e}`, {
title: 'Lume',
type: 'error',
});
}
};
useEffect(() => {
if (!db.account) {
invoke('close_splashscreen');
}
if (ndk) {
if (!db.account) invoke('close_splashscreen');
if (ndk && db.account) {
console.log('prefetching...');
prefetch();
}
@@ -64,9 +66,7 @@ export function SplashScreen() {
{isLoading ? (
<div className="flex flex-col gap-1 text-center">
<h3 className="text-lg font-semibold leading-none text-white">
{!ndk
? 'Connecting to relay...'
: `Connected to ${relayUrls.length} relays`}
{!ndk ? 'Connecting to relay...' : 'Fetching events from the last login.'}
</h3>
<p className="text-sm text-white/50">
This may take a few seconds, please don&apos;t close app.
+21 -7
View File
@@ -1,6 +1,7 @@
import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk';
import * as Dialog from '@radix-ui/react-dialog';
import { useQueryClient } from '@tanstack/react-query';
import { fetch } from '@tauri-apps/api/http';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
@@ -13,6 +14,12 @@ import { Image } from '@shared/image';
import { useNostr } from '@utils/hooks/useNostr';
interface NIP05 {
names: {
[key: string]: string;
};
}
export function EditProfileModal() {
const queryClient = useQueryClient();
@@ -47,19 +54,26 @@ export function EditProfileModal() {
});
const verifyNIP05 = async (nip05: string) => {
const url = nip05.split('@');
const username = url[0];
const service = url[1];
const verifyURL = `https://${service}/.well-known/nostr.json?name=${username}`;
const localPath = nip05.split('@')[0];
const service = nip05.split('@')[1];
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
const res = await fetch(verifyURL, {
method: 'GET',
timeout: 10,
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
});
console.log(res);
return true;
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
const data = res.data as NIP05;
if (data.names) {
if (data.names[localPath] !== db.account.pubkey) return false;
return true;
}
return false;
};
const onSubmit = async (data: NDKUserProfile) => {
@@ -131,7 +145,7 @@ export function EditProfileModal() {
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
+16 -7
View File
@@ -7,10 +7,11 @@ import { UserStats } from '@app/users/components/stats';
import { useStorage } from '@libs/storage/provider';
import { Image } from '@shared/image';
import { NIP05 } from '@shared/nip05';
import { useNostr } from '@utils/hooks/useNostr';
import { useProfile } from '@utils/hooks/useProfile';
import { shortenKey } from '@utils/shortenKey';
import { displayNpub } from '@utils/shortenKey';
export function UserProfile({ pubkey }: { pubkey: string }) {
const { db } = useStorage();
@@ -57,7 +58,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
className="h-full w-full object-cover"
/>
) : (
<div className="h-full w-full bg-black/50" />
<div className="h-full w-full bg-black" />
)}
</div>
<div className="-mt-7 flex w-full flex-col items-center px-5">
@@ -68,13 +69,21 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
/>
<div className="mt-2 flex flex-1 flex-col gap-6">
<div className="flex flex-col items-center gap-1">
<div className="inline-flex flex-col gap-1.5">
<div className="inline-flex flex-col items-center gap-1.5">
<h5 className="text-center text-xl font-semibold leading-none">
{user.display_name || user.displayName || user.name || 'No name'}
</h5>
<span className="max-w-[15rem] truncate text-center leading-none text-white/50">
{user.nip05 || user.username || shortenKey(pubkey)}
</span>
{user.nip05 ? (
<NIP05
pubkey={pubkey}
nip05={user?.nip05}
className="max-w-[15rem] truncate text-sm leading-none text-white/50"
/>
) : (
<span className="max-w-[15rem] truncate text-sm leading-none text-white/50">
{displayNpub(pubkey, 16)}
</span>
)}
</div>
<div className="flex flex-col gap-6">
{user.about || user.bio ? (
@@ -82,7 +91,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
{user.about || user.bio}
</p>
) : (
<></>
<div />
)}
<UserStats pubkey={pubkey} />
</div>
+1 -1
View File
@@ -56,7 +56,7 @@ export function UserScreen() {
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<TextNote event={event} />
<TextNote content={event.content} />
</NoteWrapper>
</div>
);
+6 -1
View File
@@ -6,6 +6,11 @@ html {
font-size: 14px;
}
input::-ms-reveal,
input::-ms-clear {
display: none;
}
a {
@apply cursor-default no-underline !important;
}
@@ -15,7 +20,7 @@ button {
}
.markdown {
@apply prose prose-white max-w-none select-text hyphens-auto text-white prose-p:mb-2 prose-p:mt-0 prose-p:break-words prose-p:[word-break:break-word] prose-p:last:mb-0 prose-a:break-words prose-a:break-all prose-a:font-normal hover:prose-a:text-fuchsia-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-fuchsia-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:break-all prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2;
@apply prose prose-white max-w-none select-text hyphens-auto text-white prose-p:mb-0 prose-p:mt-0 prose-p:break-words prose-p:[word-break:break-word] prose-p:last:mb-0 prose-a:break-words prose-a:break-all prose-a:font-normal hover:prose-a:text-fuchsia-500 prose-blockquote:mb-1 prose-blockquote:mt-1 prose-blockquote:border-l-[2px] prose-blockquote:border-fuchsia-500 prose-blockquote:pl-2 prose-pre:whitespace-pre-wrap prose-pre:break-words prose-pre:break-all prose-ol:m-0 prose-ol:mb-1 prose-ul:mb-1 prose-ul:mt-1 prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2;
}
.ProseMirror p.is-empty::before {
+1 -1
View File
@@ -1,6 +1,6 @@
import { NDKCacheAdapter } from '@nostr-dev-kit/ndk';
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk';
import { Store } from '@tauri-apps/plugin-store';
import { Store } from 'tauri-plugin-store-api';
export default class TauriAdapter implements NDKCacheAdapter {
public store: Store;
+23 -37
View File
@@ -1,5 +1,6 @@
// inspire by: https://github.com/nostr-dev-kit/ndk-react/
import NDK from '@nostr-dev-kit/ndk';
import { message } from '@tauri-apps/api/dialog';
import { useEffect, useMemo, useState } from 'react';
import TauriAdapter from '@libs/ndk/cache';
@@ -16,40 +17,32 @@ export const NDKInstance = () => {
const cacheAdapter = useMemo(() => new TauriAdapter(), [ndk]);
// TODO: fully support NIP-11
async function verifyRelays(relays: string[]) {
async function getExplicitRelays() {
try {
const urls: string[] = relays.map((relay) => {
if (relay.startsWith('ws')) {
return relay.replace('ws', 'http');
}
if (relay.startsWith('wss')) {
return relay.replace('wss', 'https');
}
});
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort('timeout'), 5000);
const timeoutId = setTimeout(() => controller.abort('timeout'), 10000);
const requests = urls.map((url) =>
fetch(url, {
// get relays
const relays = (await db.getExplicitRelayUrls()) ?? FULL_RELAYS;
const requests = relays.map((relay) => {
const url = new URL(relay);
return fetch(`https://${url.hostname + url.pathname}`, {
headers: { Accept: 'application/nostr+json' },
signal: controller.signal,
})
);
});
});
const responses = await Promise.all(requests);
const errors = responses.filter((response) => !response.ok);
if (errors.length > 0) {
throw errors.map((response) => Error(response.statusText));
}
if (errors.length > 0) throw errors.map((response) => Error(response.statusText));
const verifiedRelays: string[] = responses.map((res) => {
if (res.url.startsWith('http')) {
return res.url.replace('htto', 'ws');
}
if (res.url.startsWith('https')) {
return res.url.replace('https', 'wss');
}
const url = new URL(res.url);
if (url.protocol === 'http:') return `ws://${url.hostname + url.pathname}`;
if (url.protocol === 'https:') return `wss://${url.hostname + url.pathname}`;
});
// clear timeout
@@ -58,28 +51,21 @@ export const NDKInstance = () => {
// return all validate relays
return verifiedRelays;
} catch (e) {
console.error('ndk instance error: ', e);
await message(e, { title: 'Cannot connect to relays', type: 'error' });
}
}
async function initNDK() {
let explicitRelayUrls: string[];
const explicitRelayUrlsFromDB = await db.getExplicitRelayUrls();
console.log('relays in db: ', explicitRelayUrlsFromDB);
if (explicitRelayUrlsFromDB) {
explicitRelayUrls = await verifyRelays(explicitRelayUrlsFromDB);
} else {
explicitRelayUrls = await verifyRelays(FULL_RELAYS);
}
console.log('ndk cache adapter: ', cacheAdapter);
const explicitRelayUrls = await getExplicitRelays();
const instance = new NDK({ explicitRelayUrls, cacheAdapter });
try {
await instance.connect(10000);
} catch (error) {
throw new Error('NDK instance init failed: ', error);
await message(`NDK instance init failed: ${error}`, {
title: 'Lume',
type: 'error',
});
}
setNDK(instance);
+7 -4
View File
@@ -1,17 +1,20 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { BaseDirectory, removeFile } from '@tauri-apps/plugin-fs';
import Database from '@tauri-apps/plugin-sql';
import { Stronghold } from '@tauri-apps/plugin-stronghold';
import { BaseDirectory, removeFile } from '@tauri-apps/api/fs';
import { Platform } from '@tauri-apps/api/os';
import Database from 'tauri-plugin-sql-api';
import { Stronghold } from 'tauri-plugin-stronghold-api';
import { Account, DBEvent, Relays, Widget } from '@utils/types';
export class LumeStorage {
public db: Database;
public platform: Platform;
public secureDB: Stronghold;
public account: Account | null = null;
constructor(sqlite: Database, stronghold?: Stronghold) {
constructor(sqlite: Database, platform: Platform, stronghold?: Stronghold) {
this.db = sqlite;
this.platform = platform ?? undefined;
this.secureDB = stronghold ?? undefined;
this.account = null;
}
+19 -5
View File
@@ -1,5 +1,8 @@
import Database from '@tauri-apps/plugin-sql';
import { message } from '@tauri-apps/api/dialog';
import { platform } from '@tauri-apps/api/os';
import { appConfigDir } from '@tauri-apps/api/path';
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
import { LumeStorage } from '@libs/storage/instance';
@@ -15,11 +18,22 @@ const StorageProvider = ({ children }: PropsWithChildren<object>) => {
const [db, setDB] = useState<LumeStorage>(undefined);
async function initLumeStorage() {
const sqlite = await Database.load('sqlite:lume.db');
const lumeStorage = new LumeStorage(sqlite);
try {
const dir = await appConfigDir();
const sqlite = await Database.load('sqlite:lume.db');
const platformName = await platform();
const lumeStorage = new LumeStorage(sqlite, platformName);
if (!lumeStorage.account) await lumeStorage.getActiveAccount();
setDB(lumeStorage);
console.log('App config dir: ', dir);
if (!lumeStorage.account) await lumeStorage.getActiveAccount();
setDB(lumeStorage);
} catch (e) {
await message(`Cannot initialize database: ${e}`, {
title: 'Lume',
type: 'error',
});
}
}
useEffect(() => {
+1 -1
View File
@@ -67,7 +67,7 @@ export function ActiveAccount() {
/>
<div className="flex w-full flex-1 flex-col items-start gap-1.5">
<p className="max-w-[10rem] truncate font-bold leading-none text-white">
{user?.name || user?.display_name || user?.nip05}
{user?.name || user?.display_name}
</p>
<span className="max-w-[8rem] truncate text-sm leading-none text-white/50">
{displayNpub(db.account.pubkey, 16)}
+2 -4
View File
@@ -7,9 +7,7 @@ import { useImageUploader } from '@utils/hooks/useUploader';
export function BannerUploader({
setBanner,
}: {
setBanner: Dispatch<
SetStateAction<{ url: undefined | string; error?: undefined | string }>
>;
setBanner: Dispatch<SetStateAction<string>>;
}) {
const upload = useImageUploader();
const [loading, setLoading] = useState(false);
@@ -18,7 +16,7 @@ export function BannerUploader({
setLoading(true);
const image = await upload(null);
if (image.url) {
setBanner(image);
setBanner(image.url);
}
setLoading(false);
};
+32 -41
View File
@@ -1,32 +1,28 @@
import { message } from '@tauri-apps/api/dialog';
import Image from '@tiptap/extension-image';
import Mention from '@tiptap/extension-mention';
import Placeholder from '@tiptap/extension-placeholder';
import { EditorContent, useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { convert } from 'html-to-text';
import { nip19 } from 'nostr-tools';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
import { Suggestion } from '@shared/composer';
import { CancelIcon, LoaderIcon, PlusCircleIcon } from '@shared/icons';
import { MediaUploader, MentionPopup } from '@shared/composer';
import { CancelIcon, LoaderIcon } from '@shared/icons';
import { MentionNote } from '@shared/notes';
import { useComposer } from '@stores/composer';
import { useNostr } from '@utils/hooks/useNostr';
import { useImageUploader } from '@utils/hooks/useUploader';
import { sendNativeNotification } from '@utils/notification';
export function Composer() {
const { publish } = useNostr();
const [status, setStatus] = useState<null | 'loading' | 'done'>(null);
const [loading, setLoading] = useState<boolean>(false);
const [reply, clearReply] = useComposer((state) => [state.reply, state.clearReply]);
const expand = useComposer((state) => state.expand)
const upload = useImageUploader();
const { publish } = useNostr();
const expand = useComposer((state) => state.expand);
const editor = useEditor({
extensions: [
StarterKit.configure({
@@ -35,12 +31,6 @@ export function Composer() {
},
}),
Placeholder.configure({ placeholder: 'Type something...' }),
Mention.configure({
suggestion: Suggestion,
renderLabel({ node }) {
return `nostr:${nip19.npubEncode(node.attrs.id.pubkey)} `;
},
}),
Image.configure({
HTMLAttributes: {
class:
@@ -48,26 +38,22 @@ export function Composer() {
},
}),
],
content: '',
content: JSON.parse(localStorage.getItem('editor-content') || '{}'),
editorProps: {
attributes: {
class: 'h-full markdown break-all overflow-y-auto outline-none pr-2',
},
},
onUpdate: ({ editor }) => {
const jsonContent = JSON.stringify(editor.getJSON());
localStorage.setItem('editor-content', jsonContent);
},
});
const uploadImage = async (file?: string) => {
const image = await upload(file, true);
if (image.url) {
editor.commands.setImage({ src: image.url });
editor.commands.createParagraphNear();
}
};
const submit = async () => {
setStatus('loading');
try {
setLoading(true);
// get plaintext content
const html = editor.getHTML();
const serializedContent = convert(html, {
@@ -108,18 +94,19 @@ export function Composer() {
await publish({ content: serializedContent, kind: 1, tags });
// send native notifiation
await sendNativeNotification('Publish post successfully');
await sendNativeNotification('Post has been published successfully.');
// update state
setStatus('done');
setLoading(false);
// reset editor
editor.commands.clearContent();
// reset reply
if (reply.id) {
clearReply();
}
} catch {
setStatus(null);
console.log('failed to publish');
setLoading(false);
await message('Publishing post failed.', { title: 'Lume', type: 'error' });
}
};
@@ -136,7 +123,10 @@ export function Composer() {
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
className={twMerge('scrollbar-hide markdown break-all max-h-[500px] overflow-y-auto outline-none pr-2', expand ? 'min-h-[500px]' : 'min-h-[120px]')}
className={twMerge(
'scrollbar-hide markdown max-h-[500px] overflow-y-auto break-all pr-2 outline-none',
expand ? 'min-h-[500px]' : reply.id ? 'min-h-min' : 'min-h-[120px]'
)}
/>
{reply.id && (
<div className="relative">
@@ -152,17 +142,18 @@ export function Composer() {
)}
</div>
</div>
<div className="flex items-center justify-between bg-white/5 rounded-b-xl p-2 border-t border-white/10">
<div className="flex items-center justify-between rounded-b-xl border-t border-white/10 bg-white/5 p-2">
<div className="inline-flex items-center gap-1">
<MediaUploader editor={editor} />
<MentionPopup editor={editor} />
</div>
<button
type="button"
onClick={() => uploadImage()}
className="ml-2 inline-flex h-10 w-10 items-center justify-center rounded-lg backdrop-blur-xl hover:bg-white/10"
onClick={() => submit()}
disabled={editor && editor.isEmpty}
className="inline-flex h-10 w-20 items-center justify-center rounded-lg bg-fuchsia-500 px-2 font-semibold hover:bg-fuchsia-600 disabled:opacity-50"
>
<PlusCircleIcon className="h-5 w-5 text-white" />
</button>
<button onClick={() => submit()} className="inline-flex items-center justify-center w-max px-8 rounded-lg font-bold h-10 bg-fuchsia-500 hover:bg-fuchsia-600">
{status === 'loading' ? (
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
{loading === true ? (
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
) : (
'Post'
)}
@@ -1,6 +1,6 @@
export * from './user';
export * from './modal';
export * from './composer';
export * from './mention/list';
export * from './mention/item';
export * from './mention/suggestion';
export * from './mention/popup';
export * from './mediaUploader';
+62
View File
@@ -0,0 +1,62 @@
import { message } from '@tauri-apps/api/dialog';
import { UnlistenFn, listen } from '@tauri-apps/api/event';
import { Editor } from '@tiptap/react';
import { useEffect, useState } from 'react';
import { MediaIcon } from '@shared/icons';
import { useNostr } from '@utils/hooks/useNostr';
export function MediaUploader({ editor }: { editor: Editor }) {
const { upload } = useNostr();
const [loading, setLoading] = useState(false);
const uploadToNostrBuild = async (file?: string) => {
try {
// start loading
setLoading(true);
const image = await upload(file, true);
if (image.url) {
editor.commands.setImage({ src: image.url });
editor.commands.createParagraphNear();
// stop loading
setLoading(false);
}
} catch (e) {
// stop loading
setLoading(false);
await message(`Upload failed, error: ${e}`, { title: 'Lume', type: 'error' });
}
};
useEffect(() => {
let unlisten: UnlistenFn;
async function listenDnD() {
unlisten = await listen('tauri://file-drop', (event) => {
uploadToNostrBuild(event.payload[0]);
});
}
// start listen drag and drop event
listenDnD();
// clean up
return () => {
unlisten();
};
}, []);
return (
<button
type="button"
onClick={() => uploadToNostrBuild()}
className="ml-2 inline-flex h-10 w-max items-center justify-center gap-1.5 rounded-lg px-2 text-sm font-medium text-white/80 hover:bg-white/10 hover:backdrop-blur-xl"
>
<MediaIcon className="h-5 w-5 text-white/80" />
{loading ? 'Uploading...' : 'Add media'}
</button>
);
}
+23 -9
View File
@@ -1,22 +1,36 @@
import { Image } from '@shared/image';
import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey';
import { Profile } from '@utils/types';
export function MentionItem({ profile }: { profile: Profile }) {
export function MentionItem({ pubkey }: { pubkey: string }) {
const { status, user } = useProfile(pubkey);
if (status === 'loading') {
return (
<div className="flex items-center gap-2">
<div className="relative h-8 w-8 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
<span className="h-4 w-1/2 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
<span className="h-3 w-1/3 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
</div>
</div>
);
}
return (
<div className="flex items-center gap-2">
<div className="flex h-11 items-center justify-start gap-2.5 px-2 hover:bg-white/10">
<Image
src={profile.picture || profile.image}
alt={profile.pubkey}
src={user.picture || user.image}
alt={pubkey}
className="shirnk-0 h-8 w-8 rounded-md object-cover"
/>
<div className="flex flex-col gap-px">
<h5 className="max-w-[15rem] text-sm font-medium leading-none text-white">
{profile.ident}
<div className="flex flex-col items-start gap-px">
<h5 className="max-w-[10rem] truncate text-sm font-medium leading-none text-white">
{user.display_name || user.displayName || user.name}
</h5>
<span className="text-sm leading-none text-white/50">
{displayNpub(profile.pubkey, 16)}
{displayNpub(pubkey, 16)}
</span>
</div>
</div>
-75
View File
@@ -1,75 +0,0 @@
import { NDKUserProfile } from '@nostr-dev-kit/ndk';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { twMerge } from 'tailwind-merge';
import { MentionItem } from '@shared/composer';
export const MentionList = forwardRef((props: any, ref: any) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index) => {
const item = props.items[index];
if (item) {
props.command({ id: item });
}
};
const upHandler = () => {
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
};
const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length);
};
const enterHandler = () => {
selectItem(selectedIndex);
};
useEffect(() => setSelectedIndex(0), [props.items]);
useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (event.key === 'ArrowUp') {
upHandler();
return true;
}
if (event.key === 'ArrowDown') {
downHandler();
return true;
}
if (event.key === 'Enter') {
enterHandler();
return true;
}
return false;
},
}));
return (
<div className="flex w-[250px] flex-col rounded-xl border-t border-zinc-700/50 bg-zinc-800 px-3 py-3">
{props.items.length ? (
props.items.map((item: NDKUserProfile, index: number) => (
<button
className={twMerge(
'h-11 w-full rounded-lg px-2 text-start text-sm font-medium hover:bg-zinc-700',
`${index === selectedIndex ? 'is-selected' : ''}`
)}
key={index}
onClick={() => selectItem(index)}
>
<MentionItem profile={item} />
</button>
))
) : (
<div>No result</div>
)}
</div>
);
});
MentionList.displayName = 'MentionList';
+38
View File
@@ -0,0 +1,38 @@
import * as Popover from '@radix-ui/react-popover';
import { Editor } from '@tiptap/react';
import { nip19 } from 'nostr-tools';
import { useStorage } from '@libs/storage/provider';
import { MentionItem } from '@shared/composer';
import { MentionIcon } from '@shared/icons';
export function MentionPopup({ editor }: { editor: Editor }) {
const { db } = useStorage();
const insertMention = (pubkey: string) => {
editor.commands.insertContent(`nostr:${nip19.npubEncode(pubkey)}`);
};
return (
<Popover.Root>
<Popover.Trigger asChild>
<button
type="button"
className="inline-flex h-10 w-10 items-center justify-center rounded-lg hover:bg-white/10 hover:backdrop-blur-xl"
>
<MentionIcon className="h-5 w-5 text-white/80" />
</button>
</Popover.Trigger>
<Popover.Content className="h-full max-h-[200px] w-[250px] overflow-hidden overflow-y-auto rounded-lg bg-white/10 backdrop-blur-xl focus:outline-none">
<div className="flex flex-col gap-1 py-1">
{db.account.follows.map((item) => (
<button key={item} type="button" onClick={() => insertMention(item)}>
<MentionItem pubkey={item} />
</button>
))}
</div>
</Popover.Content>
</Popover.Root>
);
}
@@ -1,68 +0,0 @@
import { ReactRenderer } from '@tiptap/react';
import tippy from 'tippy.js';
import { MentionList } from '@shared/composer';
export const Suggestion = {
items: async ({ query }) => {
const users = [];
return users
.filter((item) => item.ident.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5);
},
render: () => {
let component;
let popup;
return {
onStart: (props) => {
component = new ReactRenderer(MentionList, {
props,
editor: props.editor,
});
if (!props.clientRect) {
return;
}
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
});
},
onUpdate(props) {
component.updateProps(props);
if (!props.clientRect) {
return;
}
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup[0].hide();
return true;
}
return component.ref?.onKeyDown(props);
},
onExit() {
popup[0].destroy();
component.destroy();
},
};
},
};
+1 -1
View File
@@ -37,7 +37,7 @@ export function ComposerModal() {
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div
className={twMerge(
+2 -3
View File
@@ -1,6 +1,7 @@
import { Image } from '@shared/image';
import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey';
export function ComposerUser({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey);
@@ -13,9 +14,7 @@ export function ComposerUser({ pubkey }: { pubkey: string }) {
className="h-10 w-10 shrink-0 rounded-lg"
/>
<h5 className="text-base font-semibold leading-none text-white">
{user?.nip05 || user?.name || (
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)}
{user?.name || user?.display_name || displayNpub(pubkey, 16)}
</h5>
</div>
);
+28
View File
@@ -0,0 +1,28 @@
import { HTMLProps, ReactNode, useCallback } from 'react';
import { twMerge } from 'tailwind-merge';
import { useStorage } from '@libs/storage/provider';
export function Frame({
children,
className,
lighter,
}: {
children: ReactNode;
className: HTMLProps<HTMLElement>['className'];
lighter?: boolean;
}) {
const { db } = useStorage();
const platformStyles = useCallback(() => {
switch (db.platform) {
case 'darwin':
if (lighter) return 'bg-black/80';
return 'bg-black/90';
default:
return 'bg-black';
}
}, []);
return <div className={twMerge(className, platformStyles())}>{children}</div>;
}
+22
View File
@@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function ArticleIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M16.25 12V4.75a1 1 0 00-1-1H3.75a1 1 0 00-1 1v13a2.5 2.5 0 002.5 2.5H18.5M16.25 12v5.75a2.5 2.5 0 005 0V13a1 1 0 00-1-1h-4zm-9.5 3.75h5.5m-5.5-8h5.5v4.5h-5.5v-4.5z"
></path>
</svg>
);
}
+2 -3
View File
@@ -13,10 +13,9 @@ export function FileIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M8.75 6.75h6.5m-6.5 4h6.5m-6.5 4h2.5m-5.5 6.5h12.5a1 1 0 001-1V3.75a1 1 0 00-1-1H5.75a1 1 0 00-1 1v16.5a1 1 0 001 1z"
/>
d="M12.75 3.25v5a1 1 0 001 1h5m-10 4h3.5m-3.5 4h6.5m-9.5-14.5h6.586a1 1 0 01.707.293l5.914 5.914a1 1 0 01.293.707V20.25a1 1 0 01-1 1H5.75a1 1 0 01-1-1V3.75a1 1 0 011-1z"
></path>
</svg>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function FollowsIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M17.75 19.25h3.673c.581 0 1.045-.496.947-1.07-.531-3.118-2.351-5.43-5.37-5.43-.446 0-.866.05-1.26.147M11.25 7a3.25 3.25 0 11-6.5 0 3.25 3.25 0 016.5 0zm8.5.5a2.75 2.75 0 11-5.5 0 2.75 2.75 0 015.5 0zM1.87 19.18c.568-3.68 2.647-6.43 6.13-6.43 3.482 0 5.561 2.75 6.13 6.43.088.575-.375 1.07-.956 1.07H2.825c-.58 0-1.043-.495-.955-1.07z"
></path>
</svg>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function GroupFeedsIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M14.425 13.18c3.361-1.396 7.598.605 8.454 6.003.09.573-.372 1.067-.952 1.067H16.75M10.75 7a3.25 3.25 0 11-6.5 0 3.25 3.25 0 016.5 0zm9 0a3.25 3.25 0 11-6.5 0 3.25 3.25 0 016.5 0zm-6.966 13.25H2.072c-.58 0-1.042-.497-.951-1.07 1.362-8.573 11.252-8.573 12.614 0 .091.573-.371 1.07-.951 1.07z"
></path>
</svg>
);
}
+1 -1
View File
@@ -16,7 +16,7 @@ export function HashtagIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElem
strokeLinejoin="round"
strokeWidth="1.5"
d="M8.75 3.75l-2 16.5m10.5-16.5l-2 16.5M3.75 7.75h16.5m0 8.5H3.75"
/>
></path>
</svg>
);
}
+5
View File
@@ -56,3 +56,8 @@ export * from './expand';
export * from './focus';
export * from './chevronUp';
export * from './secure';
export * from './verified';
export * from './mention';
export * from './groupFeeds';
export * from './article';
export * from './follows';
+22
View File
@@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function MentionIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M11.85 13.251c-3.719.065-6.427 2.567-7.18 5.915-.13.575.338 1.084.927 1.084h6.901m-.647-6.999l.147-.001c.353 0 .696.022 1.03.064m-1.177-.063a7.889 7.889 0 00-1.852.249m3.028-.186c.334.042.658.104.972.186m-.972-.186a7.475 7.475 0 011.972.524m3.25 1.412v3m0 0v3m0-3h-3m3 0h3m-5.5-11.75a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
></path>
</svg>
);
}
+21
View File
@@ -0,0 +1,21 @@
import { SVGProps } from 'react';
export function VerifiedIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm3.58 7.975a.75.75 0 00-1.16-.95l-3.976 4.859L9.03 12.47a.75.75 0 00-1.06 1.06l2 2a.75.75 0 001.11-.055l4.5-5.5z"
clipRule="evenodd"
/>
</svg>
);
}
+3 -2
View File
@@ -1,5 +1,6 @@
import { Outlet, ScrollRestoration } from 'react-router-dom';
import { Frame } from '@shared/frame';
import { Navigation } from '@shared/navigation';
export function AppLayout() {
@@ -8,14 +9,14 @@ export function AppLayout() {
<div className="shrink-0">
<Navigation />
</div>
<div className="h-full w-full flex-1 bg-black/90 backdrop-blur-xl">
<Frame className="h-full w-full flex-1">
<Outlet />
<ScrollRestoration
getKey={(location) => {
return location.pathname;
}}
/>
</div>
</Frame>
</div>
);
}
+4 -2
View File
@@ -1,10 +1,12 @@
import { Outlet } from 'react-router-dom';
import { Frame } from '@shared/frame';
export function AuthLayout() {
return (
<div className="relative h-screen w-screen bg-black/90 backdrop-blur-xl">
<Frame className="relative h-screen w-screen">
<div className="absolute left-0 top-0 z-50 h-16 w-full" data-tauri-drag-region />
<Outlet />
</div>
</Frame>
);
}
+4 -2
View File
@@ -1,10 +1,12 @@
import { Outlet } from 'react-router-dom';
import { Frame } from '@shared/frame';
export function NoteLayout() {
return (
<div className="relative h-screen w-screen bg-black/90 backdrop-blur-xl">
<Frame className="relative h-screen w-screen">
<div className="absolute left-0 top-0 z-50 h-16 w-full" data-tauri-drag-region />
<Outlet />
</div>
</Frame>
);
}
+3 -2
View File
@@ -1,12 +1,13 @@
import { Link, NavLink, Outlet, ScrollRestoration } from 'react-router-dom';
import { twMerge } from 'tailwind-merge';
import { Frame } from '@shared/frame';
import { ArrowLeftIcon, SecureIcon, SettingsIcon } from '@shared/icons';
export function SettingsLayout() {
return (
<div className="flex h-screen w-screen">
<div className="relative flex h-full w-[232px] flex-col bg-black/80">
<Frame className="relative flex h-full w-[232px] flex-col" lighter>
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
<div className="scrollbar-hide flex h-full flex-1 flex-col gap-2 overflow-y-auto pb-32">
<div className="inline-flex items-center gap-2 border-l-2 border-transparent pl-4">
@@ -55,7 +56,7 @@ export function SettingsLayout() {
</NavLink>
</div>
</div>
</div>
</Frame>
<div className="h-full w-full flex-1 bg-black/90 backdrop-blur-xl">
<Outlet />
<ScrollRestoration
-3
View File
@@ -1,6 +1,5 @@
import * as Dialog from '@radix-ui/react-dialog';
import { useQueryClient } from '@tanstack/react-query';
import { relaunch } from '@tauri-apps/plugin-process';
import { CancelIcon, LogoutIcon } from '@shared/icons';
@@ -12,8 +11,6 @@ export function Logout() {
// await removeAll();
// reset react query
queryClient.clear();
// navigate
await relaunch();
};
return (
+3 -2
View File
@@ -6,6 +6,7 @@ import { ChatsList } from '@app/chats/components/list';
import { ActiveAccount } from '@shared/accounts/active';
import { ComposerModal } from '@shared/composer';
import { Frame } from '@shared/frame';
import { BellIcon, NavArrowDownIcon, SpaceIcon } from '@shared/icons';
import { useSidebar } from '@stores/sidebar';
@@ -14,7 +15,7 @@ export function Navigation() {
const [chats, toggleChats] = useSidebar((state) => [state.chats, state.toggleChats]);
return (
<div className="relative flex h-full w-[232px] flex-col bg-black/80">
<Frame className="relative flex h-full w-[232px] flex-col" lighter>
<div data-tauri-drag-region className="h-11 w-full shrink-0" />
<div className="scrollbar-hide flex h-full flex-1 flex-col gap-6 overflow-y-auto pb-32">
<div className="flex flex-col pr-2">
@@ -80,6 +81,6 @@ export function Navigation() {
<div className="shrink-0">
<ActiveAccount />
</div>
</div>
</Frame>
);
}
+74
View File
@@ -0,0 +1,74 @@
import { useQuery } from '@tanstack/react-query';
import { fetch } from '@tauri-apps/api/http';
import { twMerge } from 'tailwind-merge';
import { UnverifiedIcon, VerifiedIcon } from '@shared/icons';
interface NIP05 {
names: {
[key: string]: string;
};
}
export function NIP05({
pubkey,
nip05,
className,
}: {
pubkey: string;
nip05: string;
className?: string;
}) {
const { status, data } = useQuery(
['nip05', nip05],
async () => {
try {
const localPath = nip05.split('@')[0];
const service = nip05.split('@')[1];
const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`;
const res = await fetch(verifyURL, {
method: 'GET',
timeout: 10,
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
});
if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`);
const data = res.data as NIP05;
if (data.names) {
if (data.names[localPath] !== pubkey) return false;
return true;
}
return false;
} catch (e) {
throw new Error(`Failed to verify NIP-05, error: ${e}`);
}
},
{
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
}
);
if (status === 'loading') {
<div className="h-3 w-20 animate-pulse rounded bg-white/10" />;
}
return (
<div className={twMerge('leadning-none inline-flex items-center gap-1', className)}>
<p>{nip05}</p>
<div className="shrink-0">
{data === true ? (
<VerifiedIcon className="h-3 w-3 text-green-500" />
) : (
<UnverifiedIcon className="h-3 w-3 text-red-500" />
)}
</div>
</div>
);
}
+3 -3
View File
@@ -28,7 +28,7 @@ export function NoteActions({
return (
<Tooltip.Provider>
<div className="-ml-1 mt-4 inline-flex w-full items-center">
<div className="-ml-1 mt-2 inline-flex w-full items-center">
<div className="inline-flex items-center gap-2">
<NoteReply id={id} pubkey={pubkey} root={root} />
<NoteReaction id={id} pubkey={pubkey} />
@@ -50,7 +50,7 @@ export function NoteActions({
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="-left-10 select-none rounded-md bg-black px-3.5 py-1.5 text-sm leading-none text-white will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Open thread
Focus
<Tooltip.Arrow className="fill-black" />
</Tooltip.Content>
</Tooltip.Portal>
@@ -61,7 +61,7 @@ export function NoteActions({
type="button"
onClick={() =>
setWidget(db, {
kind: WidgetKinds.thread,
kind: WidgetKinds.local.thread,
title: 'Thread',
content: id,
})
+1 -1
View File
@@ -1,6 +1,6 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import * as Tooltip from '@radix-ui/react-tooltip';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { writeText } from '@tauri-apps/api/clipboard';
import { nip19 } from 'nostr-tools';
import { EventPointer } from 'nostr-tools/lib/nip19';
import { useState } from 'react';
+1 -1
View File
@@ -44,7 +44,7 @@ export function NoteRepost({ id, pubkey }: { id: string; pubkey: string }) {
</Tooltip.Portal>
</Tooltip.Root>
<AlertDialog.Portal className="relative z-10">
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
<AlertDialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
<div className="flex flex-col gap-2 border-b border-white/5 px-5 py-4">
+1 -1
View File
@@ -38,7 +38,7 @@ export function NoteZap({ id }: { id: string }) {
</button>
</Dialog.Trigger>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
<div className="relative h-min w-full shrink-0 border-b border-white/5 bg-white/5 px-5 py-5">
+49 -22
View File
@@ -1,8 +1,11 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { nip19 } from 'nostr-tools';
import { Link } from 'react-router-dom';
import {
ArticleNote,
FileNote,
LinkPreview,
NoteActions,
NoteSkeleton,
TextNote,
@@ -15,31 +18,10 @@ import { useEvent } from '@utils/hooks/useEvent';
export function ChildNote({ id, root }: { id: string; root?: string }) {
const { status, data } = useEvent(id);
if (status === 'loading') {
return (
<div className="relative mb-5 overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
);
}
if (status === 'error') {
return (
<div className="mb-5 flex flex-col gap-1.5 overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<p className="text-sm font-bold text-white">
Lume cannot found the event with ID
</p>
<div className="inline-flex items-center justify-center rounded-xl border border-dashed border-red-400 bg-red-200/10 p-2">
<p className="select-text break-all text-sm font-medium text-red-400">{id}</p>
</div>
</div>
);
}
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote event={event} />;
return <TextNote content={event.content} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:
@@ -49,6 +31,51 @@ export function ChildNote({ id, root }: { id: string; root?: string }) {
}
};
if (status === 'loading') {
return (
<>
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" />
<div className="relative mb-5 overflow-hidden">
<NoteSkeleton />
</div>
</>
);
}
if (status === 'error') {
const noteLink = `https://nostr.com/${nip19.noteEncode(id)}`;
return (
<>
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" />
<div className="relative mb-5 flex flex-col">
<div className="relative z-10 flex items-start gap-3">
<div className="inline-flex h-11 w-11 items-end justify-center rounded-lg bg-black pb-1">
<img src="/lume.png" alt="lume" className="h-auto w-1/3" />
</div>
<h5 className="truncate font-semibold leading-none text-white">
Lume <span className="text-green-500">(System)</span>
</h5>
</div>
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div>
<div className="relative z-20 mt-1 flex-1 select-text">
<div className="mb-1 select-text rounded-lg bg-white/5 p-1.5 text-sm">
Lume cannot find this post with your current relays, but you can view it
via nostr.com.{' '}
<Link to={noteLink} className="text-fuchsia-500">
Learn more
</Link>
</div>
</div>
<LinkPreview urls={[noteLink]} />
</div>
</div>
</div>
</>
);
}
return (
<>
<div className="absolute bottom-0 left-[18px] h-[calc(100%-3.4rem)] w-0.5 bg-gradient-to-t from-white/20 to-white/10" />
-47
View File
@@ -1,47 +0,0 @@
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import {
Hashtag,
ImagePreview,
LinkPreview,
MentionNote,
MentionUser,
VideoPreview,
} from '@shared/notes';
import { RichContent } from '@utils/types';
export function NoteContent({ content, long }: { content: RichContent; long?: boolean }) {
if (long) {
return (
<ReactMarkdown className="markdown" remarkPlugins={[remarkGfm]}>
{content as unknown as string}
</ReactMarkdown>
);
}
return (
<>
<ReactMarkdown
className="markdown"
remarkPlugins={[remarkGfm]}
components={{
del: ({ children }) => {
const key = children[0] as string;
if (key.startsWith('pub') && key.length > 50 && key.length < 100)
return <MentionUser pubkey={key.replace('pub-', '')} />;
if (key.startsWith('tag')) return <Hashtag tag={key.replace('tag-', '')} />;
},
}}
>
{content?.parsed}
</ReactMarkdown>
{content?.images?.length > 0 && <ImagePreview urls={content.images} />}
{content?.videos?.length > 0 && <VideoPreview urls={content.videos} />}
{content?.links?.length > 0 && <LinkPreview urls={content.links} />}
{content?.notes?.length > 0 &&
content?.notes.map((note: string) => <MentionNote key={note} id={note} />)}
</>
);
}
@@ -24,7 +24,6 @@ export * from './kinds/repost';
export * from './child';
export * from './skeleton';
export * from './actions';
export * from './content';
export * from './mentions/hashtag';
export * from './stats';
export * from './wrapper';
+2 -6
View File
@@ -1,13 +1,9 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom';
import remarkGfm from 'remark-gfm';
import { Image } from '@shared/image';
export function ArticleDetailNote({ event }: { event: NDKEvent }) {
const metadata = useMemo(() => {
/*const metadata = useMemo(() => {
const title = event.tags.find((tag) => tag[0] === 'title')?.[1];
const image = event.tags.find((tag) => tag[0] === 'image')?.[1];
const summary = event.tags.find((tag) => tag[0] === 'summary')?.[1];
@@ -27,7 +23,7 @@ export function ArticleDetailNote({ event }: { event: NDKEvent }) {
publishedAt,
summary,
};
}, [event.id]);
}, [event.id]);*/
return (
<ReactMarkdown className="markdown" remarkPlugins={[remarkGfm]}>
+49 -27
View File
@@ -1,10 +1,13 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { nip19 } from 'nostr-tools';
import { useCallback } from 'react';
import { Link } from 'react-router-dom';
import {
ArticleNote,
FileNote,
LinkPreview,
NoteActions,
NoteMetadata,
NoteSkeleton,
RepostUser,
TextNote,
@@ -15,8 +18,24 @@ import { User } from '@shared/user';
import { useEvent } from '@utils/hooks/useEvent';
export function Repost({ event }: { event: NDKEvent }) {
const repostID = event.tags.find((el) => el[0] === 'e')?.[1];
const { status, data } = useEvent(repostID, event.content as unknown as string);
// @ts-expect-error, root_id isn't exist on NDKEvent
const { status, data } = useEvent(event.root_id, event.content);
const renderKind = useCallback(
(repostEvent: NDKEvent) => {
switch (repostEvent.kind) {
case NDKKind.Text:
return <TextNote content={repostEvent.content} />;
case NDKKind.Article:
return <ArticleNote event={repostEvent} />;
case 1063:
return <FileNote event={repostEvent} />;
default:
return <UnknownNote event={repostEvent} />;
}
},
[data]
);
if (status === 'loading') {
return (
@@ -29,49 +48,52 @@ export function Repost({ event }: { event: NDKEvent }) {
}
if (status === 'error') {
// @ts-expect-error, root_id isn't exist on NDKEvent
const noteLink = `https://nostr.com/${nip19.noteEncode(event.root_id)}`;
return (
<div className="h-min w-full px-3 pb-3">
<div className="flex flex-col gap-1 overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<p className="select-text break-all text-white/50">
Failed to get repost with ID
</p>
<div className="break-all rounded-lg bg-white/10 px-2 py-2 backdrop-blur-xl">
<p className="text-white">{repostID}</p>
<div className="relative mb-5 flex flex-col">
<div className="relative z-10 flex items-start gap-3">
<div className="inline-flex h-11 w-11 items-end justify-center rounded-lg bg-black pb-1">
<img src="/lume.png" alt="lume" className="h-auto w-1/3" />
</div>
<h5 className="truncate font-semibold leading-none text-white">
Lume <span className="text-green-500">(System)</span>
</h5>
</div>
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div>
<div className="relative z-20 mt-1 flex-1 select-text">
<div className="mb-1 select-text rounded-lg bg-white/5 p-1.5 text-sm">
Lume cannot find this post with your current relays, but you can view it
via nostr.com.{' '}
<Link to={noteLink} className="text-fuchsia-500">
Learn more
</Link>
</div>
</div>
<LinkPreview urls={[noteLink]} />
</div>
</div>
</div>
);
}
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote event={event} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:
return <FileNote event={event} />;
default:
return <UnknownNote event={event} />;
}
};
return (
<div className="h-min w-full px-3 pb-3">
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl">
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<div className="relative flex flex-col">
<div className="isolate flex flex-col -space-y-4">
<RepostUser pubkey={event.pubkey} />
<User pubkey={data.pubkey} time={data.created_at} isRepost={true} />
</div>
<div className="flex items-start gap-3">
<div className="-mt-2 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="relative z-20 flex-1">
{renderKind(data)}
<NoteActions id={repostID} pubkey={data.pubkey} />
<NoteActions id={data.id} pubkey={data.pubkey} />
</div>
</div>
<NoteMetadata id={repostID} />
</div>
</div>
</div>
+36 -10
View File
@@ -1,6 +1,5 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom';
import remarkGfm from 'remark-gfm';
import {
@@ -14,8 +13,24 @@ import {
import { parser } from '@utils/parser';
export function TextNote({ event }: { event: NDKEvent }) {
const content = useMemo(() => parser(event), [event.id]);
export function TextNote({ content }: { content: string }) {
const richContent = parser(content) ?? null;
if (!richContent) {
return (
<div>
<ReactMarkdown
className="markdown"
remarkPlugins={[remarkGfm]}
disallowedElements={['h1', 'h2', 'h3', 'h4', 'h5', 'h6']}
unwrapDisallowed={true}
linkTarget={'_blank'}
>
{content}
</ReactMarkdown>
</div>
);
}
return (
<div>
@@ -23,6 +38,15 @@ export function TextNote({ event }: { event: NDKEvent }) {
className="markdown"
remarkPlugins={[remarkGfm]}
components={{
a: ({ href }) => {
const cleanURL = new URL(href);
cleanURL.search = '';
return (
<Link to={href} target="_blank">
{cleanURL.hostname + cleanURL.pathname}
</Link>
);
},
del: ({ children }) => {
const key = children[0] as string;
if (typeof key !== 'string') return;
@@ -38,13 +62,15 @@ export function TextNote({ event }: { event: NDKEvent }) {
unwrapDisallowed={true}
linkTarget={'_blank'}
>
{content?.parsed}
{richContent.parsed}
</ReactMarkdown>
{content?.images?.length > 0 && <ImagePreview urls={content.images} />}
{content?.videos?.length > 0 && <VideoPreview urls={content.videos} />}
{content?.links?.length > 0 && <LinkPreview urls={content.links} />}
{content?.notes?.length > 0 &&
content?.notes.map((note: string) => <MentionNote key={note} id={note} />)}
<div>
{richContent.images.length > 0 && <ImagePreview urls={richContent.images} />}
{richContent.videos.length > 0 && <VideoPreview urls={richContent.videos} />}
{richContent.links.length > 0 && <LinkPreview urls={richContent.links} />}
{richContent.notes.length > 0 &&
richContent.notes.map((note: string) => <MentionNote key={note} id={note} />)}
</div>
</div>
);
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
export function UnknownNote({ event }: { event: NDKEvent }) {
return (
<div className="mb-3 mt-3 flex w-full flex-col gap-2">
<div className="flex w-full flex-col gap-2">
<div className="inline-flex flex-col gap-1 rounded-md bg-white/10 px-2 py-2 backdrop-blur-xl">
<span className="text-sm font-medium leading-none text-white/50">
Unknown kind: {event.kind}
+11 -3
View File
@@ -7,8 +7,9 @@ export function Hashtag({ tag }: { tag: string }) {
const setWidget = useWidgets((state) => state.setWidget);
return (
<button
type="button"
<span
role="button"
tabIndex={0}
onClick={() =>
setWidget(db, {
kind: WidgetKinds.global.hashtag,
@@ -16,9 +17,16 @@ export function Hashtag({ tag }: { tag: string }) {
content: tag.replace('#', ''),
})
}
onKeyDown={() =>
setWidget(db, {
kind: WidgetKinds.global.hashtag,
title: tag,
content: tag.replace('#', ''),
})
}
className="break-words text-fuchsia-400 hover:text-fuchsia-500"
>
{tag}
</button>
</span>
);
}
+45 -19
View File
@@ -1,11 +1,14 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { nip19 } from 'nostr-tools';
import { memo } from 'react';
import { Link } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import {
ArticleNote,
FileNote,
LinkPreview,
NoteSkeleton,
TextNote,
UnknownNote,
@@ -31,26 +34,10 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
}
};
if (status === 'loading') {
return (
<div className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
);
}
if (status === 'error') {
return (
<div className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl">
<p>Can&apos;t get event from relay</p>
</div>
);
}
const renderKind = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote event={event} />;
return <TextNote content={event.content} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:
@@ -60,16 +47,55 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
}
};
if (status === 'loading') {
return (
<div className="mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
);
}
if (status === 'error') {
const noteLink = `https://nostr.com/${nip19.noteEncode(id)}`;
return (
<div className="relative mb-5 flex flex-col">
<div className="relative z-10 flex items-start gap-3">
<div className="inline-flex h-11 w-11 items-end justify-center rounded-lg bg-black pb-1">
<img src="/lume.png" alt="lume" className="h-auto w-1/3" />
</div>
<h5 className="truncate font-semibold leading-none text-white">
Lume <span className="text-green-500">(System)</span>
</h5>
</div>
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div>
<div className="relative z-20 mt-1 flex-1 select-text">
<div className="mb-1 select-text rounded-lg bg-white/5 p-1.5 text-sm">
Lume cannot find this post with your current relays, but you can view it
via nostr.com.{' '}
<Link to={noteLink} className="text-fuchsia-500">
Learn more
</Link>
</div>
</div>
<LinkPreview urls={[noteLink]} />
</div>
</div>
</div>
);
}
return (
<div
onClick={(e) => openThread(e, id)}
onKeyDown={(e) => openThread(e, id)}
role="button"
tabIndex={0}
className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl"
className="mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl"
>
<User pubkey={data.pubkey} time={data.created_at} size="small" />
<div>{renderKind(data)}</div>
<div className="mt-1">{renderKind(data)}</div>
</div>
);
});
+13 -9
View File
@@ -12,22 +12,26 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
const setWidget = useWidgets((state) => state.setWidget);
return (
<button
type="button"
<span
role="button"
tabIndex={0}
onClick={() =>
setWidget(db, {
kind: WidgetKinds.local.user,
title: user?.nip05 || user?.name || user?.display_name,
title: user?.name || user?.display_name,
content: pubkey,
})
}
onKeyDown={() =>
setWidget(db, {
kind: WidgetKinds.local.user,
title: user?.name || user?.display_name,
content: pubkey,
})
}
className="break-words text-fuchsia-400 hover:text-fuchsia-500"
>
{user?.nip05 ||
user?.name ||
user?.display_name ||
user?.username ||
displayNpub(pubkey, 16)}
</button>
{user?.name || user?.display_name || user?.username || displayNpub(pubkey, 16)}
</span>
);
}
+1 -1
View File
@@ -95,7 +95,7 @@ export function NoteMetadata({ id }: { id: string }) {
type="button"
onClick={() =>
setWidget(db, {
kind: WidgetKinds.thread,
kind: WidgetKinds.local.thread,
title: 'Thread',
content: id,
})
+2 -2
View File
@@ -1,5 +1,5 @@
import { downloadDir } from '@tauri-apps/api/path';
import { download } from '@tauri-apps/plugin-upload';
import { download } from 'tauri-plugin-upload-api';
import { DownloadIcon } from '@shared/icons';
import { Image } from '@shared/image';
@@ -12,7 +12,7 @@ export function ImagePreview({ urls, truncate }: { urls: string[]; truncate?: bo
};
return (
<div className="mb-2 mt-3 overflow-hidden">
<div className="mt-3 overflow-hidden">
<div className="flex flex-col gap-2">
{urls.map((url) => (
<div key={url} className="group relative min-w-0 shrink-0 grow-0 basis-full">
+10 -8
View File
@@ -1,3 +1,5 @@
import { Link } from 'react-router-dom';
import { Image } from '@shared/image';
import { useOpenGraph } from '@utils/hooks/useOpenGraph';
@@ -7,7 +9,7 @@ export function LinkPreview({ urls }: { urls: string[] }) {
const domain = new URL(urls[0]);
return (
<div className="mb-2 mt-3 max-w-[420px] overflow-hidden rounded-lg bg-white/10 backdrop-blur-xl">
<div className="mt-3 overflow-hidden rounded-lg bg-white/10 backdrop-blur-xl">
{status === 'loading' ? (
<div className="flex flex-col">
<div className="h-44 w-full animate-pulse bg-white/10 backdrop-blur-xl" />
@@ -20,11 +22,11 @@ export function LinkPreview({ urls }: { urls: string[] }) {
</div>
</div>
) : (
<a
className="flex flex-col rounded-lg"
href={urls[0]}
<Link
to={urls[0]}
target="_blank"
rel="noreferrer"
className="flex flex-col rounded-lg"
>
{error ? (
<div className="flex flex-col gap-2 px-3 py-3">
@@ -42,8 +44,8 @@ export function LinkPreview({ urls }: { urls: string[] }) {
className="h-44 w-full rounded-t-lg object-cover"
/>
)}
<div className="flex flex-col gap-2 px-3 py-3">
<h5 className="line-clamp-1 font-medium leading-none text-white">
<div className="flex flex-col gap-1 border-t border-white/5 px-3 py-3">
<h5 className="line-clamp-1 font-semibold leading-none text-white">
{data.title}
</h5>
{data.description && (
@@ -51,13 +53,13 @@ export function LinkPreview({ urls }: { urls: string[] }) {
{data.description}
</p>
)}
<span className="mt-2.5 text-sm leading-none text-white/50">
<span className="mt-2.5 text-sm leading-none text-white/80">
{domain.hostname}
</span>
</div>
</>
)}
</a>
</Link>
)}
</div>
);
+1 -1
View File
@@ -2,7 +2,7 @@ import ReactPlayer from 'react-player/es6';
export function VideoPreview({ urls }: { urls: string[] }) {
return (
<div className="relative mb-2 mt-3 flex w-full flex-col gap-2">
<div className="relative mt-3 flex w-full flex-col gap-2">
{urls.map((url) => (
<ReactPlayer
key={url}
+1 -1
View File
@@ -52,7 +52,7 @@ export function NoteReplyForm({ id, pubkey }: { id: string; pubkey: string }) {
<div>
<p className="mb-1 text-sm leading-none text-white/50">Reply as</p>
<p className="text-sm font-medium leading-none text-white">
{user?.nip05 || user?.name || displayNpub(pubkey, 16)}
{user?.name || displayNpub(pubkey, 16)}
</p>
</div>
</div>
+1 -1
View File
@@ -15,7 +15,7 @@ export function Reply({ event, root }: { event: NDKEventWithReplies; root?: stri
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="flex-1">
<TextNote event={event} />
<TextNote content={event.content} />
<NoteActions id={event.id} pubkey={event.pubkey} root={root} />
</div>
</div>
+1 -1
View File
@@ -10,7 +10,7 @@ export function SubReply({ event }: { event: NDKEvent }) {
<div className="-mt-6 flex items-start gap-3">
<div className="w-11 shrink-0" />
<div className="flex-1">
<TextNote event={event} />
<TextNote content={event.content} />
<NoteActions id={event.id} pubkey={event.pubkey} />
</div>
</div>
+1 -1
View File
@@ -19,7 +19,7 @@ export function RepostUser({ pubkey }: { pubkey: string }) {
/>
<div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[18rem] truncate text-white/50">
{user?.nip05 || user?.name || user?.display_name || shortenKey(pubkey)}
{user?.name || user?.display_name || shortenKey(pubkey)}
</h5>
<span className="text-white/50">reposted</span>
</div>
+2 -5
View File
@@ -1,26 +1,24 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { ReactNode } from 'react';
import { ChildNote, NoteActions, NoteMetadata } from '@shared/notes';
import { ChildNote, NoteActions } from '@shared/notes';
import { User } from '@shared/user';
export function NoteWrapper({
event,
children,
meta = true,
root,
reply,
}: {
event: NDKEvent;
children: ReactNode;
repost?: boolean;
meta?: boolean;
root?: string;
reply?: string;
}) {
return (
<div className="h-min w-full px-3 pb-3">
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 pt-3 backdrop-blur-xl">
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<div className="relative">{root && <ChildNote id={root} />}</div>
<div className="relative">{reply && <ChildNote id={reply} root={root} />}</div>
<div className="relative flex flex-col">
@@ -32,7 +30,6 @@ export function NoteWrapper({
<NoteActions id={event.id} pubkey={event.pubkey} />
</div>
</div>
{meta ? <NoteMetadata id={event.id} /> : <div className="pb-3" />}
</div>
</div>
</div>
+15 -6
View File
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { twMerge } from 'tailwind-merge';
import { Image } from '@shared/image';
import { NIP05 } from '@shared/nip05';
import { formatCreatedAt } from '@utils/createdAt';
import { useProfile } from '@utils/hooks/useProfile';
@@ -22,8 +23,8 @@ export function User({
isChat?: boolean;
}) {
const { status, user } = useProfile(pubkey);
const createdAt = formatCreatedAt(time, isChat);
const createdAt = formatCreatedAt(time, isChat);
const avatarWidth = size === 'small' ? 'w-6' : 'w-11';
const avatarHeight = size === 'small' ? 'h-6' : 'h-11';
@@ -79,7 +80,7 @@ export function User({
size === 'small' ? 'max-w-[10rem]' : 'max-w-[15rem]'
)}
>
{user?.nip05 || user?.name || user?.display_name || displayNpub(pubkey, 16)}
{user?.display_name || user?.name || displayNpub(pubkey, 16)}
</h5>
<span className="leading-none text-white/50">·</span>
<span className="leading-none text-white/50">{createdAt}</span>
@@ -87,7 +88,7 @@ export function User({
</div>
<Popover.Portal>
<Popover.Content
className="w-[300px] overflow-hidden rounded-md bg-white/10 backdrop-blur-3xl backdrop-blur-xl focus:outline-none"
className="w-[300px] overflow-hidden rounded-lg bg-white/10 backdrop-blur-3xl focus:outline-none"
sideOffset={5}
>
<div className="flex gap-2.5 border-b border-white/5 px-3 py-3">
@@ -101,9 +102,17 @@ export function User({
<h5 className="text-sm font-semibold leading-none">
{user?.display_name || user?.name || user?.username}
</h5>
<span className="max-w-[10rem] truncate text-sm leading-none text-white/50">
{user?.nip05 || displayNpub(pubkey, 16)}
</span>
{user.nip05 ? (
<NIP05
pubkey={pubkey}
nip05={user.nip05}
className="max-w-[15rem] truncate text-sm leading-none text-white/50"
/>
) : (
<span className="max-w-[15rem] truncate text-sm leading-none text-white/50">
{displayNpub(pubkey, 16)}
</span>
)}
</div>
<div>
<p className="line-clamp-3 break-all leading-tight text-white">
+12 -3
View File
@@ -6,6 +6,7 @@ import { UserStats } from '@app/users/components/stats';
import { useStorage } from '@libs/storage/provider';
import { Image } from '@shared/image';
import { NIP05 } from '@shared/nip05';
import { useNostr } from '@utils/hooks/useNostr';
import { useProfile } from '@utils/hooks/useProfile';
@@ -58,9 +59,17 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
<h5 className="text-lg font-semibold leading-none">
{user?.displayName || user?.name || 'No name'}
</h5>
<span className="max-w-[15rem] truncate text-sm leading-none text-white/50">
{user?.nip05 || displayNpub(pubkey, 16)}
</span>
{user?.nip05 ? (
<NIP05
pubkey={pubkey}
nip05={user?.nip05}
className="max-w-[15rem] truncate text-sm leading-none text-white/50"
/>
) : (
<span className="max-w-[15rem] truncate text-sm leading-none text-white/50">
{displayNpub(pubkey, 16)}
</span>
)}
</div>
<div className="flex flex-col gap-4">
<p className="mt-2 max-w-[500px] select-text break-words text-white">
+1 -1
View File
@@ -58,7 +58,7 @@ export function GlobalHashtagWidget({ params }: { params: Widget }) {
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<TextNote event={event} />
<TextNote content={event.content} />
</NoteWrapper>
</div>
);
+1 -1
View File
@@ -60,7 +60,7 @@ export function LocalFeedsWidget({ params }: { params: Widget }) {
ref={virtualizer.measureElement}
>
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}>
<TextNote event={event} />
<TextNote content={event.content} />
</NoteWrapper>
</div>
);
+198
View File
@@ -0,0 +1,198 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useMemo, useRef } from 'react';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import {
ArticleNote,
FileNote,
NoteWrapper,
Repost,
TextNote,
UnknownNote,
} from '@shared/notes';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
import { DBEvent } from '@utils/types';
export function LocalFollowsWidget() {
const { db } = useStorage();
const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ['local-follows-widget'],
queryFn: async ({ pageParam = 0 }) => {
return await db.getAllEventsByAuthors(db.account.follows, 20, pageParam);
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
const dbEvents = useMemo(
() => (data ? data.pages.flatMap((d: { data: DBEvent[] }) => d.data) : []),
[data]
);
const parentRef = useRef<HTMLDivElement>();
const virtualizer = useVirtualizer({
count: hasNextPage ? dbEvents.length : dbEvents.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 650,
overscan: 4,
});
const items = virtualizer.getVirtualItems();
// render event match event kind
const renderItem = useCallback(
(index: string | number) => {
const dbEvent: DBEvent = dbEvents[index];
if (!dbEvent) return;
const event: NDKEvent = JSON.parse(dbEvent.event as string);
switch (event.kind) {
case NDKKind.Text:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}>
<TextNote content={event.content} />
</NoteWrapper>
</div>
);
case NDKKind.Repost:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<Repost key={dbEvent.id} event={event} />
</div>
);
case 1063:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<FileNote event={event} />
</NoteWrapper>
</div>
);
case NDKKind.Article:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<ArticleNote event={event} />
</NoteWrapper>
</div>
);
default:
return (
<div
key={dbEvent.id + index}
data-index={index}
ref={virtualizer.measureElement}
>
<NoteWrapper event={event}>
<UnknownNote event={event} />
</NoteWrapper>
</div>
);
}
},
[dbEvents]
);
return (
<div className="relative shrink-0 grow-0 basis-[400px] bg-white/10 backdrop-blur-xl">
<TitleBar title="👋 Network" />
<div ref={parentRef} className="scrollbar-hide h-full overflow-y-auto pb-20">
{status === 'loading' ? (
<div className="px-3 py-1.5">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
) : dbEvents.length === 0 ? (
<div className="flex h-full w-full flex-col items-center justify-center px-3">
<div className="flex flex-col items-center gap-4">
<img src="/ghost.png" alt="empty feeds" className="h-16 w-16" />
<div className="text-center">
<h3 className="text-xl font-semibold leading-tight">
Your newsfeed is empty
</h3>
<p className="text-center text-white/50">
Connect more people to explore more content
</p>
</div>
</div>
</div>
) : (
<div
style={{
position: 'relative',
width: '100%',
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0].start}px)`,
}}
>
{items.map((item) => renderItem(item.index))}
</div>
</div>
)}
{isFetchingNextPage && (
<div className="mb-20 px-3">
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
<NoteSkeleton />
</div>
</div>
)}
<div className="px-3 py-1.5">
{dbEvents.length > 0 ? (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-11 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"
>
{isFetchingNextPage ? (
<>
<span className="w-5" />
<span>Loading...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : hasNextPage ? (
<>
<span className="w-5" />
<span>Load more</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
) : (
<>
<span className="w-5" />
<span>Nothing more to load</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
) : null}
</div>
</div>
</div>
);
}
+3 -3
View File
@@ -28,7 +28,7 @@ export function LocalNetworkWidget() {
useInfiniteQuery({
queryKey: ['local-network-widget'],
queryFn: async ({ pageParam = 0 }) => {
return await db.getAllEvents(30, pageParam);
return await db.getAllEvents(20, pageParam);
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
@@ -62,7 +62,7 @@ export function LocalNetworkWidget() {
ref={virtualizer.measureElement}
>
<NoteWrapper event={event} root={dbEvent.root_id} reply={dbEvent.reply_id}>
<TextNote event={event} />
<TextNote content={event.content} />
</NoteWrapper>
</div>
);
@@ -122,7 +122,7 @@ export function LocalNetworkWidget() {
useEffect(() => {
if (db.account && db.account.network) {
const filter: NDKFilter = {
kinds: [1, 6],
kinds: [NDKKind.Text, NDKKind.Repost],
authors: db.account.network,
since: db.account.last_login_at ?? Math.floor(Date.now() / 1000),
};
+1 -1
View File
@@ -28,7 +28,7 @@ export function LocalThreadWidget({ params }: { params: Widget }) {
(event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote event={event} />;
return <TextNote content={event.content} />;
case NDKKind.Article:
return <ArticleNote event={event} />;
case 1063:

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